# Apollo自动更新无法生效的问题排查

# 依赖版本

  • apollo-client 1.4.0
  • spring-boot-starter 2.3.7.RELEASE
  • jasypt-spring-boot-starter 2.1.1

# 问题现象

  1. 使用@Value注解获取apollo的配置
@Value("${apollo.test}")
private Double test;
  1. 程序启动后,能正常从apollo获取配置信息
  2. 在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)

  1. CachingDelegateEncryptablePropertySource自带了一个refresh()可以清理缓存。
  2. 参考jaspyt自带的com.ulisesbocchio.jasyptspringboot.caching.RefreshScopeRefreshedEventListener刷新缓存的方案。
  3. 定义类似的类:
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();
        }
    }
}
  1. 自定义一个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();
    }
}
  1. 通过这个方法在最新版本jasypt-spring-boot:3.0.3,也能实现Apollo的字段刷新。

# 分析

为什么降低版本可以解决这个问题呢,我们来看一下jasypt的实现原理

# 流程

  1. EnableEncryptablePropertiesBeanFactoryPostProcessor.postProcessBeanFactory
    • 读取系统中的所有PropertySources
    • 调用转换PropertySources的方法
  2. EncryptablePropertySourceConverter.convertPropertySources
    • makeEncryptable: 转换所有的PropertySources
    • 遍历转换后的结果,替换系统内的PropertySources
  3. 转换
    • 代理模式
    • 装饰器模式

# 代码清单

  • 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 EncryptableMapPropertySourceWrapper
    • EncryptableEnumerablePropertySourceWrapper EncryptableEnumerablePropertySourceWrapper
    • EncryptablePropertySourceWrapper EncryptablePropertySourceWrapper

# 总结

从这个对比图就可以看出来了,2.1.1版本不仅代理了PropertySource, 还使用了缓存CachingDelegateEncryptablePropertySource,而每次远程通知更新并没有更新缓存,所以导致每次更新从缓存中取数据,从而取不到最新的值。

Last Updated: 6/26/2022, 4:44:06 PM