乐趣区

关于spring:使用Nacos实现Spring的ConfigurationProperties的map类型key动态更新删除不了的问题解决方案

遇到一个 nacos 配置动静更新 map 的问题,对于 map 的 key 能够新增,不能删除的问题排查,比方我有个

 map:
   a:a
   b:b

这里我能够批改配置进行追加 c:c,变成

 map:
   a:a
   b:b
   c:c

这样是能够的,然而不能删除,比方删除掉 a:a,后果还是

 map:
   a:a
   b:b
   c:c

源码剖析一波,进行排查
首先要介绍一下 spring 的各生命周期周期,这里动静更新就用到了 RefreshEvent 事件,在 NacosContextRefresher#registerNacosListener() 里,nacos 实现了一个批改配置的回调监听,并且会播送一个 RefreshEvent 事件
而后 RefreshEventListener 会收到事件,并调用 ContextRefresher#refresh()->refreshEnvironment(),执行 updateEnvironment(),从新加载 Context 的 Environment,之后持续播送一个 EnvironmentChangeEvent 事件,ConfigurationPropertiesRebinder 接管到该事件后,会调用 rebind(),这里就是更改具体 @ConfigurationProperties 的配置类了,具体做法就是在该办法里调用了

this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);

先 destroy 原来的 bean,而后在从新初始化 bean。而 spring 初始化 bean 的过程中就蕴含了一个 BeanPostProcessor 回调解决,其中实现类 ConfigurationPropertiesBindingPostProcessor,就会对 @ConfigurationProperties 的配置类进行解决,把配置文件内容(比方 application.yml,systemProperties 等,有优先级程序,如果是同一个属性还会笼罩)和配置类字段进行 bind,bind()->bindObject(),这里会依据类型判断执行哪种策略,首先是判断复合类型的

AggregateBinder 有三种实现:MapBinder、CollectionBind、ArrayBinder
再而后是 bindProperty(),应用的是 spring 的 Converter,最初是 bindDataObject()
DataObjectBinder 有两种实现:JavaBeanBinder、ValueObjectBinder

我的配置类的属性就是一个 map 类型,所以进入到 MapBinder 里,这里在调用 AggregateBinder#bind()会执行子类 MapBinder 的 merge()
问题就出在这个办法里,

 @Override
       protected Map<Object, Object> merge(Supplier<Map<Object, Object>> existing, Map<Object, Object> additional) {Map<Object, Object> existingMap = getExistingIfPossible(existing);
        if (existingMap == null) {return additional;}
        try {existingMap.putAll(additional);
            return copyIfPossible(existingMap);
       }
        catch (UnsupportedOperationException ex) {Map<Object, Object> result = createNewMap(additional.getClass(), existingMap);
            result.putAll(additional);
            return result;
       }
   }

能够看到这里 existingMap.putAll(additional); 是把旧的 map.putAll 新的 map,对于删除操作,旧 map 的数据更全,新 map 的数据少一下
这时候进行 put,所以数据和旧 map 一样,没有删除成果
那有没有方法实现删除呢,也是有的,就是在后面的从新初始化 bean 前回调的 destroy,只有咱们在销毁 bean 时把 map==null
则实现了全量删除再新增的成果,以此实现删除成果

 @PreDestroy
 public void destroy() {map = null;}

题外:能够看到咱们加载 configuration 配置时应用的是 ConfigurationPropertySource 类,然而咱们应用 nacos 的配置也好 NacosPropertySource,还是其余配置也好,父类都是 PropertySource,这里 spring 会把 PropertySource 包装成 SpringConfigurationPropertySource(ConfigurationPropertySource 的子类),具体可见 SpringConfigurationPropertySource.from(source);

退出移动版