Skip to content

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

Description

@JunkCoder17

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions