Apollo热更新失效问题

Apollo远程配置变更后,应用中@Value注解对应的属性值无法正常更新即Apollo配置热更新无法生效,且无论配置如何变更,配置值永远都是应用启动时的配置值。

版本

<dependency>
  <groupId>com.ctrip.framework.apollo</groupId>
  <artifactId>apollo-client</artifactId>
  <version>1.9.0</version>
</dependency>

<!-- 数据库连接加密 -->
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

问题分析

分析过程

如果觉得分析过程比较繁琐,可以跳过直接看结论。

经过调试发现,Apollo动态更新配置的主要实现在com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener类。具体代码如下图所示。

public class AutoUpdateConfigChangeListener implements ConfigChangeListener{

    @Override
    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;
            }

            // 3. update the value
            for (SpringValue val : targetValues) {
                updateSpringValue(val);
            }
        }
    }

    
    private void updateSpringValue(SpringValue springValue) {
        try {
            // 此处根据更新的key提取对应的value
            Object value = resolvePropertyValue(springValue);
            springValue.update(value);

            logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,
                        springValue);
        } catch (Throwable ex) {
            logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);
        }
    }


    private Object resolvePropertyValue(SpringValue springValue) {
        // 委托给placeholderHelper对象获取具体配置值
        Object value = placeholderHelper
        .resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder());

        // ... 此处省略其他代码
        return value;
    }
}

可以看到AutoUpdateConfigChangeListener会根据监听ConfigChangeEvent事件,提取到本次变动的配置的Key列表。resolvePropertyValue方法委托给placeholderHelper对象根据Key获取对应的配置值。

public String resolveEmbeddedValue(@Nullable String value) {
    if (value == null) {
        return null;
    } else {
        String result = value;

        for(StringValueResolver resolver : this.embeddedValueResolvers) {
            result = resolver.resolveStringValue(result);
            if (result == null) {
                return null;
            }
        }

        return result;
    }
}

// embeddedValueResolvers来源,可以看到最终委托给getEnvironment().resolvePlaceholders(strVal)
if (!beanFactory.hasEmbeddedValueResolver()) {
    beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}

如上图所示,跟踪placeholderHelper对象的resolvePropertyValue方法可以看到最终会委托给Spring的Environment对象来提取真正的配置值。

这里简单介绍下Environment获取配置的流程,Environment 实现类中会持有一个PropertyResolver类型的成员变量,进而交由 PropertyResolver 负责执行 getProperty() 逻辑。PropertyResolver 实现类中又会持有两个成员变量,分别是:ConversionServicePropertySources;首先,PropertyResolver 遍历 PropertySources 中的 PropertySource,获取原生属性值;然后委派 ConversionService 对原生属性值进行数据类型转换 (如果有必要的话)。虽然 PropertySource 自身是具备根据属性名获取属性值这一能力的,但不具备占位符解析与类型转换能力,于是在中间引入具备这两种能力的 PropertyResolver。

最终调试追踪到对应的配置由com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource查询返回,如下图代码所示

public class CachingDelegateEncryptablePropertySource<T> extends PropertySource<T> implements EncryptablePropertySource<T> {

    private final Map<String, Object> cache;
    
    @Override
    public PropertySource<T> getDelegate() {
        // 被代理的Apollo PropertySource对象
        return delegate;
    }

    @Override
    public Object getProperty(String name) {
        // 问题出现在这里,由于缓存未被刷新导致所有的配置值都拿到的是老的,导致无法动态刷新
        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);
        }
    }


    @Override
    public void refresh() {
        // 情况配置缓存方法,后续解决问题会调用该方法
        log.info("Property Source {} refreshed", delegate.getName());
        cache.clear();
    }
}

破案了,jasypt框架com.ulisesbocchio.jasyptspringboot.wrapper.EncryptablePropertySourceWrapper类会对大部分PropertySource进行包装代理,最终委托给com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource进行配置值的返回。

CachingDelegateEncryptablePropertySource类本身维护了一个cache缓存对象,只有监听到RefreshScopeRefreshedEventEnvironmentChangeEvent事件的时候才会刷新缓存,如下图代码所示。

package com.ulisesbocchio.jasyptspringboot.caching;

