背景

近日业务同学反映在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()

Jasypt与Apollo一起使用造成Apollo热更新失效问题分析的更多相关文章

  1. vue-element-admin开发模式下style标签热更新失效[解决办法]

    参考:https://forum.vuejs.org/t/vue-cli-3-x-style/46306/3 vue.config.js添加配置 css: { sourceMap: false, mo ...

  2. uniapp热更新和整包升级

    一. uniapp热更新  (热更新官方文档) 很多人在开发uniapp的时候, 发现热更新失效问题(或者热更新没有更新manifest里的新增模块,SDK,原生插件包括云插件), 其实uniapp官 ...

  3. Webpack与Vite热更新差异对比

    随着项目的日渐迭代,项目整体的代码量也会越来越多,从而导致项目体积越来越大:在Webpack时代,很多人会对历史项目(巨型项目)感到头疼,因为往往巨型项目在本地开发调试的时候会因为本地代码的修改触发H ...

  4. spring cloud 集成分布式配置中心 apollo(单机部署apollo)

    一.什么是apollo? Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限.流程治理等特性,适用 ...

  5. 腾讯开源手游热更新方案,Unity3D下的Lua编程

    原文:http://www.sohu.com/a/123334175_355140 作者|车雄生 编辑|木环 腾讯最近在开源方面的动作不断:先是微信跨平台基础组件Mars宣布开源,腾讯手游又于近期开源 ...

  6. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  7. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  8. 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

    上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...

  9. iOS热更新-8种实现方式

    一.JSPatch 热更新时,从服务器拉去js脚本.理论上可以修改和新建所有的模块,但是不建议这样做. 建议 用来做紧急的小需求和 修复严重的线上bug. 二.lua脚本 比如: wax.热更新时,从 ...

  10. 【.net 深呼吸】程序集的热更新

    当一个程序集被加载使用的时候,出于数据的完整性和安全性考虑,程序集文件(在99.9998%的情况下是.dll文件)会被锁定,如果此时你想更新程序集(实际上是替换dll文件),是不可以操作的,这时你得把 ...

随机推荐

  1. 图解 | 聊聊 MyBatis 缓存

    首发公众号-悟空聊架构:图解 | 聊聊 MyBatis 缓存 你好,我是悟空. 本文主要内容如下: 一.MyBatis 缓存中的常用概念 MyBatis 缓存:它用来优化 SQL 数据库查询的,但是可 ...

  2. laravel 浏览器谷歌network返回报错html

    laravel 在谷歌报错的时候会返回html,对于调试来说很不方便.原因是在于: 这里返回的格式是json,但是报错时候返回的是整个html所以 相对路径: app\Exceptions\Handl ...

  3. 数据结构中的哈希表(java实现)利用哈希表实现学生信息的存储

    哈希表 解释 哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过把关键码映射的位置去寻找存放值的地方 内存结构分析图 1.定义一个类为结点,存储的信息 2.定义链表的相关操作 3.定义一个数组存 ...

  4. Linux之Docker-01

    一.镜像基础命令 1.docker version  [root@DY-Ubuntu-01 ~]#docker version               #查看 Docker 版本 2.docker ...

  5. appium 移动端自动化测试工具

    appium 移动端自动化测试工具 appium 移动端自动化测试工具 appium 是哟个自动化测试开源工具 支持ios和安卓平台的应用 以及web应用和混合应用 appium 是一个跨平台的工具: ...

  6. jmeter执行报错:java.lang.UnsupportedClassVersionError解决办法

    做个记录. 问题记录: jmeter版本:5.4.1 本地Java版本:1.8.0_151 执行jmeter,报错: 2022-10-14 12:06:27,372 ERROR o.a.j.JMete ...

  7. 通过jmeter,造50个单位的并发用户数据(用户从上至下每50个单位依次排序)

    单位数据:loginfoll.csv         #50个单位的信息(agencyCode,passwd,agencyname,rgcode) 用户数据:50个单位,每个单位200个用户(用户名为 ...

  8. redis的缓存穿透、击穿、雪崩以及实用解决方案

    今天来聊聊redis的缓存穿透.击穿.雪崩以及解决方案,其中解决方案包括类似于布隆过滤器这种网上一搜一大片但是实际生产部署有一定复杂度的,也有基于spring注解通过一行代码就能解决的,其中各有优劣, ...

  9. 查看服务器出口ip

    [root@iZap201hv2fcgry1alvbznZ ~]# curl cip.cc IP : xxx.xxx.xx.xx 地址 : 中国 浙江 绍兴 运营商 : 移动 数据二 : 浙江省绍兴市 ...

  10. 2022春每日一题:Day 13

    题目:后缀排序 什么是后缀数组?他主要包含两个数组:sa和rk. 其中sa[i]表示将字符串后缀排序后第i小的编号,rk[i]表示后缀i的排名. 显然sa[rk[i]]=i,rk[sa[i]]=i. ...