# Apollo自动更新无法生效的问题排查
# 依赖版本
- apollo-client 1.4.0
- spring-boot-starter 2.3.7.RELEASE
- jasypt-spring-boot-starter 2.1.1
# 问题现象
- 使用@Value注解获取apollo的配置
@Value("${apollo.test}")
private Double test;
- 程序启动后,能正常从apollo获取配置信息
- 在apollo修改配置,热更新无法生效,控制台自动打印的newValue永远是原始值
19:02:25.896 [][Apollo-Config-1] INFO c.c.f.a.s.property.AutoUpdateConfigChangeListener.updateSpringValue 71 - Auto update apollo changed value successfully, new value: 110.0, key: apollo.test, beanName: testBeanService, field: cn.com.grasswort.test.testBeanService.test
# 问题原因
由于jasypt代理了apollo使用的CompositePropertySource,并且将启动时的配置存入缓存。
这样每次接收到远程配置变更通知时,虽然对应的CompositePropertySource已修改。
但jasypt的EncryptableEnumerablePropertySourceWrapper还是会从ConcurrentMapCache的缓存中读取配置,
而非从CompositePropertySource里实时读取配置,这样就会导致引入了jasypt-spring-boot的apollo客户端,
无法实时更新配置。
# 解决方案
# 降级
将jasypt降级为1.16版本即可
# 刷新缓存【来自于官方issue (opens new window)】
- CachingDelegateEncryptablePropertySource自带了一个refresh()可以清理缓存。
- 参考jaspyt自带的com.ulisesbocchio.jasyptspringboot.caching.RefreshScopeRefreshedEventListener刷新缓存的方案。
- 定义类似的类:
public class JaspytRefreshScopeRefreshedEventListener {
private final ConfigurableEnvironment environment;
private final EncryptablePropertySourceConverter converter;
public JaspytRefreshScopeRefreshedEventListener(ConfigurableEnvironment environment, EncryptablePropertySourceConverter converter) {
this.environment = environment;
this.converter = converter;
}
public void refresh() {
refreshCachedProperties();
decorateNewSources();
}
private void decorateNewSources() {
MutablePropertySources propSources = environment.getPropertySources();
converter.convertPropertySources(propSources);
}
private void refreshCachedProperties() {
PropertySources propertySources = environment.getPropertySources();
propertySources.forEach(this::refreshPropertySource);
}
@SuppressWarnings("rawtypes")
private void refreshPropertySource(PropertySource<?> propertySource) {
if (propertySource instanceof CompositePropertySource) {
CompositePropertySource cps = (CompositePropertySource) propertySource;
cps.getPropertySources().forEach(this::refreshPropertySource);
} else if (propertySource instanceof EncryptablePropertySource) {
EncryptablePropertySource eps = (EncryptablePropertySource) propertySource;
eps.refresh();
}
}
}
- 自定义一个ApolloConfigChangeListener监听器,在onChange()方法中如果有属性值更新,刷新缓存。
public class ApolloConfigChangeListener implements ConfigChangeListener {
private JaspytRefreshScopeRefreshedEventListener listener;
public ApolloConfigChangeListener() {
}
public void lazySet(JaspytRefreshScopeRefreshedEventListener listener) {
this.listener = listener;
}
@Override
public void onChange(ConfigChangeEvent changeEvent) {
this.listener.refresh();
}
}
- 通过这个方法在最新版本jasypt-spring-boot:3.0.3,也能实现Apollo的字段刷新。
# 分析
为什么降低版本可以解决这个问题呢,我们来看一下jasypt的实现原理
# 流程
- EnableEncryptablePropertiesBeanFactoryPostProcessor.postProcessBeanFactory
- 读取系统中的所有PropertySources
- 调用转换PropertySources的方法
- EncryptablePropertySourceConverter.convertPropertySources
- makeEncryptable: 转换所有的PropertySources
- 遍历转换后的结果,替换系统内的PropertySources
- 转换
- 代理模式
- 装饰器模式
# 代码清单
- EnableEncryptablePropertiesBeanFactoryPostProcessor
public class EnableEncryptablePropertiesBeanFactoryPostProcessor implements BeanFactoryPostProcessor, ApplicationListener<ApplicationEvent>, Ordered {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
LOG.info("Post-processing PropertySource instances");
EncryptablePropertyResolver propertyResolver = beanFactory.getBean(RESOLVER_BEAN_NAME, EncryptablePropertyResolver.class);
// 读取系统中的所有PropertySources
MutablePropertySources propSources = environment.getPropertySources();
// 转换PropertySources,用代理模式或装饰器模式处理PropertySources,以实现加密解密的功能
convertPropertySources(interceptionMode, propertyResolver, propSources);
}
}
- EncryptablePropertySourceConverter
@Slf4j
public class EncryptablePropertySourceConverter {
public static void convertPropertySources(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, MutablePropertySources propSources) {
// 处理获取到的PropertySource, 转换为EncryptablePropertySource的实现类
StreamSupport.stream(propSources.spliterator(), false)
.filter(ps -> !(ps instanceof EncryptablePropertySource))
.map(ps -> makeEncryptable(interceptionMode, propertyResolver, ps))
.collect(toList())
// 遍历转换后的结果,根据名称替换propSources中的PropertySource
.forEach(ps -> propSources.replace(ps.getName(), ps));
}
@SuppressWarnings("unchecked")
public static <T> PropertySource<T> makeEncryptable(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, PropertySource<T> propertySource) {
// 如果已经处理过,直接返回
if (propertySource instanceof EncryptablePropertySource) {
return propertySource;
}
// 转换PropertySource
PropertySource<T> encryptablePropertySource = convertPropertySource(interceptionMode, propertyResolver, propertySource);
log.info("Converting PropertySource {} [{}] to {}", propertySource.getName(), propertySource.getClass().getName(),
AopUtils.isAopProxy(encryptablePropertySource) ? "AOP Proxy" : encryptablePropertySource.getClass().getSimpleName());
return encryptablePropertySource;
}
private static <T> PropertySource<T> convertPropertySource(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, PropertySource<T> propertySource) {
// 根据InterceptionMode判定按代理模式还是装饰器模式转换PropertySource
return interceptionMode == InterceptionMode.PROXY
? proxyPropertySource(propertySource, propertyResolver) : instantiatePropertySource(propertySource, propertyResolver);
}
/**
* 代理模式
*/
public static MutablePropertySources proxyPropertySources(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, MutablePropertySources propertySources) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(MutablePropertySources.class);
proxyFactory.setProxyTargetClass(true);
proxyFactory.addInterface(PropertySources.class);
proxyFactory.setTarget(propertySources);
proxyFactory.addAdvice(new EncryptableMutablePropertySourcesInterceptor(interceptionMode, propertyResolver));
return (MutablePropertySources) proxyFactory.getProxy();
}
@SuppressWarnings("unchecked")
public static <T> PropertySource<T> proxyPropertySource(PropertySource<T> propertySource, EncryptablePropertyResolver resolver) {
//Silly Chris Beams for making CommandLinePropertySource getProperty and containsProperty methods final. Those methods
//can't be proxied with CGLib because of it. So fallback to wrapper for Command Line Arguments only.
if (CommandLinePropertySource.class.isAssignableFrom(propertySource.getClass())) {
return instantiatePropertySource(propertySource, resolver);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetClass(propertySource.getClass());
proxyFactory.setProxyTargetClass(true);
proxyFactory.addInterface(EncryptablePropertySource.class);
proxyFactory.setTarget(propertySource);
proxyFactory.addAdvice(new EncryptablePropertySourceMethodInterceptor<>(propertySource, resolver));
return (PropertySource<T>) proxyFactory.getProxy();
}
/**
* 装饰器模式
*/
@SuppressWarnings("unchecked")
public static <T> PropertySource<T> instantiatePropertySource(PropertySource<T> propertySource, EncryptablePropertyResolver resolver) {
PropertySource<T> encryptablePropertySource;
if (needsProxyAnyway(propertySource)) {
encryptablePropertySource = proxyPropertySource(propertySource, resolver);
} else if (propertySource instanceof MapPropertySource) {
encryptablePropertySource = (PropertySource<T>) new EncryptableMapPropertySourceWrapper((MapPropertySource) propertySource, resolver);
} else if (propertySource instanceof EnumerablePropertySource) {
encryptablePropertySource = new EncryptableEnumerablePropertySourceWrapper<>((EnumerablePropertySource) propertySource, resolver);
} else {
encryptablePropertySource = new EncryptablePropertySourceWrapper<>(propertySource, resolver);
}
return encryptablePropertySource;
}
@SuppressWarnings("unchecked")
private static boolean needsProxyAnyway(PropertySource<?> ps) {
return needsProxyAnyway((Class<? extends PropertySource<?>>) ps.getClass());
}
private static boolean needsProxyAnyway(Class<? extends PropertySource<?>> psClass) {
return needsProxyAnyway(psClass.getName());
}
/**
* Some Spring Boot code actually casts property sources to this specific type so must be proxied.
*/
private static boolean needsProxyAnyway(String className) {
return Stream.of(
"org.springframework.boot.context.config.ConfigFileApplicationListener$ConfigurationPropertySources",
"org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource"
).anyMatch(className::equals);
}
}
# 差异点
其实不管是2.1.1还是1.16,流程都是这样,具体的差异在于怎么去转换的
- 装饰器模式转换器
- EncryptableMapPropertySourceWrapper
- EncryptableEnumerablePropertySourceWrapper
- EncryptablePropertySourceWrapper
- EncryptableMapPropertySourceWrapper
# 总结
从这个对比图就可以看出来了,2.1.1版本不仅代理了PropertySource, 还使用了缓存CachingDelegateEncryptablePropertySource,而每次远程通知更新并没有更新缓存,所以导致每次更新从缓存中取数据,从而取不到最新的值。