使用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 merge(Supplier> existing, Map additional) {
        Map existingMap = getExistingIfPossible(existing);
        if (existingMap == null) {
            return additional;
       }
        try {
            existingMap.putAll(additional);
            return copyIfPossible(existingMap);
       }
        catch (UnsupportedOperationException ex) {
            Map 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);

你可能感兴趣的:(使用Nacos实现Spring的ConfigurationProperties的map类型key动态更新删除不了的问题解决方案)