【SpringBoot 基礎系列】實現一個自定義配置載入器(應用篇)

【SpringBoot 基礎系列】實現一個自定義配置載入器(應用篇)

Spring 中提供了@Value註解,用來綁定配置,可以實現從配置文件中,讀取對應的配置並賦值給成員變數;某些時候,我們的配置可能並不是在配置文件中,如存在 db/redis/其他文件/第三方配置服務,本文將手把手教你實現一個自定義的配置載入器,並支援@Value的使用姿勢

I. 環境 & 方案設計

1. 環境

  • SpringBoot 2.2.1.RELEASE
  • IDEA + JDK8

2. 方案設計

自定義的配置載入,有兩個核心的角色

  • 配置容器 MetaValHolder:與具體的配置打交道並提供配置
  • 配置綁定 @MetaVal:類似@Value註解,用於綁定類屬性與具體的配置,並實現配置初始化與配置變更時的刷新

上面@MetaVal提到了兩點,一個是初始化,一個是配置的刷新,接下來可以看一下如何支援這兩點

a. 初始化

初始化的前提是需要獲取到所有修飾有這個註解的成員,然後藉助MetaValHolder來獲取對應的配置,並初始化

為了實現上面這一點,最好的切入點是在 Bean 對象創建之後,獲取 bean 的所有屬性,查看是否標有這個註解,可以藉助InstantiationAwareBeanPostProcessorAdapter來實現

b. 刷新

當配置發生變更時,我們也希望綁定的屬性也會隨之改變,因此我們需要保存配置bean屬性之間的綁定關係

配置變更bean屬性的刷新 這兩個操作,我們可以藉助 Spring 的事件機制來解耦,當配置變更時,拋出一個MetaChangeEvent事件,我們默認提供一個事件處理器,用於更新通過@MetaVal註解綁定的 bean 屬性

使用事件除了解耦之外,另一個好處是更加靈活,如支援用戶對配置使用的擴展

II. 實現

1. MetaVal 註解

提供配置與 bean 屬性的綁定關係,我們這裡僅提供一個根據配置名獲取配置的基礎功能,有興趣的小夥伴可以自行擴展支援 SPEL

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MetaVal {

    /**
     * 獲取配置的規則
     *
     * @return
     */
    String value() default "";

    /**
     * meta value轉換目標對象;目前提供基本數據類型支援
     *
     * @return
     */
    MetaParser parser() default MetaParser.STRING_PARSER;
}

請注意上面的實現,除了 value 之外,還有一個 parser,因為我們的配置 value 可能是 String,當然也可能是其他的基本類型如 int,boolean;所以提供了一個基本的類型轉換器

public interface IMetaParser<T> {
    T parse(String val);
}

public enum MetaParser implements IMetaParser {
    STRING_PARSER {
        @Override
        public String parse(String val) {
            return val;
        }
    },

    SHORT_PARSER {
        @Override
        public Short parse(String val) {
            return Short.valueOf(val);
        }
    },

    INT_PARSER {
        @Override
        public Integer parse(String val) {
            return Integer.valueOf(val);
        }
    },

    LONG_PARSER {
        @Override
        public Long parse(String val) {
            return Long.valueOf(val);
        }
    },

    FLOAT_PARSER {
        @Override
        public Object parse(String val) {
            return null;
        }
    },

    DOUBLE_PARSER {
        @Override
        public Object parse(String val) {
            return Double.valueOf(val);
        }
    },

    BYTE_PARSER {
        @Override
        public Byte parse(String val) {
            if (val == null) {
                return null;
            }
            return Byte.valueOf(val);
        }
    },

    CHARACTER_PARSER {
        @Override
        public Character parse(String val) {
            if (val == null) {
                return null;
            }
            return val.charAt(0);
        }
    },

    BOOLEAN_PARSER {
        @Override
        public Boolean parse(String val) {
            return Boolean.valueOf(val);
        }
    };
}

2. MetaValHolder

