背景

近日业务同学反映在Apollo界面更改配置后, 服务中对应变量的值却没有改变

相关配置key定义如下:

@ApolloJsonValue("${apollo.config.map:{}}")
private Map<String, List<String>> apolloConfigMap;

分析

问题确认

通过远程debug服务发现,更改apollo配置后,服务中变量的值确实没有改变。 重启也不行。

尝试本地复现

在本地编写demo,按照如上变量配置方式配置, 多次修改apollo配置后,变量的值都能即时热更新, 本地复现失败

远程debug

  1. 将项目的代码clone到本地,远程debug
  2. 在apollo热更新代码处打断点,具体是: AutoUpdateConfigChangeListener#onChange方法。
public void onChange(ConfigChangeEvent changeEvent) {
Set<String> keys = changeEvent.changedKeys();
if (CollectionUtils.isEmpty(keys)) {
return;
}
for (String key : keys) {
// 1. check whether the changed key is relevant
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
} // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
if (!shouldTriggerAutoUpdate(changeEvent, key)) {
continue;
} // 3. update the value
for (SpringValue val : targetValues) {
updateSpringValue(val);
}
}
}

这个方法比较简单,循环变更的key, 第一步校验变更的key确实是bean中的属性,第二步校验确实需要热更新bean中属性值,第三步是真正的热更新。

3. 通过调试发现,在第二步时,shouldTriggerAutoUpdate方法返回了false,导致不会进行热更新。

4. 我们来看下shouldTriggerAutoUpdate方法

private boolean shouldTriggerAutoUpdate(ConfigChangeEvent changeEvent, String changedKey) {
ConfigChange configChange = changeEvent.getChange(changedKey); if (configChange.getChangeType() == PropertyChangeType.DELETED) {
return true;
} return Objects.equals(environment.getProperty(changedKey), configChange.getNewValue());
}

逻辑比较简单,返回false的是最后一句, environment中获取到的属性值与apollo中配置的新值不一样。

5. 为什么会不一样?

经过调试发现 key:apollo.config.map的值最终是从com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource中获取,而此类中有一个cache, apollo配置变更时,此cache中存的仍是旧配置。此类是jasypt相关包中的类,此包是与加解密相关的。

关键代码如下:

public Object getProperty(String name) {
// Can be called recursively, so, we cannot use computeIfAbsent.
if (cache.containsKey(name)) {
return cache.get(name);
}
synchronized (name.intern()) {
if (!cache.containsKey(name)) {
Object resolved = getProperty(resolver, filter, delegate, name);
if (resolved != null) {
cache.put(name, resolved);
}
}
return cache.get(name);
}
}

结论

因为Jasypt会封装Apollo的PropertySource类,缓存属性值,导致配置不能热更新

延伸思考

1. 为什么apollo的配置会从jasypt类中获取呢?

我们来看下com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter这个类,这是一个property converter。它的作用即是封装服务中各种的PropertySource, 当服务查询配置的值时,如果配置需要解密的话,可以实现解密。而Apollo也会创建一个PropertySource对象, 也会被jasypt包装,导致配置变更时cache无法更新。

2. 能不能apollo配置变更时更新cache或使cache失效

CachingDelegateEncryptablePropertySource类确实有一个refresh方法,可以清空缓存,下次再查询属性值时,会从真正的PropertySource中获取。而refresh方法是在com.ulisesbocchio.jasyptspringboot.caching.RefreshScopeRefreshedEventListener#onApplicationEvent方法中被调用。可以看出,如果apollo配置变更时发送事件,jasypt的onApplicationEvent应该可以被触发,并清空cache。

经过验证确实可以通过编写一个Apollo配置变更监听器,在监听器中发送ApplicationEvent事件,达到清空Cache的目的。但是经过验证,自己定义的监听器,在AutoUpdateConfigChangeListener#onChange之后执行,还是无法热更新。

Apollo将AutoUpdateConfigChangeListener监听器是放在监听器集合中的第一位的,第一个执行。所以必要要更改的话,需要更改AutoUpdateConfigChangeListener的逻辑,首先发送事件,然后再执行onChange方法中的第二步。 但Apollo将AutoUpdateConfigChangeListener放一位也是有道理的,配置变更先更新配置,再执行其它监听器,因为在其它监听器中也许需要用到热更新后的值。

解决方法

解决方法有三种,需要根据使用的场景不同选择不同的方法

  1. 如果需要用到动态配置,并且动态配置是加密的,就需要修改AutoUpdateConfigChangeListener逻辑,先发送事件。注意新增事件类后,需要配置jasypt.encryptor.refreshed-event-classes,其值为事件类的全限定名称。
  2. 如果需要用到动态配置,但动态配置是不需要加密的,需要修改EncryptablePropertySourceConverter类,使其不包装Apollo相关的PropertySource类。

    public void convertPropertySources(MutablePropertySources propSources) {

    propSources.stream()

    .filter(ps -> !(ps instanceof EncryptablePropertySource))

    .filter(ps -> !(ps instanceof CompositePropertySource && ps.getName().startsWith("Apollo")))

    .map(this::makeEncryptable)

    .collect(toList())

    .forEach(ps -> propSources.replace(ps.getName(), ps));

    }
  3. 不使用Apollo的热更新,属性值直接调用Apolo的Config获取,也能获取到变更后的值。伪代码如下:
Config apolloConfig = ConfigService.getConfig(<namespace>)
- apolloConfig.getProperty()

最新文章

  1. defered,promise回顾
  2. &lt;&lt;一种基于δ函数的图象边缘检测算法&gt;&gt;一文算法的实现。
  3. T-SQL编程 —— 用户自定义函数(标量函数)
  4. JQuery实现table分页
  5. JavaScript执行顺序分析
  6. centos6.4yum搭建lamp环境
  7. Java基础之读文件——使用通道读取混合数据1(ReadPrimesMixedData)
  8. 全栈一路坑之使用django创建博客
  9. Distributed Sentence Similarity Base on Word Mover&#39;s Distance
  10. HW7.8
  11. Ext.Net学习笔记19:Ext.Net FormPanel 简单用法
  12. linux c++ 遍历一个目录下的文件名 (包括子目录的文件名)
  13. [C++]const修饰符
  14. git config全局配置
  15. 使用Swiper轮播插件引起的探索
  16. var/let/const区别何在??(转载)
  17. 如何使Ubuntu Linux12.04 LTS版可以用root用户登陆
  18. [BZOJ2208][Jsoi2010]连通数 暴力枚举
  19. .net安装部署“Error 1001 在初始化安装时发生异常” 的解决方法
  20. Spring Cloud Zuul(服务网关)

热门文章

  1. (Java初学篇)IDEA项目新建流程和软件配置优化以及怎么彻底删除项目
  2. Java中的名称命名规范
  3. 7. url反向解析和静态文件
  4. Asp.Net Core MVC传值 Asp.Net Core API 前台写法
  5. 通过jmeter,将数据库数据查询出来并打印
  6. Python学习之实例1
  7. onps栈使用说明(1)——API接口手册
  8. 网页嵌入zabbix页面(不同域名)
  9. perl中ENV的使用
  10. 5、有一行电文,译码规律为: a ——&gt; z b——&gt; y c ——&gt; x. 即把第一个字母变成第26个字母, 第i个字母变成第(26-i+1)个字母, 非字母字符不变