SpringBoot @Value注解支持配置自動刷新能力擴展方式
在我們的日常開發(fā)中,使用@Value
來綁定配置屬于非常常見的基礎(chǔ)操作,但是這個配置注入是一次性的,簡單來說就是配置一旦賦值,則不會再修改;通常來講,這個并沒有什么問題,基礎(chǔ)的 SpringBoot 項目的配置也基本不存在配置變更,如果有使用過 SpringCloudConfig 的小伙伴,會知道@Value
可以綁定遠程配置,并支持動態(tài)刷新
接下來本文將通過一個實例來演示下,如何讓@Value
注解支持配置刷新;本文將涉及到以下知識點
- BeanPostProcessorAdapter + 自定義注解:獲取支持自動刷新的配置類
- MapPropertySource:實現(xiàn)配置動態(tài)變更
項目環(huán)境
項目依賴
本項目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
進行開發(fā)
開一個 web 服務(wù)用于測試
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
配置動態(tài)刷新支持
1. 思路介紹
要支持配合的動態(tài)刷新,重點在于下面兩點
- 如何修改
Environment
中的配置源 - 配置變更之后,如何通知到相關(guān)的類同步更新
2. 修改配置
相信很多小伙伴都不會去修改Environment
中的數(shù)據(jù)源,突然冒出一個讓我來修改配置源的數(shù)據(jù),還是有點懵的,這里推薦之前分享過一篇博文 SpringBoot 基礎(chǔ)篇之自定義配置源的使用姿勢
當我們知道如何去自定義配置源之后,再來修改數(shù)據(jù)源,就會有一點思路了
定義一個配置文件application-dynamic.yml
xhh: dynamic: name: 一灰灰blog
然后在主配置文件中使用它
spring: profiles: active: dynamic
使用配置的 java config
@Data @Component public class RefreshConfigProperties { @Value("${xhh.dynamic.name}") private String name; @Value("${xhh.dynamic.age:18}") private Integer age; @Value("hello ${xhh.dynamic.other:test}") private String other; }
接下來進入修改配置的正題
@Autowired ConfigurableEnvironment environment; // --- 配置修改 String name = "applicationConfig: [classpath:/application-dynamic.yml]"; MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name); Map<String, Object> source = propertySource.getSource(); Map<String, Object> map = new HashMap<>(source.size()); map.putAll(source); map.put(key, value); environment.getPropertySources().replace(name, new MapPropertySource(name, map));
上面的實現(xiàn)中,有幾個疑問點
name 如何找到的?
- debug...
配置變更
- 注意修改配置是新建了一個 Map,然后將舊的配置拷貝到新的 Map,然后再執(zhí)行替換;并不能直接進行修改,有興趣的小伙伴可以實測一下為什么
3. 配置同步
上面雖然是實現(xiàn)了配置的修改,但是對于使用@Value
注解修飾的變量,已經(jīng)被賦值了,如何能感知到配置的變更,并同步刷新呢?
這里就又可以拆分兩塊
找到需要修改的配置
修改事件同步
3.1 找出需要刷新的配置變量
我們這里額外增加了一個注解,用來修飾需要支持動態(tài)刷新的場景
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RefreshValue { }
接下來我們就是找出有上面這個注解的類,然后支持這些類中@Value
注解綁定的變量動態(tài)刷新
關(guān)于這個就有很多實現(xiàn)方式了,我們這里選擇BeanPostProcessor
,bean 創(chuàng)建完畢之后,借助反射來獲取@Value
綁定的變量,并緩存起來
@Component public class AnoValueRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware { private Map<String, List<FieldPair>> mapper = new HashMap<>(); private Environment environment; @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { processMetaValue(bean); return super.postProcessAfterInstantiation(bean, beanName); } /** * 這里主要的目的就是獲取支持動態(tài)刷新的配置屬性,然后緩存起來 * * @param bean */ private void processMetaValue(Object bean) { Class clz = bean.getClass(); if (!clz.isAnnotationPresent(RefreshValue.class)) { return; } try { for (Field field : clz.getDeclaredFields()) { if (field.isAnnotationPresent(Value.class)) { Value val = field.getAnnotation(Value.class); List<String> keyList = pickPropertyKey(val.value(), 0); for (String key : keyList) { mapper.computeIfAbsent(key, (k) -> new ArrayList<>()) .add(new FieldPair(bean, field, val.value())); } } } } catch (Exception e) { e.printStackTrace(); System.exit(-1); } } /** * 實現(xiàn)一個基礎(chǔ)的配置文件參數(shù)動態(tài)刷新支持 * * @param value * @return 提取key列表 */ private List<String> pickPropertyKey(String value, int begin) { int start = value.indexOf("${", begin) + 2; if (start < 2) { return new ArrayList<>(); } int middle = value.indexOf(":", start); int end = value.indexOf("}", start); String key; if (middle > 0 && middle < end) { // 包含默認值 key = value.substring(start, middle); } else { // 不包含默認值 key = value.substring(start, end); } List<String> keys = pickPropertyKey(value, end); keys.add(key); return keys; } @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Data @NoArgsConstructor @AllArgsConstructor public static class FieldPair { private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}", ":", true); Object bean; Field field; String value; public void updateValue(Environment environment) { boolean access = field.isAccessible(); if (!access) { field.setAccessible(true); } String updateVal = propertyPlaceholderHelper.replacePlaceholders(value, environment::getProperty); try { if (field.getType() == String.class) { field.set(bean, updateVal); } else { field.set(bean, JSONObject.parseObject(updateVal, field.getType())); } } catch (IllegalAccessException e) { e.printStackTrace(); } field.setAccessible(access); } } }
上面的實現(xiàn)雖然有點長,但是核心邏輯就下面節(jié)點
processMetaValue()
:
- 通過反射
- 撈取帶有
@Value
注解的變量
pickPropertyKey()
:
- 主要就是解析
@Value
注解中表達式,挑出變量名,用于緩存 - 如:
@value("hello ${name:xhh} ${now:111}
- 解析之后,有兩個變量,一個
name
一個now
- 緩存
Map<String, List<FieldPair>>
- 緩存的 key,為變量名
- 緩存的 value,自定義類,主要用于反射修改配置值
3.2 修改事件同步
從命名也可以看出,我們這里選擇事件機制來實現(xiàn)同步,直接借助 Spring Event 來完成
一個簡單的自定義類事件類
public static class ConfigUpdateEvent extends ApplicationEvent { String key; public ConfigUpdateEvent(Object source, String key) { super(source); this.key = key; } }
消費也比較簡單,直接將下面這段代碼,放在上面的AnoValueRefreshPostProcessor
, 接收到變更事件,通過 key 從緩存中找到需要變更的 Field,然后依次執(zhí)行刷新即可
@EventListener public void updateConfig(ConfigUpdateEvent configUpdateEvent) { List<FieldPair> list = mapper.get(configUpdateEvent.key); if (!CollectionUtils.isEmpty(list)) { list.forEach(f -> f.updateValue(environment)); } }
實例演示
最后將前面修改配置的代碼塊封裝一下,提供一個接口,來驗證下我們的配置刷新
@RestController public class DynamicRest { @Autowired ApplicationContext applicationContext; @Autowired ConfigurableEnvironment environment; @Autowired RefreshConfigProperties refreshConfigProperties; @GetMapping(path = "dynamic/update") public RefreshConfigProperties updateEnvironment(String key, String value) { String name = "applicationConfig: [classpath:/application-dynamic.yml]"; MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name); Map<String, Object> source = propertySource.getSource(); Map<String, Object> map = new HashMap<>(source.size()); map.putAll(source); map.put(key, value); environment.getPropertySources().replace(name, new MapPropertySource(name, map)); applicationContext.publishEvent(new AnoValueRefreshPostProcessor.ConfigUpdateEvent(this, key)); return refreshConfigProperties; } }
總結(jié)
本文主要通過簡單的幾步,對@Value
進行了拓展,支持配置動態(tài)刷新,核心知識點下面三塊:
- 使用 BeanPostProcess 來掃描需要刷新的變量
- 利用 Spring Event 事件機制來實現(xiàn)刷新同步感知
- 至于配置的修改,則主要是
MapPropertySource
來實現(xiàn)配置的替換修改
請注意,上面的這個實現(xiàn)思路,與 Spring Cloud Config 是有差異的。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Jackson將json string轉(zhuǎn)為Object,org.json讀取json數(shù)組的實例
下面小編就為大家?guī)硪黄狫ackson將json string轉(zhuǎn)為Object,org.json讀取json數(shù)組的實例,具有很好的參考價值,希望對大家有所幫助2017-12-12java實現(xiàn)查找PDF關(guān)鍵字所在頁碼及其坐標
這篇文章主要介紹了java實現(xiàn)查找PDF關(guān)鍵字所在頁碼及其坐標的方法,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-09-09SpringBoot項目中@RestControllerAdvice全局異常失效問題的解決
@RestController注解是一個用于定義RESTful Web服務(wù)的控制器的特殊注解,它是@Controller和@ResponseBody注解的結(jié)合體,意味著你不需要在每個處理請求的方法上都添加@ResponseBody,本文給大家介紹了解決SpringBoot項目中@RestControllerAdvice全局異常失效問題2024-11-11IntelliJ IDEA下Maven創(chuàng)建Scala項目的方法步驟
這篇文章主要介紹了IntelliJ IDEA下Maven創(chuàng)建Scala項目的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06String與Blob互轉(zhuǎn)和file文件與Blob互轉(zhuǎn)方式
這篇文章主要介紹了String與Blob互轉(zhuǎn)和file文件與Blob互轉(zhuǎn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05基于Java?Agent的premain方式實現(xiàn)方法耗時監(jiān)控問題
javaagent是在JDK5之后提供的新特性,也可以叫java代理,這篇文章主要介紹了基于Java?Agent的premain方式實現(xiàn)方法耗時監(jiān)控問題,需要的朋友可以參考下2022-10-10