提供配置的核心類,我們這裡只定義了一個介面,具體的配置獲取與業務需求相關

public interface MetaValHolder {
    /**
     * 獲取配置
     *
     * @param key
     * @return
     */
    String getProperty(String key);
}

為了支援配置刷新,我們提供一個基於 Spring 事件通知機制的抽象類

public abstract class AbstractMetaValHolder implements MetaValHolder, ApplicationContextAware {

    protected ApplicationContext applicationContext;

    public void updateProperty(String key, String value) {
        String old = this.doUpdateProperty(key, value);
        this.applicationContext.publishEvent(new MetaChangeEvent(this, key, old, value));
    }

    /**
     * 更新配置
     *
     * @param key
     * @param value
     * @return
     */
    public abstract String doUpdateProperty(String key, String value);

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

3. MetaValueRegister 配置綁定與初始化

這個類,主要提供掃描所有的 bean,並獲取到@MetaVal修飾的屬性,並初始化

public class MetaValueRegister extends InstantiationAwareBeanPostProcessorAdapter {

    private MetaContainer metaContainer;

    public MetaValueRegister(MetaContainer metaContainer) {
        this.metaContainer = metaContainer;
    }

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        processMetaValue(bean);
        return super.postProcessAfterInstantiation(bean, beanName);
    }

