Apollo/Nacos配置动态刷新原理及优劣
一. 配置方式
这里只说与Spring集成后的配置方式,这也是项目中主要使用的方式
Apollo
- 在属性上直接加@value注解,这个属性就会随着配置的更改动态更新
- 类实现ConfigChangeListener,在类中方法上@ApolloConfigChangeListener注解,注解在方法上,监控配置的变化,配置变化后会自定义方法来达到动态刷新bean的目的
Nacos
- Nacos无需任何配置,即可对有@ConfigurationProperties注解的类进行配置的动态刷新
- Nacos自2022.0.0.0-RC1版本后可通过spring.cloud.nacos.config.refresh-bahavior指定刷新模式,默认是all_beans(刷新所有bean),可选specific_bean (只刷新有配置值更改的bean)
优劣对比
先说结论,二者的优劣势对比,具体的原理机制等相对复杂,放后文详细讲解
Apollo
优势
- 提供出listener,使用者可灵活自行定制bean的刷新方式
劣势
- Apollo除@Value注解外不提供动态刷新的默认实现方案,而@Value注解平常用的较少,就比较鸡肋,用户想要用@ConfigurationProperties配置类的方式动态刷新,必须要自己去实现
Nacos
优势
- 有动态刷新的默认实现,用户可直接使用,且从2022.0.0.0-RC1版本后可自选bean的刷新模式
劣势
- 不管选用哪种nacos的刷新方案,RefreshScope域都会全刷新,触发RefreshScopeRefreshedEvent事件的发布,如eureka会订阅该事件,并于该事件发布时,触发eurekaClient的重新注册,若是配置热更新的比较频繁,那么会触发eurekaClient的频繁重注册
最终抉择
- 动态热更新的时候,肯定是更新哪个配置,那么只将与这个配置对应的bean进行更新最好,若是用的nacos,那么可选用2022.0.0.0-RC1版本,bean的刷新行为选用specific_bean指定刷新,若是用的Apollo,需自己实现,也可参考nacos中的SmartConfigurationPropertiesRebinder类,进行自实现,配置动态刷新后,建议还是要发布下RefreshScopeRefreshedEvent事件,使得依赖该事件发布的其他组件,在配置刷新后,可重新配置与该之相关的的内容,避免真的更改了例如eureka的配置后,eureka因不能重注册client导致的配置无法生效的问题。这里比较坑的地方就是springcloud没有提供一种机制,可监听自己的配置更改及事件发布对应着触发事件,进行导致只是更改了用户自定义的一些配置也触发了eureka客户端重注册这种看似风马牛不相及的行为出现
动态刷新机制
这里只以nacos为例,重点讲解服务通过监听器拿到配置变更之后的流程,Apollo在自行实现时,也可参考此流程
- NacosContextRefresher监听ApplicationReadyEvent事件,会在应用准备启动时间发布后,注册NacosListener
- 在注册时,会实现listener的innerReceive方法,在配置变更后,会通知到该方法,该方法会触发事件的发布:
applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// many Spring contextif (this.ready.compareAndSet(false, true)) {this.registerNacosListenersForApplications();}}/*** register Nacos Listeners.*/private void registerNacosListenersForApplications() {if (isRefreshEnabled()) {for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {if (!propertySource.isRefreshable()) {continue;}String dataId = propertySource.getDataId();registerNacosListener(propertySource.getGroup(), dataId);}}}private void registerNacosListener(final String groupKey, final String dataKey) {String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);Listener listener = listenerMap.computeIfAbsent(key,lst -> new AbstractSharedListener() {@Overridepublic void innerReceive(String dataId, String group,String configInfo) {refreshCountIncrement();nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);// 这里发布RefreshEvent事件,用以刷新bean实例applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));if (log.isDebugEnabled()) {log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s",group, dataId, configInfo));}}});try {configService.addListener(dataKey, groupKey, listener);log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey,groupKey);}catch (NacosException e) {log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,groupKey), e);}}
- bean刷新的处理,在订阅了该事件的RefreshEventListener中
public void handle(RefreshEvent event) {if (this.ready.get()) { // don't handle events before app is readylog.debug("Event received " + event.getEventDesc());Set<String> keys = this.refresh.refresh();log.info("Refresh keys changed: " + keys);}}
- 程序流转到ContextRefresher的refresh方法中
public synchronized Set<String> refresh() {// 刷新环境变量Set<String> keys = refreshEnvironment();// 刷新RefreshScope内的所有缓存this.scope.refreshAll();return keys;}
- 我们追溯到refreshEnvironment方法内,其内的重点有两处,一处是在这里获取到了配置中心更改的值,另一处,则将更改的值放入EnvironmentChangeEvent事件中进行发布
public synchronized Set<String> refreshEnvironment() {Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();Set<String> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));return keys;
- 我们找到订阅EnvironmentChangeEvent该事件的类SmartConfigurationPropertiesRebinder,跟到onApplicationEvent方法,该方法会根据refreshBehavior选择对应的bean刷新方式
@Overridepublic void onApplicationEvent(EnvironmentChangeEvent event) {if (this.applicationContext.equals(event.getSource())// Backwards compatible|| event.getKeys().equals(event.getSource())) {switch (refreshBehavior) {case SPECIFIC_BEAN -> rebindSpecificBean(event);default -> rebind();}}}
- 我们先跟踪到默认处理方案:rebind方法中,该方法在当前类中的父类:ConfigurationPropertiesRebinder中实现
@ManagedOperationpublic void rebind() {this.errors.clear();for (String name : this.beans.getBeanNames()) {rebind(name);}}
- 跟踪到rebind(name)方法,可以看到在本方法中,对指定name的bean进行destory(销毁)并且重新initialize(实例化)
@ManagedOperationpublic boolean rebind(String name) {if (!this.beans.getBeanNames().contains(name)) {return false;}if (this.applicationContext != null) {try {Object bean = this.applicationContext.getBean(name);if (AopUtils.isAopProxy(bean)) {bean = ProxyUtils.getTargetObject(bean);}if (bean != null) {if (getNeverRefreshable().contains(bean.getClass().getName())) {return false; // ignore}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;}
- 到这里我们就知道,原来在这里进行了bean的重新实例化,那么重新实例化的这些bean是什么bean呢,beans变量是关键线索,beans对应着ConfigurationPropertiesBeans类,当前类中注入了ConfigurationPropertiesBeans对象,我们跟踪到该对象,看该类的注释,可知该类是当前相聚中所有包含@ConfigurationProperties注解的bean的集合类
/*** Collects references to <code>@ConfigurationProperties</code> beans in the context and* its parent.**/
- 也就是说,在默认的情况下,随着配置的变更,会导致所有包含@ConfigurationProperties注解的bean重新绑定
- 接下来,我们回到第6步,进入SmartConfigurationPropertiesRebinder类的rebindSpecificBean(event)方法中
private void rebindSpecificBean(EnvironmentChangeEvent event) {Set<String> refreshedSet = new HashSet<>();beanMap.forEach((name, bean) -> event.getKeys().forEach(changeKey -> {String prefix = AnnotationUtils.getValue(bean.getAnnotation()).toString();// prevent multiple refresh one ConfigurationPropertiesBean.if (changeKey.startsWith(prefix) && refreshedSet.add(name)) {rebind(name);}}));}
- 可以看出该方法主要做的是事情是遍历beanMap,拿到对应的bean及其对应的前缀(etc: spring.xxx)然后再拿到变更的key,若匹配则才对当前bean进行更新,以此方法实现了只更新特定的bean,而不会像第10步一样全部更新
- 可以看出当前beanMap是核心,这个beanMap怎么得到的呢,可以看出它是在当前类构造函数中就已填充了
public SmartConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {super(beans);fillBeanMap(beans);}@SuppressWarnings("unchecked")private void fillBeanMap(ConfigurationPropertiesBeans beans) {this.beanMap = new HashMap<>();Field field = ReflectionUtils.findField(beans.getClass(), "beans");if (field != null) {field.setAccessible(true);this.beanMap.putAll((Map<String, ConfigurationPropertiesBean>) Optional.ofNullable(ReflectionUtils.getField(field, beans)).orElse(Collections.emptyMap()));}}
- 可看出它获取到父类的beans字段所对应值,而后采用反射最终取出ConfigurationPropertiesBeans类中的beans对象,放入当前的beanMap中
- 至此,对应第4步中的refreshEnvironment()方法执行完毕,接下来我们跟踪进入到RefreshScope的refreshAll方法中
@ManagedOperation(description = "Dispose of the current instance of all beans "+ "in this scope and force a refresh on next method execution.")public void refreshAll() {super.destroy();this.context.publishEvent(new RefreshScopeRefreshedEvent());}
- 我们跟踪进GenericScope的destory方法中
@Overridepublic void destroy() {List<Throwable> errors = new ArrayList<Throwable>();Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();for (BeanLifecycleWrapper wrapper : wrappers) {try {Lock lock = this.locks.get(wrapper.getName()).writeLock();lock.lock();try {wrapper.destroy();}finally {lock.unlock();}}catch (RuntimeException e) {errors.add(e);}}if (!errors.isEmpty()) {throw wrapIfNecessary(errors.get(0));}this.errors.clear();}
- 我们看到destroy方法会对当前的域缓存进行清空,若清空时返回数据,则会对缓存对应的bean进行destroy,之后会有其他地方对bean重新创建,这里也就是会对所有属于RefreshScope域的对象进行的重新实例化
- 在destroy执行后,会发布RefreshScopeRefreshedEvent事件,我们可以看查找所有订阅该事件的类,就可知有哪些组件会受此影响了