Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
shawncny opened this issue Mar 22, 2024 · 7 comments
Open

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

shawncny opened this issue Mar 22, 2024 · 7 comments

Comments

@shawncny
Copy link

shawncny commented Mar 22, 2024

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

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

Copy link

dosubot bot commented Mar 22, 2024

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
Copy link
Author

shawncny commented Mar 22, 2024

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

Copy link

stale bot commented Apr 21, 2024

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 stale bot added the stale label Apr 21, 2024
@ListenQ
Copy link

ListenQ commented Apr 22, 2024

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

@stale stale bot removed the stale label Apr 22, 2024
@shawncny
Copy link
Author

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
Copy link

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

@shawncny
Copy link
Author

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants