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 实现类中又会持有两个成员变量,分别是:ConversionService
与PropertySources
;首先,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缓存对象,只有监听到RefreshScopeRefreshedEvent和EnvironmentChangeEvent事件的时候才会刷新缓存,如下图代码所示。
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容器实现自动装配替换,这里设置了装配条件,只有类路径中同时存在AutoUpdateConfigChangeListener和CachingDelegateEncryptablePropertySource才会生效配置,如果多个项目都需要使用可以通过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();
}
}
});
}
}