SpringBoot @Value注解支持配置自動(dòng)刷新能力擴(kuò)展方式
在我們的日常開發(fā)中,使用@Value來(lái)綁定配置屬于非常常見的基礎(chǔ)操作,但是這個(gè)配置注入是一次性的,簡(jiǎn)單來(lái)說(shuō)就是配置一旦賦值,則不會(huì)再修改;通常來(lái)講,這個(gè)并沒有什么問(wèn)題,基礎(chǔ)的 SpringBoot 項(xiàng)目的配置也基本不存在配置變更,如果有使用過(guò) SpringCloudConfig 的小伙伴,會(huì)知道@Value可以綁定遠(yuǎn)程配置,并支持動(dòng)態(tài)刷新
接下來(lái)本文將通過(guò)一個(gè)實(shí)例來(lái)演示下,如何讓@Value注解支持配置刷新;本文將涉及到以下知識(shí)點(diǎn)
- BeanPostProcessorAdapter + 自定義注解:獲取支持自動(dòng)刷新的配置類
- MapPropertySource:實(shí)現(xiàn)配置動(dòng)態(tài)變更
項(xiàng)目環(huán)境
項(xiàng)目依賴
本項(xiàng)目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA進(jìn)行開發(fā)
開一個(gè) web 服務(wù)用于測(cè)試
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>配置動(dòng)態(tài)刷新支持
1. 思路介紹
要支持配合的動(dòng)態(tài)刷新,重點(diǎn)在于下面兩點(diǎn)
- 如何修改
Environment中的配置源 - 配置變更之后,如何通知到相關(guān)的類同步更新
2. 修改配置
相信很多小伙伴都不會(huì)去修改Environment中的數(shù)據(jù)源,突然冒出一個(gè)讓我來(lái)修改配置源的數(shù)據(jù),還是有點(diǎn)懵的,這里推薦之前分享過(guò)一篇博文 SpringBoot 基礎(chǔ)篇之自定義配置源的使用姿勢(shì)
當(dāng)我們知道如何去自定義配置源之后,再來(lái)修改數(shù)據(jù)源,就會(huì)有一點(diǎn)思路了
定義一個(gè)配置文件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;
}接下來(lái)進(jìn)入修改配置的正題
@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));
上面的實(shí)現(xiàn)中,有幾個(gè)疑問(wèn)點(diǎn)
name 如何找到的?
- debug...
配置變更
- 注意修改配置是新建了一個(gè) Map,然后將舊的配置拷貝到新的 Map,然后再執(zhí)行替換;并不能直接進(jìn)行修改,有興趣的小伙伴可以實(shí)測(cè)一下為什么
3. 配置同步
上面雖然是實(shí)現(xiàn)了配置的修改,但是對(duì)于使用@Value注解修飾的變量,已經(jīng)被賦值了,如何能感知到配置的變更,并同步刷新呢?
這里就又可以拆分兩塊
找到需要修改的配置
修改事件同步
3.1 找出需要刷新的配置變量
我們這里額外增加了一個(gè)注解,用來(lái)修飾需要支持動(dòng)態(tài)刷新的場(chǎng)景
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshValue {
}接下來(lái)我們就是找出有上面這個(gè)注解的類,然后支持這些類中@Value注解綁定的變量動(dòng)態(tài)刷新
關(guān)于這個(gè)就有很多實(shí)現(xiàn)方式了,我們這里選擇BeanPostProcessor,bean 創(chuàng)建完畢之后,借助反射來(lái)獲取@Value綁定的變量,并緩存起來(lái)
@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);
}
/**
* 這里主要的目的就是獲取支持動(dòng)態(tài)刷新的配置屬性,然后緩存起來(lá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);
}
}
/**
* 實(shí)現(xiàn)一個(gè)基礎(chǔ)的配置文件參數(shù)動(dòng)態(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) {
// 包含默認(rèn)值
key = value.substring(start, middle);
} else {
// 不包含默認(rèn)值
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);
}
}
}上面的實(shí)現(xiàn)雖然有點(diǎn)長(zhǎng),但是核心邏輯就下面節(jié)點(diǎn)
processMetaValue():
- 通過(guò)反射
- 撈取帶有
@Value注解的變量
pickPropertyKey():
- 主要就是解析
@Value注解中表達(dá)式,挑出變量名,用于緩存 - 如:
@value("hello ${name:xhh} ${now:111} - 解析之后,有兩個(gè)變量,一個(gè)
name一個(gè)now - 緩存
Map<String, List<FieldPair>> - 緩存的 key,為變量名
- 緩存的 value,自定義類,主要用于反射修改配置值
3.2 修改事件同步
從命名也可以看出,我們這里選擇事件機(jī)制來(lái)實(shí)現(xiàn)同步,直接借助 Spring Event 來(lái)完成
一個(gè)簡(jiǎn)單的自定義類事件類
public static class ConfigUpdateEvent extends ApplicationEvent {
String key;
public ConfigUpdateEvent(Object source, String key) {
super(source);
this.key = key;
}
}消費(fèi)也比較簡(jiǎn)單,直接將下面這段代碼,放在上面的AnoValueRefreshPostProcessor, 接收到變更事件,通過(guò) 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));
}
}實(shí)例演示
最后將前面修改配置的代碼塊封裝一下,提供一個(gè)接口,來(lái)驗(yàn)證下我們的配置刷新
@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é)
本文主要通過(guò)簡(jiǎn)單的幾步,對(duì)@Value進(jìn)行了拓展,支持配置動(dòng)態(tài)刷新,核心知識(shí)點(diǎn)下面三塊:
- 使用 BeanPostProcess 來(lái)掃描需要刷新的變量
- 利用 Spring Event 事件機(jī)制來(lái)實(shí)現(xiàn)刷新同步感知
- 至于配置的修改,則主要是
MapPropertySource來(lái)實(shí)現(xiàn)配置的替換修改
請(qǐng)注意,上面的這個(gè)實(shí)現(xiàn)思路,與 Spring Cloud Config 是有差異的。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring AOP + Guava RateLimiter實(shí)現(xiàn)優(yōu)雅限流
本文主要介紹了Spring AOP + Guava RateLimiter實(shí)現(xiàn)優(yōu)雅限流,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2026-04-04
通過(guò)Java設(shè)置Word頁(yè)面背景色過(guò)程詳解
這篇文章主要介紹了通過(guò)Java設(shè)置Word頁(yè)面背景色過(guò)程詳解,Word中可以針對(duì)不同文檔排版設(shè)計(jì)要求來(lái)設(shè)置背景設(shè)置顏色。常見的可設(shè)置單一顏色、漸變色或加載圖片來(lái)設(shè)置成背景。下面通過(guò)Java來(lái)設(shè)置以上3種Word頁(yè)面背景色,需要的朋友可以參考下2019-07-07
SWT(JFace)體驗(yàn)之打開多個(gè)Form
SWT(JFace)體驗(yàn)之打開多個(gè)Form的實(shí)現(xiàn)代碼。2009-06-06
關(guān)于spring-security(記住密碼,CSRF)
文章主要介紹了Spring Security中的PersistentTokenRepository、CSRF保護(hù)機(jī)制以及如何在登錄頁(yè)面添加記住我功能,并分享了相關(guān)實(shí)現(xiàn)代碼和配置2024-11-11
SpringBoot整合SpringBoot-Admin實(shí)現(xiàn)監(jiān)控應(yīng)用功能
本文主要介紹如何整合Spring Boot Admin,以此監(jiān)控Springboot應(yīng)用,文中有相關(guān)的示例代碼供大家參考,需要的朋友可以參考下2023-05-05
SpringCloud?微服務(wù)數(shù)據(jù)權(quán)限控制的實(shí)現(xiàn)
這篇文章主要介紹的是權(quán)限控制的數(shù)據(jù)權(quán)限層面,意思是控制可訪問(wèn)數(shù)據(jù)資源的數(shù)量,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-11-11
Mybatis中l(wèi)ike搭配concat的寫法詳解
這篇文章主要介紹了Mybatis中l(wèi)ike搭配concat的寫法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
@Valid 無(wú)法校驗(yàn)List<E>的問(wèn)題
這篇文章主要介紹了@Valid 無(wú)法校驗(yàn)List<E>的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10

