揭秘SpringBoot!一分鐘教你實現(xiàn)配置的動態(tài)神刷新
關于SpringBoot的自定義配置源、配置刷新之前也介紹過幾篇博文;最近正好在使用apollo時,排查配置未動態(tài)刷新的問題時,看了下它的具體實現(xiàn)發(fā)現(xiàn)挺有意思的;
接下來我們致敬經(jīng)典,看一下如果讓我們來實現(xiàn)配置的動態(tài)刷新,應該怎么搞?
# I. 配置使用姿勢
既然要支持配置的動態(tài)刷新,那么我們就得先看一下,在SpringBoot中,常見的配置使用姿勢有哪些
# 1. @Value注解綁定
直接通過@Value
注解,將一個對象得成員變量與Environment中的配置進行綁定,如
@Slf4j @RestController public class IndexController @Value("${config.type:-1}") private Integer type; @Value("${config.wechat:默認}") 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綁定
通過@ConfigurationProperties
注解聲明一個配置類,這個類中的成員變量都是從Environment
中進行初始化
如:
@ConfigurationProperties(prefix = "config") public class MyConfig { private String user; private String pwd; private Integer type; }
# 3. Environment.getProperty()直接獲取配置
直接從上下文中獲取配置,也常見于各種使用場景中,如
environment.getProperty("config.user");
# II. 配置刷新
接下來我們看一下,如何實現(xiàn)配置刷新后,上面的三種使用姿勢都能獲取到刷新后的值
# 1. 自定義一個屬性配置源
自定義一個配置源,我們直接基于內存的ConcurrentHashMap
來進行模擬,內部提供了一個配置更新的方法,當配置刷新之后,還會對外廣播一個配置變更事件
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() { // 將內存的配置信息設置為最高優(yōu)先級 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); } } /** * 主要實現(xiàn)配置變更事件發(fā)布于監(jiān)聽 */ @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; } } }
接下來就需要將這個自定義的配置元,注冊到 environment
上下文,在這里我們可以借助ApplicationContextInitializer
來實現(xiàn),在上下文初始化前,完成自定義配置注冊
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)初始化過了,直接忽略 return; } MapPropertySource propertySource = new MapPropertySource("selfSource", SelfConfigContext.getInstance().getCache()); environment.getPropertySources().addFirst(propertySource); } }
接下來注冊這個擴展點,直接選擇在項目啟動時,進行注冊
@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實時獲取配置的方式,支持配置刷新應該相對簡單,如直接吐出一個接口,支持更新我們自定義配置源的配置,不做任何變更,這個配置應該時同時更新的
首先提供一個Spring的工具類,用于更簡單的獲取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í)行驗證一下:
# 3. @ConfigurationProperties
配置刷新
之前在介紹自定義屬性配置綁定時介紹過,通過Binder
來實現(xiàn)綁定配置的Config對象動態(tài)刷新,我們這里同樣可以實現(xiàn)配置變更時,主動刷新@ConfigurationProperties
注解綁定的屬性
具體實現(xiàn)如下,
@Slf4j @Component public class ConfigAutoRefresher implements ApplicationRunner { private Binder binder; /** * 配置變更之后的刷新 */ @EventListener() public void refreshConfig(ConfigChangeListener.ConfigChangeEvent event) { log.info("配置發(fā)生變更,開始動態(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對象對應的配置值 * * @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); } }
注意上面的實現(xiàn),分三類:
public <T> void bind(Bindable<T> bindable)
: 具體實現(xiàn)綁定配置刷新的邏輯
核心思想就是將當前對象與environment配置進行重新綁定
public void run
: binder初始化
在應用啟動之后進行回調,確保是在environment準備完畢之后回調,獲取用于屬性配置綁定的binder,避免出現(xiàn)envionment
還沒有準備好
也可以借助實現(xiàn)EnvironmentPostProcessor
來實現(xiàn)
public void refreshConfig(ConfigChangeListener.ConfigChangeEvent event)
: 配置刷新
通過@EventListener
監(jiān)聽配置變更事件,找到所有的ConfigurationProperties
修飾對象,執(zhí)行重新綁定邏輯
接下來我們驗證一下配置變更是否會生效
@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); } }
定義一個UserConfig來接收config
前綴開始的配置,通過update接口來更新相關配置,更新完畢之后返回UserConfig的結果
# 4. @Value 配置刷新
最后我們再來看一下@Value注解綁定的配置的刷新策略
其核心思想就是找出所有@Value
綁定的成員變量,當監(jiān)聽到配置變更之后,通過反射的方式進行刷新
關鍵的實現(xiàn)如下
/** * 配置變更注冊, 找到 @Value 注解修飾的配置,注冊到 SpringValueRegistry,實現(xiàn)統(tǒ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); } } /** * 通過 @Value 修飾方法的方式,通過一個傳參進行實現(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); } } }
上面的實現(xiàn),主要利用到BeanPostProcessor
,在bean初始化之后,掃描當前bean中是否有@Value
綁定的屬性,若有,則注冊到自定義的SpringValueRegistry
中
注意事項:
@Value
有兩種綁定姿勢,直接放在成員變量上,以及通過方法進行注入
所以上面的實現(xiàn)策略中,有Field
和Method
兩種不同的處理策略;
@Value
支持SpEL表達式,我們需要對配置key進行解析
相關的源碼,推薦直接在下面的項目中進行獲取,demo中的實現(xiàn)也是來自apollo-client
接下來再看一下注冊配置綁定的實現(xiàn),核心方法比較簡單,兩個,一個注冊,一個刷新
@Slf4j public class SpringValueRegistry { public static Map<String, Set<SpringValue>> registry = new ConcurrentHashMap<>(); /** * 像registry中注冊配置key綁定的對象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對應的配置發(fā)生了變更,找到綁定這個配置的屬性,進行反射刷新 * * @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 { /** * 適合用于:配置是通過set類方法實現(xiàn)注入綁定的方式,只有一個傳參,為對應的配置key */ private MethodParameter methodParameter; /** * 成員變量 */ private Field field; /** * bean示例的弱引用 */ private WeakReference<Object> beanRef; /** * Spring Bean Name */ private String beanName; /** * 配置對應的key: 如 config.user */ private String key; /** * 配置引用,如 ${config.user} */ private String placeholder; /** * 配置綁定的目標類型 */ 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]; } /** * 配置基于反射的動態(tài)變更 * * @param newVal String: 配置對應的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的構建,主要就是基于反射需要使用到的一些關鍵信息的組成上;可以按需進行設計補充
到此,關于@Value注解的配置動態(tài)刷新就已經(jīng)實現(xiàn)了,接下來寫幾個demo驗證一下
@Slf4j @RestController public class IndexController { @Value("${config.type:-1}") private Integer type; @Value("${config.wechat:默認}") 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. 小結
本文主要介紹了項目中配置的動態(tài)刷新的實現(xiàn)方案,也可以看作是apollo配置中心的簡易實現(xiàn)原理,其中涉及到的知識點較多,下面做一個簡單的小結
- 配置的三種使用姿勢
@Value
綁定@ConfigurationProperties
綁定對象environment.getProperty()
- 自定義配置源加載
environment.getPropertySources().addFirst(MapPropertySource)
- 配置刷新
- Binder實現(xiàn)ConfigurationProperties刷新
- 反射實現(xiàn)@Value注解刷新
到此這篇關于揭秘SpringBoot!一分鐘教你實現(xiàn)配置的動態(tài)神刷新的文章就介紹到這了,更多相關SpringBoot 動態(tài)刷新內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot新建項目jdk只有17/21,無法選中1.8解決辦法
最近博主也有創(chuàng)建springboot項目,發(fā)現(xiàn)了IntelliJ IDEA在通過Spring Initilizer初始化項目的時候已經(jīng)沒有java8版本的選項了,這里給大家總結下,這篇文章主要給大家介紹了springboot新建項目jdk只有17/21,無法選中1.8的解決辦法,需要的朋友可以參考下2023-12-12舉例講解Java的Spring框架中AOP程序設計方式的使用
這篇文章主要介紹了Java的Spring框架中AOP程序設計方式的使用講解,文中舉的AOP下拋出異常的例子非常實用,需要的朋友可以參考下2016-04-04Spring事務@Transactional注解四種不生效案例場景分析
這篇文章主要為大家介紹了Spring事務@Transactional注解四種不生效的案例場景示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07springboot?靜態(tài)方法中使用@Autowired注入方式
這篇文章主要介紹了springboot?靜態(tài)方法中使用@Autowired注入方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02Java數(shù)據(jù)結構之并查集的實現(xiàn)
并查集是一種用來管理元素分組情況的數(shù)據(jù)結構。并查集可以高效地進行如下操作。本文將通過Java實現(xiàn)并查集,感興趣的小伙伴可以了解一下2022-01-01