apolloconfig / apollo

Apollo is a reliable configuration management system suitable for microservice configuration management scenarios.
https://www.apolloconfig.com
Apache License 2.0
28.94k stars 10.19k forks source link

Spring 中 ConfigurationProperties 配置热更新问题 #5108

Closed shawncny closed 3 days ago

shawncny commented 3 months ago

你的特性请求和某个问题有关吗?请描述 在使用 @ConfigurationProperties 配置类如果需要热更新的话,需要为每个配置类写一个 ApolloConfigChangeListener 来实现热更新,这样配置类多的时候处理起来很重复

清晰简洁地描述一下你希望的解决方案 能不能从 spring 容器内得到所有的 @ConfigurationProperties 配置 Bean ,然后解析得到注解里面的 prefix 属性,传递给 @ApolloConfigChangeListener 注解的 interestedKeyPrefixes 属性,这样就不需要使用者为每一个配置 Bean 去创建 changeListener

dosubot[bot] commented 3 months ago

Based on the information you provided and the context from the Apollo repository, it seems you're looking for a way to dynamically register @ApolloConfigChangeListener for each @ConfigurationProperties bean in your Spring application. This would allow you to avoid creating a separate change listener for each configuration bean.

There is an open issue in the Apollo repository that discusses a similar requirement: @ApolloConfigChangeListener interestedKeyPrefixes When can dynamic parameterization be supported. However, as of the last update on January 24, 2024, there is no direct solution provided in the discussion comments. A contributor, @huxleyliau, has shown interest in implementing dynamic parameterization for interestedKeyPrefixes and has been assigned the issue. Therefore, the solution might be in progress but not yet available.

In the meantime, you can use a workaround to achieve your goal. Here's a conceptual approach:

  1. Scan for @ConfigurationProperties Beans: Use Spring's ApplicationContext to find beans annotated with @ConfigurationProperties.

  2. Extract Prefixes: For each found bean, extract the prefix value from the @ConfigurationProperties annotation.

  3. Register Apollo Config Change Listener: Dynamically register an @ApolloConfigChangeListener for each unique prefix, ensuring that changes in the Apollo configuration for these prefixes trigger the appropriate update logic.

