#5636·apollo

RefreshScope刷新@ConfigurationProperties配置bean后访问配置bean有概率读取到旧值

Author: JunkCoder17Created Jun 25, 2026Updated Jul 4, 2026
Labelsbugneed investigationarea/client

描述bug

使用RefreshScope刷新被@ConfigurationProperties注解的配置类 在调用RefreshScope.refreshAll()后访问被刷新的配置类,发现配置类的内容有一定概率为旧值

复现

修改配置时概率复现

期望

调用RefreshScope.refreshAll()后访问被刷新的配置类,应该获取到新的配置内容

截图 为了保证我的listener执行时相关的配置bean已经刷新,我参考ApolloConfigChangeListener实现了PostRefreshConfigChangeListener,所有PostRefreshConfigChangeListener在RefreshScope.refreshAll()执行完之后才会执行,ApolloConfigChangeListener负责执行RefreshScope.refreshAll()和触发listener,相关代码如下

java
@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);
        }
    }
}
java
@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;
    }
}
java
@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());
    }
}

配置变更 发布前配置

yaml
props:
  pairs:
    - k: k1
      v: v1

发布后配置

yaml
props:
  pairs:
    - k: k1
      v: v1
    - k: k2
      v: v2

出现问题时的输出日志,可以看到ConfigChangeEvent中有k2的条目,但访问配置bean时只加载到了k1

log
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
  • 错误日志
  • 配置:
  • 平台和操作系统