apollo icon indicating copy to clipboard operation
apollo copied to clipboard

Spring 中 ConfigurationProperties 配置热更新问题

Open toshawn opened this issue 1 year ago • 5 comments

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

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

toshawn avatar Mar 22 '24 05:03 toshawn

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.

dosubot[bot] avatar Mar 22 '24 05:03 dosubot[bot]

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;
    }
}

toshawn avatar Mar 22 '24 06:03 toshawn

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] avatar Apr 21 '24 19:04 stale[bot]

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

ListenQ avatar Apr 22 '24 07:04 ListenQ

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 注解

toshawn avatar Apr 23 '24 03:04 toshawn

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

lingmeme avatar May 22 '24 06:05 lingmeme

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

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

toshawn avatar May 22 '24 14:05 toshawn

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] avatar Jun 22 '24 11:06 stale[bot]

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.

stale[bot] avatar Jun 29 '24 12:06 stale[bot]