@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class RefreshScopeRefreshedEventListener implements ApplicationListener<ApplicationEvent> {

    public static final String REFRESHED_EVENT_CLASS = "org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent";
    public static final String ENVIRONMENT_EVENT_CLASS = "org.springframework.cloud.context.environment.EnvironmentChangeEvent";
    
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (isAssignable(ENVIRONMENT_EVENT_CLASS, event) || isAssignable(REFRESHED_EVENT_CLASS, event)) {
            log.info("Refreshing cached encryptable property sources");
            refreshCachedProperties();
            decorateNewSources();
        }
    }
    
    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();
        }
    }
}

结论

总结一下,Apollo框架下的AutoUpdateConfigChangeListener类负责监听配置变化并对@Value注解的属性进行动态更新,刷新的配置值则通过Environment获取。而Jasypt框架会对绝大部分PropertySource配置进行包装代理,并委托给CachingDelegateEncryptablePropertySource类获取实际的配置值。最终获取最新配置时被CachingDelegateEncryptablePropertySource拦截并返回了cahce中的旧的配置值。以下是时序图。

解决思路

前面已经明确了问题是因为缓存导致的,那解决思路即在AutoUpdateConfigChangeListener#onChange方法前清空配置缓存,而CachingDelegateEncryptablePropertySource本身提供了refresh接口清空配置缓存(参考前面代码)。

首先想到的是通过切面的方式对AutoUpdateConfigChangeListener#onChange方法进行拦截,可惜的是AutoUpdateConfigChangeListener是直接new出来的,不是Spring容器托管的,该方案只能放弃。

好在Apollo提供了ConfigPropertySourcesProcessor这个核心组件的替换方式,而ConfigPropertySourcesProcessor是装载AutoUpdateConfigChangeListener类的核心组件,所以我们只需要定义一个自己的ConfigPropertySourcesProcessor并且替换掉AutoUpdateConfigChangeListener(实现一个能在onChange之前刷新缓存的AutoUpdateConfigChangeListener的子类)即可修复该问题。

package com.ctrip.framework.apollo.spring.boot;

@Configuration
@ConditionalOnProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED)
@ConditionalOnMissingBean(PropertySourcesProcessor.class)
public class ApolloAutoConfiguration {

    @Bean
    public ConfigPropertySourcesProcessor configPropertySourcesProcessor() {
        return new ConfigPropertySourcesProcessor();
    }
}

具体实现

首先一个实现一个能在onChange之前刷新缓存的AutoUpdateConfigChangeListener子类

核心的逻辑在refreshPropertySource(PropertySource<?> propertySource)方法对Environment下所有的EncryptablePropertySource进行refresh,如下图代码所示。

public class ApolloRefreshCacheConfigChangeListener extends AutoUpdateConfigChangeListener {

    private final Environment environment;

    public ApolloRefreshCacheConfigChangeListener(Environment environment, ConfigurableListableBeanFactory beanFactory) {
        super(environment, beanFactory);
        this.environment = environment;
    }


    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
        refreshCachedProperties();
        super.onChange(changeEvent);
    }

    
    
    private void refreshCachedProperties() {
        if (this.environment instanceof ConfigurableEnvironment) {
            ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
            PropertySources propertySources = configurableEnvironment.getPropertySources();
            propertySources.forEach(this::refreshPropertySource);
        }
    }


    /**
     * 刷新EncryptablePropertySource缓存,
     * 如果是CompositePropertySource则递归刷新内部的EncryptablePropertySource缓存
     * @param propertySource
     */
    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();
        }
    }
}

实现自定义的ConfigPropertySourcesProcessor替换Apollo原生的ConfigPropertySourcesProcessor,由于原生的ConfigPropertySourcesProcessor内部的字段都是用private修饰的,扩展性较差,这里只能完全复制其全部代码并稍加改动进行替换。