    /**
     * 掃描bean的所有屬性,並獲取@MetaVal修飾的屬性
     * @param bean
     */
    private void processMetaValue(Object bean) {
        try {
            Class clz = bean.getClass();
            MetaVal metaVal;
            for (Field field : clz.getDeclaredFields()) {
                metaVal = field.getAnnotation(MetaVal.class);
                if (metaVal != null) {
                    // 快取配置與Field的綁定關係,並初始化
                    metaContainer.addInvokeCell(metaVal, bean, field);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
}

請注意,上面核心點在metaContainer.addInvokeCell(metaVal, bean, field);這一行

4. MetaContainer

配置容器,保存配置與 field 映射關係,提供配置的基本操作

@Slf4j
public class MetaContainer {
    private MetaValHolder metaValHolder;

    // 保存配置與Field之間的綁定關係
    private Map<String, Set<InvokeCell>> metaCache = new ConcurrentHashMap<>();

    public MetaContainer(MetaValHolder metaValHolder) {
        this.metaValHolder = metaValHolder;
    }

    public String getProperty(String key) {
        return metaValHolder.getProperty(key);
    }

    // 用於新增綁定關係並初始化
    public void addInvokeCell(MetaVal metaVal, Object target, Field field) throws IllegalAccessException {
        String metaKey = metaVal.value();
        if (!metaCache.containsKey(metaKey)) {
            synchronized (this) {
                if (!metaCache.containsKey(metaKey)) {
                    metaCache.put(metaKey, new HashSet<>());
                }
            }
        }

        metaCache.get(metaKey).add(new InvokeCell(metaVal, target, field, getProperty(metaKey)));
    }

    // 配置更新
    public void updateMetaVal(String metaKey, String oldVal, String newVal) {
        Set<InvokeCell> cacheSet = metaCache.get(metaKey);
        if (CollectionUtils.isEmpty(cacheSet)) {
            return;
        }

        cacheSet.forEach(s -> {
            try {
                s.update(newVal);
                log.info("update {} from {} to {}", s.getSignature(), oldVal, newVal);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        });
    }

    @Data
    public static class InvokeCell {
        private MetaVal metaVal;

        private Object target;

        private Field field;

        private String signature;

        private Object value;

        public InvokeCell(MetaVal metaVal, Object target, Field field, String value) throws IllegalAccessException {
            this.metaVal = metaVal;
            this.target = target;
            this.field = field;
            field.setAccessible(true);
            signature = target.getClass().getName() + "." + field.getName();
            this.update(value);
        }

        public void update(String value) throws IllegalAccessException {
            this.value = this.metaVal.parser().parse(value);
            field.set(target, this.value);
        }
    }

}

5. Event/Listener

接下來就是事件通知機制的支援了

MetaChangeEvent 配置變更事件,提供基本的三個資訊,配置 key,原 value,新 value

@ToString
@EqualsAndHashCode
public class MetaChangeEvent extends ApplicationEvent {
    private static final long serialVersionUID = -9100039605582210577L;
    private String key;

    private String oldVal;

    private String newVal;


    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public MetaChangeEvent(Object source) {
        super(source);
    }

    public MetaChangeEvent(Object source, String key, String oldVal, String newVal) {
        super(source);
        this.key = key;
        this.oldVal = oldVal;
        this.newVal = newVal;
    }

    public String getKey() {
        return key;
    }

    public String getOldVal() {
        return oldVal;
    }

    public String getNewVal() {
        return newVal;
    }
}

MetaChangeListener 事件處理器,刷新@MetaVal 綁定的配置

public class MetaChangeListener implements ApplicationListener<MetaChangeEvent> {
    private MetaContainer metaContainer;

    public MetaChangeListener(MetaContainer metaContainer) {
        this.metaContainer = metaContainer;
    }

    @Override
    public void onApplicationEvent(MetaChangeEvent event) {
        metaContainer.updateMetaVal(event.getKey(), event.getOldVal(), event.getNewVal());
    }
}

6. bean 配置

上面五步,一個自定義的配置載入器基本上就完成了,剩下的就是 bean 的聲明

@Configuration
public class DynamicConfig {

    @Bean
    @ConditionalOnMissingBean(MetaValHolder.class)
    public MetaValHolder metaValHolder() {
        return key -> null;
    }

    @Bean
    public MetaContainer metaContainer(MetaValHolder metaValHolder) {
        return new MetaContainer(metaValHolder);
    }

    @Bean
    public MetaValueRegister metaValueRegister(MetaContainer metaContainer) {
        return new MetaValueRegister(metaContainer);
    }

    @Bean
    public MetaChangeListener metaChangeListener(MetaContainer metaContainer) {
        return new MetaChangeListener(metaContainer);
    }
}

以二方工具包方式提供外部使用,所以需要在資源目錄下,新建文件META-INF/spring.factories(常規套路了)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.git.hui.boot.dynamic.config.DynamicConfig

6. 測試

上面完成基本功能,接下來進入測試環節,自定義一個配置載入

@Component
public class MetaPropertyHolder extends AbstractMetaValHolder {
    public Map<String, String> metas = new HashMap<>(8);

    {
        metas.put("name", "一灰灰");
        metas.put("blog", "//blog.hhui.top");
        metas.put("age", "18");
    }

    @Override
    public String getProperty(String key) {
        return metas.getOrDefault(key, "");
    }

    @Override
    public String doUpdateProperty(String key, String value) {
        return metas.put(key, value);
    }
}

一個使用MetaVal的 demoBean

@Component
public class DemoBean {

    @MetaVal("name")
    private String name;

    @MetaVal("blog")
    private String blog;

    @MetaVal(value = "age", parser = MetaParser.INT_PARSER)
    private Integer age;

    public String sayHello() {
        return "歡迎關注 [" + name + "] 部落格:" + blog + " | " + age;
    }

}

一個簡單的 REST 服務,用於查看/更新配置

@RestController
public class DemoAction {

    @Autowired
    private DemoBean demoBean;

    @Autowired
    private MetaPropertyHolder metaPropertyHolder;

    @GetMapping(path = "hello")
    public String hello() {
        return demoBean.sayHello();
    }

    @GetMapping(path = "update")
    public String updateBlog(@RequestParam(name = "key") String key, @RequestParam(name = "val") String val,
            HttpServletResponse response) throws IOException {
        metaPropertyHolder.updateProperty(key, val);
        response.sendRedirect("/hello");
        return "over!";
    }
}

啟動類

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

動圖演示配置獲取和刷新過程

配置刷新時,會有日誌輸出,如下

II. 其他

0. 項目

工程源碼

推薦博文

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

一灰灰blog