本文基于SpringBoot 2.1.6.RELEASE版本
SpringBoot源码解析:Environment加载配置原理(1)
SpringBoot源码解析:Environment获取配置原理(2)
SpringBoot源码解析:@ConfigurationProperties怎么绑定Environment原理探究(3)
SpringBoot源码解析:@Value怎么绑定Environment注入属性值(4)
前文我们介绍了@ConfigurationProperties @Value的基本原理。那我们要动态刷新这些配置(我们从分布式配置中心修改配置,能实时修改我们服务的配置值),又该怎么做呢?
目录
1. 管理界面修改提交配置,服务器监听配置修改事件
-
SpringCloud config-server修改配置,触发调用config-client,发布RefreshEvent事件,执行RefreshEventListener监听器。
-
Apollo config-server修改配置,推拉结合方式触发config-client,发布ConfigChangeEvent时间,执行AutoUpdateConfigChangeListener监听器
-
DisConf config-server修改配置,ZooKeeper通知config-client(Disconf其实并没有和spring environment结合,所以这是disconf-client的一个弊端,我们先不讲)
综上,其实各种分布式配置中心,都是触发client,发布一个配置修改的事件,通过对应的listener去处理。
2. 加载修改的配置到Environment中
- 前文我们介绍过怎么把配置文件中的配置加载到Environment中,就不详细介绍了。
- 此处只需要拿到对应修改配置或者对应整个配置文件(通过监听修改,或主动去查询),设置到environment中配置对应的PropertySources中即可。
3. 刷新@ConfigurationProperties配置
刷新@ConfigurationProperties配置相对简单,我们直接看看SpringCloud是怎么做的就好。
-
ConfigurationPropertiesRebinder监听到加载更新environment完成的事件EnvironmentChangeEvent,就开始重新绑定@ConfigurationProperties配置
-
ConfigurationPropertiesRebinder(spring-cloud-context.2.1.6.RELEASE)
//ConfigurationPropertiesBeans是一个BeanPostProcessor,所以bean初始化的时候会把所有@ConfigurationProperties的bean都放进去 private ConfigurationPropertiesBeans beans; @ManagedOperation public void rebind() { this.errors.clear(); //取出所有含有@ConfigurationProperties的bean,重新绑定配置 for (String name : this.beans.getBeanNames()) { rebind(name); } } @ManagedOperation public boolean rebind(String name) { if (!this.beans.getBeanNames().contains(name)) { return false; } if (this.applicationContext != null) { try { Object bean = this.applicationContext.getBean(name); //判断拿到的bean是否是aop代理,如果是aop代理,拿出原本代理的目标bean if (AopUtils.isAopProxy(bean)) { bean = ProxyUtils.getTargetObject(bean); } //把拿到的bean重新销毁和初始化,就是这么简单(我们知道@ConfigurationProperties的绑定配置类ConfigurationPropertiesBindingPostProcessor是一个BeanPostProcessor,初始化的时候就会调用绑定配置,不清楚的看一下前文解析) if (bean != null) { this.applicationContext.getAutowireCapableBeanFactory() .destroyBean(bean); this.applicationContext.getAutowireCapableBeanFactory() .initializeBean(bean, name); return true; } } catch (RuntimeException e) { this.errors.put(name, e); throw e; } catch (Exception e) { this.errors.put(name, e); throw new IllegalStateException("Cannot rebind to " + name, e); } } return false; }
4. 刷新@Value配置
刷新@Value,SpringCloud和Apollo的方式不一样。我们看看Apollo怎么做的
-
也是利用一个BeanPostProcessor:ApolloProcessor把bean上面的字段或方法上有@Value注解的注册到SpringValueRegistry
ApolloProcessor
public abstract class ApolloProcessor implements BeanPostProcessor, PriorityOrdered { @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; } //省略代码... }
我们看看SpringValueRegistry,注册的结构如下
public class SpringValueRegistry { //<BeanFactory,<配置的key值, SpringValue>> private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap(); }
再看看SpringValue结构
public class SpringValue { //如果@value在方法上,对应的方法上的参数 private MethodParameter methodParameter; //如果@value是在字段上,对应的字段 private Field field; //bean的引用 private WeakReference<Object> beanRef; //bean名称 private String beanName; //配置的key值 private String key; private String placeholder; private Class<?> targetType; private Type genericType; private boolean isJson; }
-
监听到ConfigChangeEvent事件,查找修改的key是否存在于上面的SpringValueRegistry中,存在,就修改对应的bean的值。
AutoUpdateConfigChangeListener
//从SpringValueRegistry中取出了修改key对应的SpringValue对象 private void updateSpringValue(SpringValue springValue) { try { //取出修改key对应的environment中的新的value Object value = resolvePropertyValue(springValue); //更新value springValue.update(value); logger.info("Auto update apollo changed value successfully, new value: {}, {}", value, springValue); } catch (Throwable ex) { logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex); } }
-
我们看看springValue怎么更新value。
SpringValue
//更新的时候,同样区分字段和方法 public void update(Object newVal) throws IllegalAccessException, InvocationTargetException { if (isField()) { injectField(newVal); } else { injectMethod(newVal); } } //看到如下修改对应bean的值,都是通过反射的方式处理的。 private void injectField(Object newVal) throws IllegalAccessException { Object bean = beanRef.get(); if (bean == null) { return; } boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(bean, newVal); field.setAccessible(accessible); } private void injectMethod(Object newVal) throws InvocationTargetException, IllegalAccessException { Object bean = beanRef.get(); if (bean == null) { return; } methodParameter.getMethod().invoke(bean, newVal); }
其实更新@ConfigurationProperties和更新@Value不在乎先后顺序。
5. 总结
- config-server修改配置,触发config-client(主动,被动)
- config-client监听到配置修改,发布一个配置修改事件,由监听器去监听刷新Environment
- 刷新Environment之后发布一个Environment更改事件
- 监听器监听到以上事件,刷新@ConfigurationProperties(利用BeanPostProcessor初始化Bean),@Value利用反射方式重新绑定新值。