描述bug
使用RefreshScope刷新被@ConfigurationProperties注解的配置类
在调用RefreshScope.refreshAll()后访问被刷新的配置类,发现配置类的内容有一定概率为旧值
复现
修改配置时概率复现
期望
调用RefreshScope.refreshAll()后访问被刷新的配置类,应该获取到新的配置内容
截图
为了保证我的listener执行时相关的配置bean已经刷新,我参考ApolloConfigChangeListener实现了PostRefreshConfigChangeListener,所有PostRefreshConfigChangeListener在RefreshScope.refreshAll()执行完之后才会执行,ApolloConfigChangeListener负责执行RefreshScope.refreshAll()和触发listener,相关代码如下
@Component
@RequiredArgsConstructor
@Slf4j
public class ApolloAutoRefreshConfig implements BeanPostProcessor {
public static final String APOLLO_NAMESPACES = "${apollo.bootstrap.namespaces}";
private final RefreshScope refreshScope;
private final Environment environment;
private final List<PostRefreshConfigChangeListener> listeners;
private static final String NAMESPACE_DELIMITER = ",";
private static final Splitter NAMESPACE_SPLITTER = Splitter.on(NAMESPACE_DELIMITER)
.omitEmptyStrings().trimResults();
@ApolloConfigChangeListener(value = APOLLO_NAMESPACES)
@Trace
public void onChange(ConfigChangeEvent changeEvent) {
refreshScope.refreshAll();
log.info("Apollo config refreshed, namespace: {}, keys: {}", changeEvent.getNamespace(), changeEvent.changedKeys());
for (PostRefreshConfigChangeListener listener : listeners) {
if (listenerInterestedInEvent(listener, changeEvent)) {
listener.onChange(changeEvent);
}
}
}
private boolean listenerInterestedInEvent(PostRefreshConfigChangeListener listener, ConfigChangeEvent changeEvent) {
String namespace = changeEvent.getNamespace();
Set<String> changedKeys = changeEvent.changedKeys();
Set<String> interestedNamespaces = listener.getNamespaces();
Set<String> interestedKeys = listener.getInterestedKeys();
Set<String> interestedKeyPrefixes = listener.getInterestedKeyPrefixes();
if (!interestedNamespaces.contains(namespace)) {
return false;
}
if (CollectionUtils.isEmpty(interestedKeys) && CollectionUtils.isEmpty(interestedKeyPrefixes)) {
return true;
}
if (interestedKeys != null) {
for (String interestedKey : interestedKeys) {
if (changedKeys.contains(interestedKey)) {
return true;
}
}
}
if (interestedKeyPrefixes != null) {
for (String prefix : interestedKeyPrefixes) {
for (String changedKey : changedKeys) {
if (changedKey.startsWith(prefix)) {
return true;
}
}
}
}
return false;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class<?> clazz = bean.getClass();
List<Method> methods = new ArrayList<>();
ReflectionUtils.doWithMethods(clazz, methods::add);
for (Method method : methods) {
PostRefreshApolloConfigChangeListener annotation = AnnotationUtils
.findAnnotation(method, PostRefreshApolloConfigChangeListener.class);
if (annotation == null) {
continue;
}
Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1,
"Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
method);
Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
"Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
method);
ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
Set<String> resolvedNamespaces = new HashSet<>();
for (String namespace : namespaces) {
String resolvedNamespace = environment.resolveRequiredPlaceholders(namespace);
if (resolvedNamespace.contains(NAMESPACE_DELIMITER)) {
resolvedNamespaces.addAll(NAMESPACE_SPLITTER.splitToList(resolvedNamespace));
} else {
resolvedNamespaces.add(resolvedNamespace);
}
}
Set<String> interestedKeys =
annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
Set<String> interestedKeyPrefixes =
annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes)
: null;
PostRefreshConfigChangeListener listener = new PostRefreshConfigChangeListener(bean, method, resolvedNamespaces, interestedKeys, interestedKeyPrefixes);
listeners.add(listener);
log.info("add custom listener {}", beanName);
}
return bean;
}
@RequiredArgsConstructor
@Getter
public static class PostRefreshConfigChangeListener implements ConfigChangeListener {
private final Object bean;
private final Method method;
private final Set<String> namespaces;
private final Set<String> interestedKeys;
private final Set<String> interestedKeyPrefixes;
@Override
public void onChange(ConfigChangeEvent configChangeEvent) {
ReflectionUtils.invokeMethod(method, bean, configChangeEvent);
}
}
}
@RefreshScope
@Component
@ConfigurationProperties("props")
@Getter
@Setter
public class MyProps {
private List<Pair> pairs;
@Getter
@Setter
public static class Pair {
private String k;
private String v;
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class MyListener {
private final MyProps myProps;
private Map<String, String> map;
@PostRefreshApolloConfigChangeListener(value = "${apollo.bootstrap.namespaces}", interestedKeyPrefixes = {"props"})
public void onChange(ConfigChangeEvent changeEvent) {
this.map = myProps.getPairs().stream().collect(Collectors.toMap(Pair::getK, Pair::getV));
log.info("Loaded Map: {}", this.map.keySet());
}
}
配置变更
发布前配置
props:
pairs:
- k: k1
v: v1
发布后配置
props:
pairs:
- k: k1
v: v1
- k: k2
v: v2
出现问题时的输出日志,可以看到ConfigChangeEvent中有k2的条目,但访问配置bean时只加载到了k1
INFO [Apollo-Config-2] [TID: N/A] c.t.n.e.r.c.ApolloAutoRefreshConfig: Apollo config refreshed, namespace: application.yaml, keys: [props.pairs[1].v, props.pairs[1].k]
INFO [Apollo-Config-2] [TID: N/A] c.t.n.e.t.MyListener: Loaded Map: [k1]
额外的细节和日志
- 版本:Apollo客户端 2.2.0,Apollo服务端 2.0.1,SpringBoot 3.5.7,SpringCloud 4.3.0,JDK 21.0.7
- 错误日志
- 配置:
- 平台和操作系统
描述bug
使用RefreshScope刷新被@ConfigurationProperties注解的配置类
在调用RefreshScope.refreshAll()后访问被刷新的配置类,发现配置类的内容有一定概率为旧值
复现
修改配置时概率复现
期望
调用RefreshScope.refreshAll()后访问被刷新的配置类,应该获取到新的配置内容
截图
为了保证我的listener执行时相关的配置bean已经刷新,我参考ApolloConfigChangeListener实现了PostRefreshConfigChangeListener,所有PostRefreshConfigChangeListener在RefreshScope.refreshAll()执行完之后才会执行,ApolloConfigChangeListener负责执行RefreshScope.refreshAll()和触发listener,相关代码如下
配置变更
发布前配置
发布后配置
出现问题时的输出日志,可以看到ConfigChangeEvent中有k2的条目,但访问配置bean时只加载到了k1
额外的细节和日志