public class JasyptCompatibleConfigPropertySourceProcessor extends ConfigPropertySourcesProcessor {
    
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        this.configUtil = ApolloInjector.getInstance(ConfigUtil.class);
        initializePropertySources();
        initializeAutoUpdatePropertiesFeature(beanFactory);
    }

    private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
        if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() ||
                !AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {
            return;
        }

        // 这里使用自定义的ApolloRefreshCacheConfigChangeListener代替Apollo原生的AutoUpdateConfigChangeListener
        AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new ApolloRefreshCacheConfigChangeListener(
                environment, beanFactory);

        List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
        for (ConfigPropertySource configPropertySource : configPropertySources) {
            configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
        }
    }

    // 这里省略其他复制的代码
}

最后创建一个配置类将JasyptCompatibleConfigPropertySourceProcessor注册到Spring容器实现自动装配替换,这里设置了装配条件,只有类路径中同时存在AutoUpdateConfigChangeListenerCachingDelegateEncryptablePropertySource才会生效配置,如果多个项目都需要使用可以通过Spring的SPI机制并打成JAR包实现自动装配生效,这里就不赘述了。

@Configuration
@ConditionalOnClass(value = {AutoUpdateConfigChangeListener.class, CachingDelegateEncryptablePropertySource.class})
public class ApolloJasyptCompatibleConfiguration {

    @Bean
    public ConfigPropertySourcesProcessor configPropertySourcesProcessor() {
        return new JasyptCompatibleConfigPropertySourceProcessor();
    }
}

至此,问题解决。

实现@ConfigurationProperties注解的Bean自动刷新属性值

这里通过@ApolloConfigChangeListener注解实现一个配置变更的监听器进而转发一个EnvironmentChangeEvent来实现,这样@ConfigurationProperties注解的Bean的属性值就能进行热更新了。

这里需要注意的是@ApolloConfigChangeListener注解配置的value值要与应用关注的Apollo的NAMESPACE相对应。

@Configuration
public class ApolloRefreshConfiguration implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    
    @ApolloConfigChangeListener(value = {"application"})
    public void publishEnvironmentChangeEventWhenConfigChange(ConfigChangeEvent event) {
        Set<String> changedKeys = event.changedKeys();
        if (CollectionUtils.isEmpty(changedKeys)) {
            return;
        }
        
        applicationContext.publishEvent(new EnvironmentChangeEvent(changedKeys));
    }
}

扩展

网上有通过@ApolloConfigChangeListener注解实现一个配置变更的监听器进而转发一个EnvironmentChangeEvent来修复Apollo与Jasypt不兼容导致热更新失效的方案,但是一会有效一会又无效的,这里一起解答下原因。

理论上该方案是可行的,前面问题原因分析的时候也介绍了RefreshScopeRefreshedEventListener该监听器接收到EnvironmentChangeEvent事件会刷新EncryptablePropertySource缓存。

但问题在于apollo-client代码里无论是@ApolloConfigChangeListener注解生成的监听器还是自带的AutoUpdateConfigChangeListener都是监听ConfigChangeEvent事件,且都会维护在com.ctrip.framework.apollo.internals.AbstractConfig内部属性m_listeners,且AutoUpdateConfigChangeListener早于@ApolloConfigChangeListener注解生成的监听器置入。

而在实现事件监听的时候Apollo采用了线程池的方式使各个监听器并行消费事件(代码如下),导致EnvironmentChangeEvent事件的转发有时晚于或者早于AutoUpdateConfigChangeListener#onChange的实现逻辑,所以就导致了热更新一会有效一会又无效的现象。

protected void fireConfigChange(final ConfigChangeEvent changeEvent) {
    for (final ConfigChangeListener listener : m_listeners) {
        // check whether the listener is interested in this change event
        if (!isConfigChangeListenerInterested(listener, changeEvent)) {
            continue;
        }
        m_executorService.submit(new Runnable() {
            @Override
            public void run() {
                String listenerName = listener.getClass().getName();
                Transaction transaction = Tracer.newTransaction("Apollo.ConfigChangeListener", listenerName);
                try {
                    // 这里包括Auto
                    listener.onChange(changeEvent);
                    transaction.setStatus(Transaction.SUCCESS);
                } catch (Throwable ex) {
                    transaction.setStatus(ex);
                    Tracer.logError(ex);
                    logger.error("Failed to invoke config change listener {}", listenerName, ex);
                } finally {
                    transaction.complete();
                }
            }
        });
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值