揭秘SpringBoot!一分鐘教你實(shí)現(xiàn)配置的動(dòng)態(tài)神刷新
關(guān)于SpringBoot的自定義配置源、配置刷新之前也介紹過幾篇博文;最近正好在使用apollo時(shí),排查配置未動(dòng)態(tài)刷新的問題時(shí),看了下它的具體實(shí)現(xiàn)發(fā)現(xiàn)挺有意思的;
接下來我們致敬經(jīng)典,看一下如果讓我們來實(shí)現(xiàn)配置的動(dòng)態(tài)刷新,應(yīng)該怎么搞?
# I. 配置使用姿勢
既然要支持配置的動(dòng)態(tài)刷新,那么我們就得先看一下,在SpringBoot中,常見的配置使用姿勢有哪些
# 1. @Value注解綁定
直接通過@Value注解,將一個(gè)對象得成員變量與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綁定
通過@ConfigurationProperties注解聲明一個(gè)配置類,這個(gè)類中的成員變量都是從Environment中進(jìn)行初始化
如:
@ConfigurationProperties(prefix = "config")
public class MyConfig {
private String user;
private String pwd;
private Integer type;
}# 3. Environment.getProperty()直接獲取配置
直接從上下文中獲取配置,也常見于各種使用場景中,如
environment.getProperty("config.user");
# II. 配置刷新
接下來我們看一下,如何實(shí)現(xiàn)配置刷新后,上面的三種使用姿勢都能獲取到刷新后的值
# 1. 自定義一個(gè)屬性配置源
自定義一個(gè)配置源,我們直接基于內(nèi)存的ConcurrentHashMap來進(jìn)行模擬,內(nèi)部提供了一個(gè)配置更新的方法,當(dāng)配置刷新之后,還會(huì)對外廣播一個(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)聽
*/
@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;
}
}
}接下來就需要將這個(gè)自定義的配置元,注冊到 environment 上下文,在這里我們可以借助ApplicationContextInitializer來實(shí)現(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);
}
}接下來注冊這個(gè)擴(kuò)展點(diǎn),直接選擇在項(xiàng)目啟動(dòng)時(shí),進(jìn)行注冊
@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)該相對簡單,如直接吐出一個(gè)接口,支持更新我們自定義配置源的配置,不做任何變更,這個(gè)配置應(yīng)該時(shí)同時(shí)更新的
首先提供一個(gè)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í)行驗(yàn)證一下:
# 3. @ConfigurationProperties 配置刷新
之前在介紹自定義屬性配置綁定時(shí)介紹過,通過Binder來實(shí)現(xiàn)綁定配置的Config對象動(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ā)生變更,開始動(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對象對應(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)前對象與environment配置進(jìn)行重新綁定
public void run: binder初始化
在應(yīng)用啟動(dòng)之后進(jìn)行回調(diào),確保是在environment準(zhǔn)備完畢之后回調(diào),獲取用于屬性配置綁定的binder,避免出現(xiàn)envionment還沒有準(zhǔn)備好
也可以借助實(shí)現(xiàn)EnvironmentPostProcessor來實(shí)現(xiàn)
public void refreshConfig(ConfigChangeListener.ConfigChangeEvent event): 配置刷新
通過@EventListener監(jiān)聽配置變更事件,找到所有的ConfigurationProperties修飾對象,執(zhí)行重新綁定邏輯
接下來我們驗(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來接收config前綴開始的配置,通過update接口來更新相關(guān)配置,更新完畢之后返回UserConfig的結(jié)果
# 4. @Value 配置刷新
最后我們再來看一下@Value注解綁定的配置的刷新策略
其核心思想就是找出所有@Value綁定的成員變量,當(dāng)監(jiān)聽到配置變更之后,通過反射的方式進(jìn)行刷新
關(guān)鍵的實(shí)現(xiàn)如下
/**
* 配置變更注冊, 找到 @Value 注解修飾的配置,注冊到 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);
}
}
/**
* 通過 @Value 修飾方法的方式,通過一個(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綁定的屬性,若有,則注冊到自定義的SpringValueRegistry中
注意事項(xiàng):
@Value有兩種綁定姿勢,直接放在成員變量上,以及通過方法進(jìn)行注入
所以上面的實(shí)現(xiàn)策略中,有Field和Method兩種不同的處理策略;
@Value支持SpEL表達(dá)式,我們需要對配置key進(jìn)行解析
相關(guān)的源碼,推薦直接在下面的項(xiàng)目中進(jìn)行獲取,demo中的實(shí)現(xiàn)也是來自apollo-client
接下來再看一下注冊配置綁定的實(shí)現(xiàn),核心方法比較簡單,兩個(gè),一個(gè)注冊,一個(gè)刷新
@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對應(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 {
/**
* 適合用于:配置是通過set類方法實(shí)現(xiàn)注入綁定的方式,只有一個(gè)傳參,為對應(yīng)的配置key
*/
private MethodParameter methodParameter;
/**
* 成員變量
*/
private Field field;
/**
* bean示例的弱引用
*/
private WeakReference<Object> beanRef;
/**
* Spring Bean Name
*/
private String beanName;
/**
* 配置對應(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: 配置對應(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)了,接下來寫幾個(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配置中心的簡易實(shí)現(xiàn)原理,其中涉及到的知識(shí)點(diǎn)較多,下面做一個(gè)簡單的小結(jié)
- 配置的三種使用姿勢
@Value綁定@ConfigurationProperties綁定對象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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot新建項(xiàng)目jdk只有17/21,無法選中1.8解決辦法
最近博主也有創(chuàng)建springboot項(xiàng)目,發(fā)現(xiàn)了IntelliJ IDEA在通過Spring Initilizer初始化項(xiàng)目的時(shí)候已經(jīng)沒有java8版本的選項(xiàng)了,這里給大家總結(jié)下,這篇文章主要給大家介紹了springboot新建項(xiàng)目jdk只有17/21,無法選中1.8的解決辦法,需要的朋友可以參考下2023-12-12
Spring用三級(jí)緩存處理循環(huán)依賴的方法詳解
這篇文章主要介紹了Spring用三級(jí)緩存處理循環(huán)依賴的方法,在Spring?框架中,依賴注入是其核心特性之一,它允許對象之間的依賴關(guān)系在運(yùn)行時(shí)動(dòng)態(tài)注入,然而,當(dāng)多個(gè)Bean之間的依賴關(guān)系形成一個(gè)閉環(huán)時(shí),就會(huì)出現(xiàn)循環(huán)依賴問題,本文就為解決此問題,需要的朋友可以參考下2025-02-02
舉例講解Java的Spring框架中AOP程序設(shè)計(jì)方式的使用
這篇文章主要介紹了Java的Spring框架中AOP程序設(shè)計(jì)方式的使用講解,文中舉的AOP下拋出異常的例子非常實(shí)用,需要的朋友可以參考下2016-04-04
SPFA算法的實(shí)現(xiàn)原理及其應(yīng)用詳解
SPFA算法,全稱為Shortest?Path?Faster?Algorithm,是求解單源最短路徑問題的一種常用算法,本文就來聊聊它的實(shí)現(xiàn)原理與簡單應(yīng)用吧2023-05-05
java得到某年某周的第一天實(shí)現(xiàn)思路及代碼
某年某周的第一天,此功能如何使用java編程得到呢?既然有了問題就有解決方法,感興趣的朋友可以了解下本文,或許會(huì)給你帶來意想不到的收獲哦2013-01-01
Java微服務(wù)架構(gòu)中的關(guān)鍵技術(shù)和設(shè)計(jì)原則解讀
Java是一種面向?qū)ο蟮母呒?jí)編程語言,具有跨平臺(tái)兼容性、自動(dòng)內(nèi)存管理等特點(diǎn),它支持多線程、異常處理,并擁有豐富的標(biāo)準(zhǔn)庫和強(qiáng)大的社區(qū)生態(tài),微服務(wù)架構(gòu)是將應(yīng)用分解為多個(gè)小型服務(wù)的設(shè)計(jì)風(fēng)格2024-11-11
Spring事務(wù)@Transactional注解四種不生效案例場景分析
這篇文章主要為大家介紹了Spring事務(wù)@Transactional注解四種不生效的案例場景示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
springboot?靜態(tài)方法中使用@Autowired注入方式
這篇文章主要介紹了springboot?靜態(tài)方法中使用@Autowired注入方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
Java數(shù)據(jù)結(jié)構(gòu)之并查集的實(shí)現(xiàn)
并查集是一種用來管理元素分組情況的數(shù)據(jù)結(jié)構(gòu)。并查集可以高效地進(jìn)行如下操作。本文將通過Java實(shí)現(xiàn)并查集,感興趣的小伙伴可以了解一下2022-01-01

