揭秘SpringBoot!一分鐘教你實(shí)現(xiàn)配置的動(dòng)態(tài)神刷新
關(guān)于SpringBoot的自定義配置源、配置刷新之前也介紹過(guò)幾篇博文;最近正好在使用apollo時(shí),排查配置未動(dòng)態(tài)刷新的問(wèn)題時(shí),看了下它的具體實(shí)現(xiàn)發(fā)現(xiàn)挺有意思的;
接下來(lái)我們致敬經(jīng)典,看一下如果讓我們來(lái)實(shí)現(xiàn)配置的動(dòng)態(tài)刷新,應(yīng)該怎么搞?
# I. 配置使用姿勢(shì)
既然要支持配置的動(dòng)態(tài)刷新,那么我們就得先看一下,在SpringBoot中,常見(jiàn)的配置使用姿勢(shì)有哪些
# 1. @Value注解綁定
直接通過(guò)@Value
注解,將一個(gè)對(duì)象得成員變量與Environment中的配置進(jìn)行綁定,如
@Slf4j @RestController public class IndexController @Value("${config.type:-1}") private Integer type; @Value("${config.wechat:默認(rèn)}") private String wechat; private String email; @Value("${config.email:default@email}") public IndexController setEmail(String email) { this.email = email; return this; } }
注意:@Value
支持SpEL
# 2. @ConfigurationProperties綁定
通過(guò)@ConfigurationProperties
注解聲明一個(gè)配置類,這個(gè)類中的成員變量都是從Environment
中進(jìn)行初始化
如:
@ConfigurationProperties(prefix = "config") public class MyConfig { private String user; private String pwd; private Integer type; }
# 3. Environment.getProperty()直接獲取配置
直接從上下文中獲取配置,也常見(jiàn)于各種使用場(chǎng)景中,如
environment.getProperty("config.user");
# II. 配置刷新
接下來(lái)我們看一下,如何實(shí)現(xiàn)配置刷新后,上面的三種使用姿勢(shì)都能獲取到刷新后的值
# 1. 自定義一個(gè)屬性配置源
自定義一個(gè)配置源,我們直接基于內(nèi)存的ConcurrentHashMap
來(lái)進(jìn)行模擬,內(nèi)部提供了一個(gè)配置更新的方法,當(dāng)配置刷新之后,還會(huì)對(duì)外廣播一個(gè)配置變更事件
public class SelfConfigContext { private static volatile SelfConfigContext instance = new SelfConfigContext(); public static SelfConfigContext getInstance() { return instance; } private Map<String, Object> cache = new ConcurrentHashMap<>(); public Map<String, Object> getCache() { return cache; } private SelfConfigContext() { // 將內(nèi)存的配置信息設(shè)置為最高優(yōu)先級(jí) cache.put("config.type", 33); cache.put("config.wechat", "一灰灰blog"); cache.put("config.github", "liuyueyi"); } /** * 更新配置 * * @param key * @param val */ public void updateConfig(String key, Object val) { cache.put(key, val); ConfigChangeListener.publishConfigChangeEvent(key); } } /** * 主要實(shí)現(xiàn)配置變更事件發(fā)布于監(jiān)聽(tīng) */ @Component public class ConfigChangeListener implements ApplicationListener<ConfigChangeListener.ConfigChangeEvent> { @Override public void onApplicationEvent(ConfigChangeEvent configChangeEvent) { SpringValueRegistry.updateValue(configChangeEvent.getKey()); } public static void publishConfigChangeEvent(String key) { SpringUtil.getApplicationContext().publishEvent(new ConfigChangeEvent(Thread.currentThread().getStackTrace()[0], key)); } @Getter public static class ConfigChangeEvent extends ApplicationEvent { private String key; public ConfigChangeEvent(Object source, String key) { super(source); this.key = key; } } }
接下來(lái)就需要將這個(gè)自定義的配置元,注冊(cè)到 environment
上下文,在這里我們可以借助ApplicationContextInitializer
來(lái)實(shí)現(xiàn),在上下文初始化前,完成自定義配置注冊(cè)
public class SelfConfigContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext configurableApplicationContext) { System.out.println("postProcessEnvironment#initialize"); ConfigurableEnvironment env = configurableApplicationContext.getEnvironment(); initialize(env); } protected void initialize(ConfigurableEnvironment environment) { if (environment.getPropertySources().contains("selfSource")) { // 已經(jīng)初始化過(guò)了,直接忽略 return; } MapPropertySource propertySource = new MapPropertySource("selfSource", SelfConfigContext.getInstance().getCache()); environment.getPropertySources().addFirst(propertySource); } }
接下來(lái)注冊(cè)這個(gè)擴(kuò)展點(diǎn),直接選擇在項(xiàng)目啟動(dòng)時(shí),進(jìn)行注冊(cè)
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(Application.class); springApplication.addInitializers(new SelfConfigContextInitializer()); springApplication.run(args); } }
# 2. Environment配置刷新
envionment實(shí)時(shí)獲取配置的方式,支持配置刷新應(yīng)該相對(duì)簡(jiǎn)單,如直接吐出一個(gè)接口,支持更新我們自定義配置源的配置,不做任何變更,這個(gè)配置應(yīng)該時(shí)同時(shí)更新的
首先提供一個(gè)Spring的工具類,用于更簡(jiǎn)單的獲取Spring上下文
@Component public class SpringUtil implements ApplicationContextAware, EnvironmentAware { private static ApplicationContext applicationContext; private static Environment environment; private static Binder binder; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringUtil.applicationContext = applicationContext; } @Override public void setEnvironment(Environment environment) { SpringUtil.environment = environment; binder = Binder.get(environment); } public static ApplicationContext getApplicationContext() { return applicationContext; } public static Environment getEnvironment() { return environment; } public static Binder getBinder() { return binder; } }
配置更新的示例
@Slf4j @RestController public class IndexController { @GetMapping(path = "update") public String updateCache(String key, String val) { SelfConfigContext.getInstance().updateConfig(key, val); return "ok"; } @GetMapping(path = "get") public String getProperty(String key) { return SpringUtil.getEnvironment().getProperty(key); } }
執(zhí)行驗(yàn)證一下:
# 3. @ConfigurationProperties
配置刷新
之前在介紹自定義屬性配置綁定時(shí)介紹過(guò),通過(guò)Binder
來(lái)實(shí)現(xiàn)綁定配置的Config對(duì)象動(dòng)態(tài)刷新,我們這里同樣可以實(shí)現(xiàn)配置變更時(shí),主動(dòng)刷新@ConfigurationProperties
注解綁定的屬性
具體實(shí)現(xiàn)如下,
@Slf4j @Component public class ConfigAutoRefresher implements ApplicationRunner { private Binder binder; /** * 配置變更之后的刷新 */ @EventListener() public void refreshConfig(ConfigChangeListener.ConfigChangeEvent event) { log.info("配置發(fā)生變更,開(kāi)始動(dòng)態(tài)刷新: {}", event); SpringUtil.getApplicationContext().getBeansWithAnnotation(ConfigurationProperties.class).values().forEach(bean -> { Bindable<?> target = Bindable.ofInstance(bean).withAnnotations(AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class)); bind(target); }); } /** * 重新綁定bean對(duì)象對(duì)應(yīng)的配置值 * * @param bindable * @param <T> */ public <T> void bind(Bindable<T> bindable) { ConfigurationProperties propertiesAno = bindable.getAnnotation(ConfigurationProperties.class); if (propertiesAno != null) { BindHandler bindHandler = getBindHandler(propertiesAno); this.binder.bind(propertiesAno.prefix(), bindable, bindHandler); } } private BindHandler getBindHandler(ConfigurationProperties annotation) { BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler(); if (annotation.ignoreInvalidFields()) { handler = new IgnoreErrorsBindHandler(handler); } if (!annotation.ignoreUnknownFields()) { UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); handler = new NoUnboundElementsBindHandler(handler, filter); } return handler; } @Override public void run(ApplicationArguments args) throws Exception { log.info("初始化!"); ConfigurableEnvironment environment = (ConfigurableEnvironment) SpringUtil.getEnvironment(); this.binder = new Binder(ConfigurationPropertySources.from(environment.getPropertySources()), new PropertySourcesPlaceholdersResolver(environment), new DefaultConversionService(), ((ConfigurableApplicationContext) SpringUtil.getApplicationContext()) .getBeanFactory()::copyRegisteredEditorsTo); } }
注意上面的實(shí)現(xiàn),分三類:
public <T> void bind(Bindable<T> bindable)
: 具體實(shí)現(xiàn)綁定配置刷新的邏輯
核心思想就是將當(dāng)前對(duì)象與environment配置進(jìn)行重新綁定
public void run
: binder初始化
在應(yīng)用啟動(dòng)之后進(jìn)行回調(diào),確保是在environment準(zhǔn)備完畢之后回調(diào),獲取用于屬性配置綁定的binder,避免出現(xiàn)envionment
還沒(méi)有準(zhǔn)備好
也可以借助實(shí)現(xiàn)EnvironmentPostProcessor
來(lái)實(shí)現(xiàn)
public void refreshConfig(ConfigChangeListener.ConfigChangeEvent event)
: 配置刷新
通過(guò)@EventListener
監(jiān)聽(tīng)配置變更事件,找到所有的ConfigurationProperties
修飾對(duì)象,執(zhí)行重新綁定邏輯
接下來(lái)我們驗(yàn)證一下配置變更是否會(huì)生效
@Data @Component @ConfigurationProperties(prefix = "config") public class UserConfig { private String user; private String pwd; private Integer type; private String wechat; } @Slf4j @RestController public class IndexController { @Autowired private UserConfig userConfig; @GetMapping(path = "/user") public UserConfig user() { return userConfig; } @GetMapping(path = "update") public String updateCache(String key, String val) { selfConfigContainer.refreshConfig(key, val); SelfConfigContext.getInstance().updateConfig(key, val); return JSON.toJSONString(userConfig); } }
定義一個(gè)UserConfig來(lái)接收config
前綴開(kāi)始的配置,通過(guò)update接口來(lái)更新相關(guān)配置,更新完畢之后返回UserConfig的結(jié)果
# 4. @Value 配置刷新
最后我們?cè)賮?lái)看一下@Value注解綁定的配置的刷新策略
其核心思想就是找出所有@Value
綁定的成員變量,當(dāng)監(jiān)聽(tīng)到配置變更之后,通過(guò)反射的方式進(jìn)行刷新
關(guān)鍵的實(shí)現(xiàn)如下
/** * 配置變更注冊(cè), 找到 @Value 注解修飾的配置,注冊(cè)到 SpringValueRegistry,實(shí)現(xiàn)統(tǒng)一的配置變更自動(dòng)刷新管理 * * @author YiHui * @date 2023/6/26 */ @Slf4j @Component public class SpringValueProcessor implements BeanPostProcessor { private final PlaceholderHelper placeholderHelper; public SpringValueProcessor() { this.placeholderHelper = new PlaceholderHelper(); } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { Class clazz = bean.getClass(); for (Field field : findAllField(clazz)) { processField(bean, beanName, field); } for (Method method : findAllMethod(clazz)) { processMethod(bean, beanName, method); } return bean; } private List<Field> findAllField(Class clazz) { final List<Field> res = new LinkedList<>(); ReflectionUtils.doWithFields(clazz, res::add); return res; } private List<Method> findAllMethod(Class clazz) { final List<Method> res = new LinkedList<>(); ReflectionUtils.doWithMethods(clazz, res::add); return res; } /** * 成員變量上添加 @Value 方式綁定的配置 * * @param bean * @param beanName * @param field */ protected void processField(Object bean, String beanName, Field field) { // register @Value on field Value value = field.getAnnotation(Value.class); if (value == null) { return; } Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value()); if (keys.isEmpty()) { return; } for (String key : keys) { SpringValueRegistry.SpringValue springValue = new SpringValueRegistry.SpringValue(key, value.value(), bean, beanName, field); SpringValueRegistry.register(key, springValue); log.debug("Monitoring {}", springValue); } } /** * 通過(guò) @Value 修飾方法的方式,通過(guò)一個(gè)傳參進(jìn)行實(shí)現(xiàn)的配置綁定 * * @param bean * @param beanName * @param method */ protected void processMethod(Object bean, String beanName, Method method) { //register @Value on method Value value = method.getAnnotation(Value.class); if (value == null) { return; } //skip Configuration bean methods if (method.getAnnotation(Bean.class) != null) { return; } if (method.getParameterTypes().length != 1) { log.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters", bean.getClass().getName(), method.getName(), method.getParameterTypes().length); return; } Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value()); if (keys.isEmpty()) { return; } for (String key : keys) { SpringValueRegistry.SpringValue springValue = new SpringValueRegistry.SpringValue(key, value.value(), bean, beanName, method); SpringValueRegistry.register(key, springValue); log.info("Monitoring {}", springValue); } } }
上面的實(shí)現(xiàn),主要利用到BeanPostProcessor
,在bean初始化之后,掃描當(dāng)前bean中是否有@Value
綁定的屬性,若有,則注冊(cè)到自定義的SpringValueRegistry
中
注意事項(xiàng):
@Value
有兩種綁定姿勢(shì),直接放在成員變量上,以及通過(guò)方法進(jìn)行注入
所以上面的實(shí)現(xiàn)策略中,有Field
和Method
兩種不同的處理策略;
@Value
支持SpEL表達(dá)式,我們需要對(duì)配置key進(jìn)行解析
相關(guān)的源碼,推薦直接在下面的項(xiàng)目中進(jìn)行獲取,demo中的實(shí)現(xiàn)也是來(lái)自apollo-client
接下來(lái)再看一下注冊(cè)配置綁定的實(shí)現(xiàn),核心方法比較簡(jiǎn)單,兩個(gè),一個(gè)注冊(cè),一個(gè)刷新
@Slf4j public class SpringValueRegistry { public static Map<String, Set<SpringValue>> registry = new ConcurrentHashMap<>(); /** * 像registry中注冊(cè)配置key綁定的對(duì)象W * * @param key * @param val */ public static void register(String key, SpringValue val) { if (!registry.containsKey(key)) { synchronized (SpringValueRegistry.class) { if (!registry.containsKey(key)) { registry.put(key, new HashSet<>()); } } } Set<SpringValue> set = registry.getOrDefault(key, new HashSet<>()); set.add(val); } /** * key對(duì)應(yīng)的配置發(fā)生了變更,找到綁定這個(gè)配置的屬性,進(jìn)行反射刷新 * * @param key */ public static void updateValue(String key) { Set<SpringValue> set = registry.getOrDefault(key, new HashSet<>()); set.forEach(s -> { try { s.update((s1, aClass) -> SpringUtil.getBinder().bindOrCreate(s1, aClass)); } catch (Exception e) { throw new RuntimeException(e); } }); } @Data public static class SpringValue { /** * 適合用于:配置是通過(guò)set類方法實(shí)現(xiàn)注入綁定的方式,只有一個(gè)傳參,為對(duì)應(yīng)的配置key */ private MethodParameter methodParameter; /** * 成員變量 */ private Field field; /** * bean示例的弱引用 */ private WeakReference<Object> beanRef; /** * Spring Bean Name */ private String beanName; /** * 配置對(duì)應(yīng)的key: 如 config.user */ private String key; /** * 配置引用,如 ${config.user} */ private String placeholder; /** * 配置綁定的目標(biāo)類型 */ private Class<?> targetType; public SpringValue(String key, String placeholder, Object bean, String beanName, Field field) { this.beanRef = new WeakReference<>(bean); this.beanName = beanName; this.field = field; this.key = key; this.placeholder = placeholder; this.targetType = field.getType(); } public SpringValue(String key, String placeholder, Object bean, String beanName, Method method) { this.beanRef = new WeakReference<>(bean); this.beanName = beanName; this.methodParameter = new MethodParameter(method, 0); this.key = key; this.placeholder = placeholder; Class<?>[] paramTps = method.getParameterTypes(); this.targetType = paramTps[0]; } /** * 配置基于反射的動(dòng)態(tài)變更 * * @param newVal String: 配置對(duì)應(yīng)的key Class: 配置綁定的成員/方法參數(shù)類型, Object 新的配置值 * @throws Exception */ public void update(BiFunction<String, Class, Object> newVal) throws Exception { if (isField()) { injectField(newVal); } else { injectMethod(newVal); } } private void injectField(BiFunction<String, Class, Object> newVal) throws Exception { Object bean = beanRef.get(); if (bean == null) { return; } boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(bean, newVal.apply(key, field.getType())); field.setAccessible(accessible); log.info("更新value: {}#{} = {}", beanName, field.getName(), field.get(bean)); } private void injectMethod(BiFunction<String, Class, Object> newVal) throws Exception { Object bean = beanRef.get(); if (bean == null) { return; } Object va = newVal.apply(key, methodParameter.getParameterType()); methodParameter.getMethod().invoke(bean, va); log.info("更新method: {}#{} = {}", beanName, methodParameter.getMethod().getName(), va); } public boolean isField() { return this.field != null; } } }
SpringValue的構(gòu)建,主要就是基于反射需要使用到的一些關(guān)鍵信息的組成上;可以按需進(jìn)行設(shè)計(jì)補(bǔ)充
到此,關(guān)于@Value注解的配置動(dòng)態(tài)刷新就已經(jīng)實(shí)現(xiàn)了,接下來(lái)寫幾個(gè)demo驗(yàn)證一下
@Slf4j @RestController public class IndexController { @Value("${config.type:-1}") private Integer type; @Value("${config.wechat:默認(rèn)}") private String wechat; private String email; @Value("${config.email:default@email}") public IndexController setEmail(String email) { this.email = email; return this; } @GetMapping(path = "update") public String updateCache(String key, String val) { SelfConfigContext.getInstance().updateConfig(key, val); return wechat + "_" + type + "_" + email; } }
# 5. 小結(jié)
本文主要介紹了項(xiàng)目中配置的動(dòng)態(tài)刷新的實(shí)現(xiàn)方案,也可以看作是apollo配置中心的簡(jiǎn)易實(shí)現(xiàn)原理,其中涉及到的知識(shí)點(diǎn)較多,下面做一個(gè)簡(jiǎn)單的小結(jié)
- 配置的三種使用姿勢(shì)
@Value
綁定@ConfigurationProperties
綁定對(duì)象environment.getProperty()
- 自定義配置源加載
environment.getPropertySources().addFirst(MapPropertySource)
- 配置刷新
- Binder實(shí)現(xiàn)ConfigurationProperties刷新
- 反射實(shí)現(xiàn)@Value注解刷新
到此這篇關(guān)于揭秘SpringBoot!一分鐘教你實(shí)現(xiàn)配置的動(dòng)態(tài)神刷新的文章就介紹到這了,更多相關(guān)SpringBoot 動(dòng)態(tài)刷新內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot新建項(xiàng)目jdk只有17/21,無(wú)法選中1.8解決辦法
最近博主也有創(chuàng)建springboot項(xiàng)目,發(fā)現(xiàn)了IntelliJ IDEA在通過(guò)Spring Initilizer初始化項(xiàng)目的時(shí)候已經(jīng)沒(méi)有java8版本的選項(xiàng)了,這里給大家總結(jié)下,這篇文章主要給大家介紹了springboot新建項(xiàng)目jdk只有17/21,無(wú)法選中1.8的解決辦法,需要的朋友可以參考下2023-12-12Spring用三級(jí)緩存處理循環(huán)依賴的方法詳解
這篇文章主要介紹了Spring用三級(jí)緩存處理循環(huán)依賴的方法,在Spring?框架中,依賴注入是其核心特性之一,它允許對(duì)象之間的依賴關(guān)系在運(yùn)行時(shí)動(dòng)態(tài)注入,然而,當(dāng)多個(gè)Bean之間的依賴關(guān)系形成一個(gè)閉環(huán)時(shí),就會(huì)出現(xiàn)循環(huán)依賴問(wèn)題,本文就為解決此問(wèn)題,需要的朋友可以參考下2025-02-02舉例講解Java的Spring框架中AOP程序設(shè)計(jì)方式的使用
這篇文章主要介紹了Java的Spring框架中AOP程序設(shè)計(jì)方式的使用講解,文中舉的AOP下拋出異常的例子非常實(shí)用,需要的朋友可以參考下2016-04-04SPFA算法的實(shí)現(xiàn)原理及其應(yīng)用詳解
SPFA算法,全稱為Shortest?Path?Faster?Algorithm,是求解單源最短路徑問(wèn)題的一種常用算法,本文就來(lái)聊聊它的實(shí)現(xiàn)原理與簡(jiǎn)單應(yīng)用吧2023-05-05java得到某年某周的第一天實(shí)現(xiàn)思路及代碼
某年某周的第一天,此功能如何使用java編程得到呢?既然有了問(wèn)題就有解決方法,感興趣的朋友可以了解下本文,或許會(huì)給你帶來(lái)意想不到的收獲哦2013-01-01Java微服務(wù)架構(gòu)中的關(guān)鍵技術(shù)和設(shè)計(jì)原則解讀
Java是一種面向?qū)ο蟮母呒?jí)編程語(yǔ)言,具有跨平臺(tái)兼容性、自動(dòng)內(nèi)存管理等特點(diǎn),它支持多線程、異常處理,并擁有豐富的標(biāo)準(zhǔn)庫(kù)和強(qiáng)大的社區(qū)生態(tài),微服務(wù)架構(gòu)是將應(yīng)用分解為多個(gè)小型服務(wù)的設(shè)計(jì)風(fēng)格2024-11-11Spring中@Async用法詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了Spring中@Async用法詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-02-02Spring事務(wù)@Transactional注解四種不生效案例場(chǎng)景分析
這篇文章主要為大家介紹了Spring事務(wù)@Transactional注解四種不生效的案例場(chǎng)景示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07springboot?靜態(tài)方法中使用@Autowired注入方式
這篇文章主要介紹了springboot?靜態(tài)方法中使用@Autowired注入方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Java數(shù)據(jù)結(jié)構(gòu)之并查集的實(shí)現(xiàn)
并查集是一種用來(lái)管理元素分組情況的數(shù)據(jù)結(jié)構(gòu)。并查集可以高效地進(jìn)行如下操作。本文將通過(guò)Java實(shí)現(xiàn)并查集,感興趣的小伙伴可以了解一下2022-01-01