【SpringBoot 基礎(chǔ)系列】實(shí)現(xiàn)一個(gè)自定義配置加載器(應(yīng)用篇)
Spring 中提供了@Value
注解,用來綁定配置,可以實(shí)現(xiàn)從配置文件中耕拷,讀取對應(yīng)的配置并賦值給成員變量种樱;某些時(shí)候,我們的配置可能并不是在配置文件中腕巡,如存在 db/redis/其他文件/第三方配置服務(wù)玄坦,本文將手把手教你實(shí)現(xiàn)一個(gè)自定義的配置加載器,并支持@Value
的使用姿勢
I. 環(huán)境 & 方案設(shè)計(jì)
1. 環(huán)境
- SpringBoot
2.2.1.RELEASE
- IDEA + JDK8
2. 方案設(shè)計(jì)
自定義的配置加載绘沉,有兩個(gè)核心的角色
- 配置容器
MetaValHolder
:與具體的配置打交道并提供配置 - 配置綁定
@MetaVal
:類似@Value
注解煎楣,用于綁定類屬性與具體的配置,并實(shí)現(xiàn)配置初始化與配置變更時(shí)的刷新
上面@MetaVal
提到了兩點(diǎn)车伞,一個(gè)是初始化择懂,一個(gè)是配置的刷新,接下來可以看一下如何支持這兩點(diǎn)
a. 初始化
初始化的前提是需要獲取到所有修飾有這個(gè)注解的成員另玖,然后借助MetaValHolder
來獲取對應(yīng)的配置困曙,并初始化
為了實(shí)現(xiàn)上面這一點(diǎn)表伦,最好的切入點(diǎn)是在 Bean 對象創(chuàng)建之后,獲取 bean 的所有屬性慷丽,查看是否標(biāo)有這個(gè)注解蹦哼,可以借助InstantiationAwareBeanPostProcessorAdapter
來實(shí)現(xiàn)
b. 刷新
當(dāng)配置發(fā)生變更時(shí),我們也希望綁定的屬性也會(huì)隨之改變要糊,因此我們需要保存配置
與bean屬性
之間的綁定關(guān)系
配置變更
與 bean屬性的刷新
這兩個(gè)操作纲熏,我們可以借助 Spring 的事件機(jī)制來解耦,當(dāng)配置變更時(shí)杨耙,拋出一個(gè)MetaChangeEvent
事件赤套,我們默認(rèn)提供一個(gè)事件處理器,用于更新通過@MetaVal
注解綁定的 bean 屬性
使用事件除了解耦之外珊膜,另一個(gè)好處是更加靈活容握,如支持用戶對配置使用的擴(kuò)展
II. 實(shí)現(xiàn)
1. MetaVal 注解
提供配置與 bean 屬性的綁定關(guān)系,我們這里僅提供一個(gè)根據(jù)配置名獲取配置的基礎(chǔ)功能车柠,有興趣的小伙伴可以自行擴(kuò)展支持 SPEL
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MetaVal {
/**
* 獲取配置的規(guī)則
*
* @return
*/
String value() default "";
/**
* meta value轉(zhuǎn)換目標(biāo)對象剔氏;目前提供基本數(shù)據(jù)類型支持
*
* @return
*/
MetaParser parser() default MetaParser.STRING_PARSER;
}
請注意上面的實(shí)現(xiàn),除了 value 之外竹祷,還有一個(gè) parser谈跛,因?yàn)槲覀兊呐渲?value 可能是 String,當(dāng)然也可能是其他的基本類型如 int塑陵,boolean感憾;所以提供了一個(gè)基本的類型轉(zhuǎn)換器
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
提供配置的核心類,我們這里只定義了一個(gè)接口令花,具體的配置獲取與業(yè)務(wù)需求相關(guān)
public interface MetaValHolder {
/**
* 獲取配置
*
* @param key
* @return
*/
String getProperty(String key);
}
為了支持配置刷新阻桅,我們提供一個(gè)基于 Spring 事件通知機(jī)制的抽象類
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
配置綁定與初始化
這個(gè)類,主要提供掃描所有的 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的綁定關(guān)系扮碧,并初始化
metaContainer.addInvokeCell(metaVal, bean, field);
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}
}
請注意趟章,上面核心點(diǎn)在metaContainer.addInvokeCell(metaVal, bean, field);
這一行
4. MetaContainer
配置容器,保存配置與 field 映射關(guān)系慎王,提供配置的基本操作
@Slf4j
public class MetaContainer {
private MetaValHolder metaValHolder;
// 保存配置與Field之間的綁定關(guān)系
private Map<String, Set<InvokeCell>> metaCache = new ConcurrentHashMap<>();
public MetaContainer(MetaValHolder metaValHolder) {
this.metaValHolder = metaValHolder;
}
public String getProperty(String key) {
return metaValHolder.getProperty(key);
}
// 用于新增綁定關(guān)系并初始化
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
接下來就是事件通知機(jī)制的支持了
MetaChangeEvent 配置變更事件蚓土,提供基本的三個(gè)信息,配置 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 配置
上面五步漫蛔,一個(gè)自定義的配置加載器基本上就完成了嗜愈,剩下的就是 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
(常規(guī)套路了)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.git.hui.boot.dynamic.config.DynamicConfig
6. 測試
上面完成基本功能蠕嫁,接下來進(jìn)入測試環(huán)節(jié),自定義一個(gè)配置加載
@Component
public class MetaPropertyHolder extends AbstractMetaValHolder {
public Map<String, String> metas = new HashMap<>(8);
{
metas.put("name", "一灰灰");
metas.put("blog", "https://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);
}
}
一個(gè)使用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 "歡迎關(guān)注 [" + name + "] 博客:" + blog + " | " + age;
}
}
一個(gè)簡單的 REST 服務(wù)毯盈,用于查看/更新配置
@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!";
}
}
啟動(dòng)類
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
動(dòng)圖演示配置獲取和刷新過程
配置刷新時(shí)剃毒,會(huì)有日志輸出,如下
II. 其他
0. 項(xiàng)目
工程源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源碼: - https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config - https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config-demo
推薦博文
- 【DB 系列】借助 Redis 實(shí)現(xiàn)排行榜功能(應(yīng)用篇)
- 【DB 系列】借助 Redis 搭建一個(gè)簡單站點(diǎn)統(tǒng)計(jì)服務(wù)(應(yīng)用篇)
- 【W(wǎng)EB 系列】實(shí)現(xiàn)后端的接口版本支持(應(yīng)用篇)
- 【W(wǎng)EB 系列】徒手?jǐn)]一個(gè)掃碼登錄示例工程(應(yīng)用篇)
- 【基礎(chǔ)系列】AOP 實(shí)現(xiàn)一個(gè)日志插件(應(yīng)用篇)
- 【基礎(chǔ)系列】Bean 之注銷與動(dòng)態(tài)注冊實(shí)現(xiàn)服務(wù) mock(應(yīng)用篇)
- 【基礎(chǔ)系列】從0到1實(shí)現(xiàn)一個(gè)自定義Bean注冊器(應(yīng)用篇)
- 【基礎(chǔ)系列】FactoryBean及代理實(shí)現(xiàn)SPI機(jī)制的實(shí)例(應(yīng)用篇)
- 【基礎(chǔ)系列-實(shí)戰(zhàn)】如何指定bean最先加載(應(yīng)用篇)
- 【基礎(chǔ)系列】實(shí)現(xiàn)一個(gè)簡單的分布式定時(shí)任務(wù)(應(yīng)用篇)
1. 一灰灰 Blog
盡信書則不如搂赋,以上內(nèi)容赘阀,純屬一家之言,因個(gè)人能力有限脑奠,難免有疏漏和錯(cuò)誤之處基公,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評(píng)指正宋欺,不吝感激
下面一灰灰的個(gè)人博客轰豆,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個(gè)人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top