【SpringBoot 基礎系列】實現一個自定義配置加載器(應用篇)
Spring 中提供了@Value註解,用來綁定配置,可以實現從配置文件中,讀取對應的配置並賦值給成員變量;某些時候,我們的配置可能並不是在配置文件中,如存在 db/redis/其他文件/第三方配置服務,本文將手把手教你實現一個自定義的配置加載器,並支持@Value的使用姿勢
自定義的配置加載,有兩個核心的角色
上面@MetaVal提到了兩點,一個是初始化,一個是配置的刷新,接下來可以看一下如何支持這兩點
初始化的前提是需要獲取到所有修飾有這個註解的成員,然後藉助MetaValHolder來獲取對應的配置,並初始化
為了實現上面這一點,最好的切入點是在 Bean 對象創建之後,獲取 bean 的所有屬性,查看是否標有這個註解,可以藉助InstantiationAwareBeanPostProcessorAdapter來實現
當配置發生變更時,我們也希望綁定的屬性也會隨之改變,因此我們需要保存配置與bean屬性之間的綁定關係
配置變更 與 bean屬性的刷新 這兩個操作,我們可以藉助 Spring 的事件機制來解耦,當配置變更時,拋出一個MetaChangeEvent事件,我們默認提供一個事件處理器,用於更新通過@MetaVal註解綁定的 bean 屬性
使用事件除了解耦之外,另一個好處是更加靈活,如支持用戶對配置使用的擴展
提供配置與 bean 屬性的綁定關係,我們這裡僅提供一個根據配置名獲取配置的基礎功能,有興趣的小夥伴可以自行擴展支持 SPEL
@Target({ElementType.FIELD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface MetaVal { /** * 獲取配置的規則 * * @return */ String value() default &34;; /** * 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); } };}
提供配置的核心類,我們這裡只定義了一個接口,具體的配置獲取與業務需求相關
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; }}
這個類,主要提供掃描所有的 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);這一行
配置容器,保存配置與 field 映射關係,提供配置的基本操作
@Slf4jpublic 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(&34;, 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() + &34; + field.getName(); this.update(value); } public void update(String value) throws IllegalAccessException { this.value = this.metaVal.parser().parse(value); field.set(target, this.value); } }}
接下來就是事件通知機制的支持了
MetaChangeEvent 配置變更事件,提供基本的三個信息,配置 key,原 value,新 value
@ToString@EqualsAndHashCodepublic 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()); }}
上面五步,一個自定義的配置加載器基本上就完成了,剩下的就是 bean 的聲明
@Configurationpublic 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
上面完成基本功能,接下來進入測試環節,自定義一個配置加載
@Componentpublic class MetaPropertyHolder extends AbstractMetaValHolder { public Map<String, String> metas = new HashMap<>(8); { metas.put(&34;, &34;); metas.put(&34;, &34;); metas.put(&34;, &34;); } @Override public String getProperty(String key) { return metas.getOrDefault(key, &34;); } @Override public String doUpdateProperty(String key, String value) { return metas.put(key, value); }}
一個使用MetaVal的 demoBean
@Componentpublic class DemoBean { @MetaVal(&34;) private String name; @MetaVal(&34;) private String blog; @MetaVal(value = &34;, parser = MetaParser.INT_PARSER) private Integer age; public String sayHello() { return &34; + name + &34; + blog + &34; + age; }}
一個簡單的 REST 服務,用於查看/更新配置
@RestControllerpublic class DemoAction { @Autowired private DemoBean demoBean; @Autowired private MetaPropertyHolder metaPropertyHolder; @GetMapping(path = &34;) public String hello() { return demoBean.sayHello(); } @GetMapping(path = &34;) public String updateBlog(@RequestParam(name = &34;) String key, @RequestParam(name = &34;) String val, HttpServletResponse response) throws IOException { metaPropertyHolder.updateProperty(key, val); response.sendRedirect(&34;); return &34;; }}
啟動類
@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class); }}
動圖演示配置獲取和刷新過程
配置刷新時,會有日誌輸出,如下
工程源碼
推薦博文
一灰灰blog