Here's a code snippet that demonstrates how to achieve this:

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Component
public class ApolloConfigChangeListenerRegistrar implements BeanPostProcessor, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private final Set<String> registeredPrefixes = new HashSet<>();

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

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        ConfigurationProperties annotation = applicationContext.findAnnotationOnBean(beanName, ConfigurationProperties.class);
        if (annotation != null) {
            String prefix = annotation.prefix();
            // Ensure that we only register a listener once per prefix
            if (!registeredPrefixes.contains(prefix)) {
                registerApolloConfigChangeListener(prefix);
                registeredPrefixes.add(prefix);
            }
        }
        return bean;
    }

    private void registerApolloConfigChangeListener(String prefix) {
        try {
            // Assuming there's a method to add listeners dynamically, otherwise, you'll need to adapt this part
            Method method = this.getClass().getMethod("onChange", String.class);
            ApolloConfigChangeListener listener = method.getAnnotation(ApolloConfigChangeListener.class);
            if (listener == null) {
                listener = new ApolloConfigChangeListener() {
                    @Override
                    public Class<? extends java.lang.annotation.Annotation> annotationType() {
                        return ApolloConfigChangeListener.class;
                    }

                    @Override
                    public String[] value() {
                        return new String[0]; // default value
                    }

                    @Override
                    public String[] interestedKeyPrefixes() {
                        return new String[]{prefix}; // set the interested prefix
                    }
                };
                // Here you should add the listener to Apollo client. This step depends on how your Apollo client is configured.
                // This is a placeholder to show where the listener should be registered.
                // addApolloConfigChangeListener(listener);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    // Placeholder for the method that would be called on configuration changes
    public void onChange(String changedKey) {
        // Logic to handle configuration change
    }
}

Please note that this code is a conceptual demonstration. The actual implementation of dynamically registering @ApolloConfigChangeListener with Apollo might differ based on the version of Apollo client you are using and how your application is structured. The key idea is to leverage Spring's lifecycle hooks (BeanPostProcessor and ApplicationContextAware) to scan for @ConfigurationProperties beans, extract their prefixes, and dynamically register them with Apollo for configuration changes.

For more information on how Apollo handles the registration of @ApolloConfigChangeListener annotations, you can refer to the Java SDK User Guide in the Apollo repository. It provides examples of how to use @ApolloConfigChangeListener to listen for configuration changes.

I hope this helps! If you have further questions or need more clarification, feel free to ask.

Sources

#### About Dosu This response is meant to be useful and save you time. It is **not meant to be a precise solution**, but rather a starting point for your own research. Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant. If you want to continue the conversation, start your reply with @dosu-bot.
shawncny commented 3 months ago
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.springframework.cloud.context.config.annotation.RefreshScope;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@RefreshScope
public @interface ApolloRefreshScope {

    /**
     * @see ApolloConfigChangeListener#value()
     */
    String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};

    /**
     * @see ApolloConfigChangeListener#interestedKeys()
     */
    String[] interestedKeys() default {};

    /**
     * @see ApolloConfigChangeListener#interestedKeyPrefixes()
     */
    String[] interestedKeyPrefixes() default {};
}
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;

@Component
@RequiredArgsConstructor
@Slf4j
public class ApolloRefreshScopeProcessor implements BeanPostProcessor {
    private static final String NAMESPACE_DELIMITER = ",";
    private static final Splitter NAMESPACE_SPLITTER = Splitter.on(NAMESPACE_DELIMITER)
            .omitEmptyStrings().trimResults();
    private final Environment environment;
    private final org.springframework.cloud.context.scope.refresh.RefreshScope refreshScope;

    @Override
    public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        ApolloRefreshScope annotation = AnnotationUtils
                .findAnnotation(beanClass, ApolloRefreshScope.class);
        if (annotation != null) {
            String[] namespaces = annotation.value();
            String[] annotatedInterestedKeys = annotation.interestedKeys();
            String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
            if (annotatedInterestedKeyPrefixes.length == 0) {
                annotatedInterestedKeyPrefixes = findKeyPrefixesByConfigurationProperties(beanClass);
            }
            Set<String> interestedKeys =
                    annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
            Set<String> interestedKeyPrefixes =
                    annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes)
                            : null;
            Set<String> resolvedNamespaces = processResolveNamespaceValue(namespaces);
            for (String namespace : resolvedNamespaces) {
                Config config = ConfigService.getConfig(namespace);
                if (interestedKeys == null && interestedKeyPrefixes == null) {
                    config.addChangeListener(createListener(bean, beanName, beanClass));
                } else {
                    config.addChangeListener(createListener(bean, beanName, beanClass), interestedKeys, interestedKeyPrefixes);
                }
            }
        }
        return bean;
    }

    private String[] findKeyPrefixesByConfigurationProperties(Class<?> beanClass) {
        ConfigurationProperties annotation = AnnotationUtils
                .findAnnotation(beanClass, ConfigurationProperties.class);
        if (annotation != null) {
            return new String[]{annotation.prefix()};
        }
        return new String[0];
    }

    private ConfigChangeListener createListener(final Object bean, final String beanName, final Class<?> beanClass) {
        return changeEvent -> {
            if (bean instanceof ConfigChangeListener) {
                ((ConfigChangeListener) bean).onChange(changeEvent);
            }
            refreshScope.refresh(beanName);
        };
    }

    private Set<String> processResolveNamespaceValue(String[] namespaces) {

        Set<String> resolvedNamespaces = new HashSet<>();

        for (String namespace : namespaces) {
            final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace);

            if (resolvedNamespace.contains(NAMESPACE_DELIMITER)) {
                resolvedNamespaces.addAll(NAMESPACE_SPLITTER.splitToList(resolvedNamespace));
            } else {
                resolvedNamespaces.add(resolvedNamespace);
            }
        }

        return resolvedNamespaces;
    }
}
stale[bot] commented 2 months ago

This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions.

ListenQ commented 2 months ago

This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions.

使用applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); 和 refreshScope 失效不起作用

@Configuration
@ConfigurationProperties("sms.outbound")
@EnableApolloConfig
@Data
public class OutConfig {
  private List<Map<String,String>> auths;
 }

pom.xml是

    <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
            <version>3.1.5</version>
            <scope>compile</scope>
        </dependency>

changeListener是

ConfigService.getConfig("out_config").addChangeListener(new ConfigChangeListener(){
    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
           applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
         }
});
shawncny commented 2 months ago

This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions.

使用applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); 和 refreshScope 失效不起作用

@Configuration
@ConfigurationProperties("sms.outbound")
@EnableApolloConfig
@Data
public class OutConfig {
  private List<Map<String,String>> auths;
 }

pom.xml是

  <dependency>
          <groupId>com.ctrip.framework.apollo</groupId>
          <artifactId>apollo-client</artifactId>
          <version>2.0.0</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-context</artifactId>
          <version>3.1.5</version>
          <scope>compile</scope>
      </dependency>

changeListener是

ConfigService.getConfig("out_config").addChangeListener(new ConfigChangeListener(){
  @Override
  public void onChange(ConfigChangeEvent changeEvent) {
           applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
         }
});

需要添加 @RefreshScope 注解

ling0900 commented 1 month ago

能否提供demo代码,我想本地看看。 @shawncny

shawncny commented 1 month ago

能否提供demo代码,我想本地看看。 @shawncny

就两个java文件,如果你要的话我发你

stale[bot] commented 1 week ago

This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions.

stale[bot] commented 3 days ago

This issue has been automatically closed because it has not had activity in the last 7 days. If this issue is still valid, please ping a maintainer and ask them to label it as "help wanted". Thank you for your contributions.