spring 配置动态刷新 包括(apollp 和nacos)

Apollo

分布式 apollo 简介

Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。

本文主要介绍如何使用 apollo 与 springboot 实现动态刷新配置,如果之前不了解 apollo 可以查看如下文档

https://github.com/ctripcorp/apollo

学习了解一下 apollo,再来查看本文

apollo 与 spring 实现动态刷新配置本文主要演示 2 种刷新,一种基于普通字段刷新、一种基于 bean 上使用了 @ConfigurationProperties 刷新

1、普通字段刷新(@Value)

== 不需要额外配置,直接就可以动态刷新 ==

a、pom.xml 配置

        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <version>1.6.0</version>
        </dependency>


b、客户端配置 AppId,Apollo Meta Server

此配置有多种方法,本示例直接在 application.yml 配置,配置内容如下

app:
  id: ${spring.application.name}
apollo:
  meta: http://192.168.88.128:8080,http://192.168.88.129:8080
  bootstrap:
    enabled: true
    eagerLoad:
      enabled: true


c、项目中启动类上加上 @EnableApolloConfig 注解,形如下

@SpringBootApplication
@EnableApolloConfig(value = {"application","user.properties","product.properties","order.properties"})
public class ApolloApplication {

    public static void main(String[] args) {

        SpringApplication.run(ApolloApplication.class, args);
    }

}


@EnableApolloConfig 不一定要加在启动类上,加在被 spring 管理的类上即可

d、在需刷新的字段上配置 @Value 注解,形如

    @Value("${hello}")
    private String hello;

通过以上三步就可以实现普通字段的动态刷新

2、bean 使用 @ConfigurationProperties 动态刷新

bean 使用 @ConfigurationProperties 注解目前还不支持自动刷新,得编写一定的代码实现刷新。目前官方提供 2 种刷新方案

  • 基于 RefreshScope 实现刷新
  • 基于 EnvironmentChangeEvent 实现刷新
  • 本文再提供一种,当 bean 上如果使用了 @ConditionalOnProperty 如何实现刷新

a、基于 RefreshScope 实现刷新

1、pom.xml 要额外引入

    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>



2、bean 上使用 @RefreshScope 注解

@Component
@ConfigurationProperties(prefix = "product")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@RefreshScope
public class Product {

    private Long id;

    private String productName;

    private BigDecimal price;

}


3、利用 RefreshScope 搭配 @ApolloConfigChangeListener 监听实现 bean 的动态刷新,其代码实现如下

 @ApolloConfigChangeListener(value="product.properties")
    private void refresh(ConfigChangeEvent changeEvent){

        refreshScope.refresh("product");

        PrintChangeKeyUtils.printChange(changeEvent);
    }


@ApolloConfigChangeListener 注解

参数​​​​作用​​​​类型​​​​默认值​​​​示例​​
value / configNames指定要监听的 Apollo 命名空间(namespace)列表String[]空数组(监听 application)@ApolloConfigChangeListener({“app1”, “app2”})
interestedKeyPrefixes仅监听以指定前缀开头的配置项变更(过滤无关配置)String[]空数组(监听所有)@ApolloConfigChangeListener(interestedKeyPrefixes = “spring.redis”)
interestedKeys明确指定要监听的配置项 Key(完全匹配)String[]空数组(监听所有)@ApolloConfigChangeListener(interestedKeys = {“timeout”, “max.retries”})
ignoreNamespaceNotFound是否忽略未找到命名空间的错误(避免启动失败)booleanfalse(严格模式)@ApolloConfigChangeListener(value = “optional-ns”, ignoreNamespaceNotFound = true)

b、基于 EnvironmentChangeEvent 实现刷新

利用 spring 的事件驱动配合 @ApolloConfigChangeListener 监听实现 bean 的动态刷新,其代码如下

@Component
@Slf4j
public class UserPropertiesRefresh implements ApplicationContextAware {

    private ApplicationContext applicationContext;



    @ApolloConfigChangeListener(value="user.properties",interestedKeyPrefixes = {"user."})
    private void refresh(ConfigChangeEvent changeEvent){
        applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));

        PrintChangeKeyUtils.printChange(changeEvent);
    }

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




}



c、当 bean 上有 @ConditionalOnProperty 如何实现刷新

当 bean 上有 @ConditionalOnProperty 注解时,上述的两种方案可以说失效了,因为 @ConditionalOnProperty 是一个条件注解,当不满足条件注解时,bean 是没法注册到 spring 容器中的。如果我们要实现此种情况的下的动态刷新,我们就得自己手动注册或者销毁 bean 了。其实现流程如下

1、当满足条件注解时,则手动创建 bean,然后配合 @ApolloConfigChangeListener 监听该 bean 的属性变化。当该 bean 属性有变化时,手动把属性注入 bean。同时刷新依赖该 bean 的其他 bean

2、当不满足条件注解时,则手动从 spring 容器中移除 bean,同时刷新依赖该 bean 的其他 bean

其刷新核心代码如下

public class OrderPropertiesRefresh implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @ApolloConfig(value = "order.properties")
    private Config config;


    @ApolloConfigChangeListener(value="order.properties",interestedKeyPrefixes = {"order."},interestedKeys = {"model.isShowOrder"})
    private void refresh(ConfigChangeEvent changeEvent){
        for (String basePackage : listBasePackages()) {
            Set<Class> conditionalClasses = ClassScannerUtils.scan(basePackage, ConditionalOnProperty.class);
            if(!CollectionUtils.isEmpty(conditionalClasses)){
                for (Class conditionalClass : conditionalClasses) {
                    ConditionalOnProperty conditionalOnProperty = (ConditionalOnProperty) conditionalClass.getAnnotation(ConditionalOnProperty.class);
                    String[] conditionalOnPropertyKeys = conditionalOnProperty.name();
                    String beanChangeCondition = this.getChangeKey(changeEvent,conditionalOnPropertyKeys);
                    String conditionalOnPropertyValue = conditionalOnProperty.havingValue();
                    boolean isChangeBean = this.changeBean(conditionalClass, beanChangeCondition, conditionalOnPropertyValue);
                    if(!isChangeBean){
                        // 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
                        applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
                    }
                }
            }
        }


        PrintChangeKeyUtils.printChange(changeEvent);
        printAllBeans();
    }


    /**
     * 根据条件对bean进行注册或者移除
     * @param conditionalClass
     * @param beanChangeCondition bean发生改变的条件
     * @param conditionalOnPropertyValue
     */
    private boolean changeBean(Class conditionalClass, String beanChangeCondition, String conditionalOnPropertyValue) {
        boolean isNeedRegisterBeanIfKeyChange = this.isNeedRegisterBeanIfKeyChange(beanChangeCondition,conditionalOnPropertyValue);
        boolean isNeedRemoveBeanIfKeyChange = this.isNeedRemoveBeanIfKeyChange(beanChangeCondition,conditionalOnPropertyValue);
        String beanName = StringUtils.uncapitalize(conditionalClass.getSimpleName());
        if(isNeedRegisterBeanIfKeyChange){
            boolean isAlreadyRegisterBean = this.isExistBean(beanName);
            if(!isAlreadyRegisterBean){
                this.registerBean(beanName,conditionalClass);
                return true;
            }
        }else if(isNeedRemoveBeanIfKeyChange){
            this.unregisterBean(beanName);
            return true;
        }
        return false;
    }

    /**
     * bean注册
     * @param beanName
     * @param beanClass
     */
    public void registerBean(String beanName,Class beanClass) {
        log.info("registerBean->beanName:{},beanClass:{}",beanName,beanClass);
        BeanDefinitionBuilder beanDefinitionBurinilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
        BeanDefinition beanDefinition = beanDefinitionBurinilder.getBeanDefinition();
        setBeanField(beanClass, beanDefinition);
        getBeanDefinitionRegistry().registerBeanDefinition(beanName,beanDefinition);

    }

    /**
     * 设置bean字段值
     * @param beanClass
     * @param beanDefinition
     */
    private void setBeanField(Class beanClass, BeanDefinition beanDefinition) {
        ConfigurationProperties configurationProperties = (ConfigurationProperties) beanClass.getAnnotation(ConfigurationProperties.class);
        if(ObjectUtils.isNotEmpty(configurationProperties)){
            String prefix = configurationProperties.prefix();
            for (String propertyName : config.getPropertyNames()) {
                String fieldPrefix = prefix + ".";
                if(propertyName.startsWith(fieldPrefix)){
                    String fieldName = propertyName.substring(fieldPrefix.length());
                    String fieldVal = config.getProperty(propertyName,null);
                    log.info("setBeanField-->fieldName:{},fieldVal:{}",fieldName,fieldVal);
                    beanDefinition.getPropertyValues().add(fieldName,fieldVal);
                }
            }
        }
    }



    /**
     * bean移除
     * @param beanName
     */
    public void unregisterBean(String beanName){
        log.info("unregisterBean->beanName:{}",beanName);
        getBeanDefinitionRegistry().removeBeanDefinition(beanName);
    }


    public  <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    public  <T> T getBean(Class<T> clz) {
        return (T) applicationContext.getBean(clz);
    }

    public boolean isExistBean(String beanName){
        return applicationContext.containsBean(beanName);
    }

    public boolean isExistBean(Class clz){
        try {
            Object bean = applicationContext.getBean(clz);
            return true;
        } catch (BeansException e) {
            // log.error(e.getMessage(),e);
        }
        return false;
    }

    private boolean isNeedRegisterBeanIfKeyChange(String changeKey,String conditionalOnPropertyValue){
        if(StringUtils.isEmpty(changeKey)){
            return false;
        }
        String apolloConfigValue = config.getProperty(changeKey,null);
        return conditionalOnPropertyValue.equals(apolloConfigValue);
    }

    private boolean isNeedRemoveBeanIfKeyChange(String changeKey,String conditionalOnPropertyValue){
        if(!StringUtils.isEmpty(changeKey)){
            String apolloConfigValue = config.getProperty(changeKey,null);
            return !conditionalOnPropertyValue.equals(apolloConfigValue);
        }

        return false;

    }

    private boolean isChangeKey(ConfigChangeEvent changeEvent,String conditionalOnPropertyKey){
        Set<String> changeKeys = changeEvent.changedKeys();
        if(!CollectionUtils.isEmpty(changeKeys) && changeKeys.contains(conditionalOnPropertyKey)){
            return true;
        }
        return false;
    }

    private String getChangeKey(ConfigChangeEvent changeEvent, String[] conditionalOnPropertyKeys){
        if(ArrayUtils.isEmpty(conditionalOnPropertyKeys)){
            return null;
        }
        String changeKey = null;
        for (String conditionalOnPropertyKey : conditionalOnPropertyKeys) {
            if(isChangeKey(changeEvent,conditionalOnPropertyKey)){
                changeKey = conditionalOnPropertyKey;
                break;
            }
        }

        return changeKey;
    }

    private BeanDefinitionRegistry getBeanDefinitionRegistry(){
        ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
        BeanDefinitionRegistry beanDefinitionRegistry = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
        return beanDefinitionRegistry;
    }

    private List<String> listBasePackages(){
        ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
        return AutoConfigurationPackages.get(configurableContext.getBeanFactory());
    }


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

    public  void printAllBeans() {
        String[] beans = applicationContext.getBeanDefinitionNames();
        Arrays.sort(beans);
        for (String beanName : beans) {
            Class<?> beanType = applicationContext.getType(beanName);
            System.out.println(beanType);
        }
    }

}


如果条件注解的值也是配置在 apollo 上,可能会出现依赖条件注解的 bean 的其他 bean,在项目拉取 apollo 配置时,就已经注入 spring 容器中,此时就算条件注解满足条件,则引用该条件注解 bean 的其他 bean,也会拿不到条件注解 bean。此时有 2 种方法解决,一种是在依赖条件注解 bean 的其他 bean 注入之前,先手动注册条件注解 bean 到 spring 容器中,其核心代码如下

@Component
@Slf4j
public class RefreshBeanFactory implements BeanFactoryPostProcessor {



    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        Config config = ConfigService.getConfig("order.properties");
        List<String> basePackages = AutoConfigurationPackages.get(configurableListableBeanFactory);
        for (String basePackage : basePackages) {
            Set<Class> conditionalClasses = ClassScannerUtils.scan(basePackage, ConditionalOnProperty.class);
            if(!CollectionUtils.isEmpty(conditionalClasses)){
                for (Class conditionalClass : conditionalClasses) {
                    ConditionalOnProperty conditionalOnProperty = (ConditionalOnProperty) conditionalClass.getAnnotation(ConditionalOnProperty.class);
                    String[] conditionalOnPropertyKeys = conditionalOnProperty.name();
                    String beanConditionKey = this.getConditionalOnPropertyKey(config,conditionalOnPropertyKeys);
                    String conditionalOnPropertyValue = conditionalOnProperty.havingValue();
                    this.registerBeanIfMatchCondition((DefaultListableBeanFactory)configurableListableBeanFactory,config,conditionalClass,beanConditionKey,conditionalOnPropertyValue);
                }
            }
        }


    }

    private void registerBeanIfMatchCondition(DefaultListableBeanFactory beanFactory,Config config,Class conditionalClass, String beanConditionKey, String conditionalOnPropertyValue) {
        boolean isNeedRegisterBean = this.isNeedRegisterBean(config,beanConditionKey,conditionalOnPropertyValue);
        String beanName = StringUtils.uncapitalize(conditionalClass.getSimpleName());
        if(isNeedRegisterBean){
                this.registerBean(config,beanFactory,beanName,conditionalClass);

        }

    }

    public void registerBean(Config config,DefaultListableBeanFactory beanFactory, String beanName, Class beanClass) {
        log.info("registerBean->beanName:{},beanClass:{}",beanName,beanClass);
        BeanDefinitionBuilder beanDefinitionBurinilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
        BeanDefinition beanDefinition = beanDefinitionBurinilder.getBeanDefinition();
        setBeanField(config,beanClass, beanDefinition);
        beanFactory.registerBeanDefinition(beanName,beanDefinition);


    }

    private void setBeanField(Config config,Class beanClass, BeanDefinition beanDefinition) {
        ConfigurationProperties configurationProperties = (ConfigurationProperties) beanClass.getAnnotation(ConfigurationProperties.class);
        if(ObjectUtils.isNotEmpty(configurationProperties)){
            String prefix = configurationProperties.prefix();
            for (String propertyName : config.getPropertyNames()) {
                String fieldPrefix = prefix + ".";
                if(propertyName.startsWith(fieldPrefix)){
                    String fieldName = propertyName.substring(fieldPrefix.length());
                    String fieldVal = config.getProperty(propertyName,null);
                    log.info("setBeanField-->fieldName:{},fieldVal:{}",fieldName,fieldVal);
                    beanDefinition.getPropertyValues().add(fieldName,fieldVal);
                }
            }
        }
    }

    public boolean isNeedRegisterBean(Config config,String beanConditionKey,String conditionalOnPropertyValue){
        if(StringUtils.isEmpty(beanConditionKey)){
            return false;
        }
        String apolloConfigValue = config.getProperty(beanConditionKey,null);
        return conditionalOnPropertyValue.equals(apolloConfigValue);
    }


    private String getConditionalOnPropertyKey(Config config, String[] conditionalOnPropertyKeys){
        if(ArrayUtils.isEmpty(conditionalOnPropertyKeys)){
            return null;
        }
        String changeKey = null;
        for (String conditionalOnPropertyKey : conditionalOnPropertyKeys) {
            if(isConditionalOnPropertyKey(config,conditionalOnPropertyKey)){
                changeKey = conditionalOnPropertyKey;
                break;
            }
        }

        return changeKey;
    }

    private boolean isConditionalOnPropertyKey(Config config,String conditionalOnPropertyKey){
        Set<String> propertyNames = config.getPropertyNames();
        if(!CollectionUtils.isEmpty(propertyNames) && propertyNames.contains(conditionalOnPropertyKey)){
            return true;
        }
        return false;
    }



}



其次利用懒加载的思想,在使用条件注解 bean 时,使用形如下方法

Order order = (Order) SpringContextUtils.getBean("order");


为什么

@Value 自动热刷新的原理:

  • Apollo 客户端内部使用了自己的机制来监听配置变化
  • 当配置变化时,Apollo 会使用 Spring 的 PlaceholderHelper 来自动更新带有 @Value 注解的字段

这是 Apollo 客户端内置的功能,所以不需要额外的代码

@ConfigurationProperties 不会自动热刷新的原因:

  • @ConfigurationProperties 的工作方式不同,它在应用启动时将配置绑定到 bean 对象的属性上
  • 默认情况下,Spring 只在 bean 初始化时进行一次绑定,之后不会自动重新绑定
  • 没有内置机制来监听这些属性的变化并更新已创建的 bean

因此需要添加额外代码来触发更新,主要有两种方式:

  1. EnvironmentChangeEvent 方式:
javaapplicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
  • 这会触发 Spring 的 ConfigurationPropertiesRebinder 重新绑定所有@ConfigurationProperties beans
  • Spring 监听此事件并执行重新绑定操作
  1. RefreshScope 方式:
javarefreshScope.refresh("product");

这会销毁标有 @RefreshScope 的特定 bean,并在下次访问时重新创建

总结:

@Value 的热更新是 Apollo 客户端内置实现的
@ConfigurationProperties 的热更新需要借助 Spring 的事件机制或 RefreshScope 机制显式触发
两者底层更新原理确实不同,这就是为什么 @ConfigurationProperties 需要额外代码而 @Value 不需要的原因

总结

本文主要介绍了常用的动态刷新,但本文的代码示例实现的功能不局限于此,本文的代码还实现如何通过自定义注解与 apollo 整合来实现一些业务操作,同时也实现了基于 hystrix 注解与 apollo 整合,实现基于线程隔离的动态熔断,感兴趣的朋友可以复制文末链接到浏览器,进行查看

apollo 基本上是能满足我们日常的业务开发要求,但是对于一些需求,比如动态刷新线上数据库资源啥,我们还是得做一定的量的改造,好在携程也提供了 apollo-use-cases,在里面可以找到常用的使用场景以及示例代码,其链接如下

https://github.com/ctripcorp/apollo-use-cases

感兴趣的朋友,可以查看下。

demo 链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-apollo

nacos

nacos 设计的时候就满足了。无论是 @Value 还是 @ConfigurationProperties

参考

apollo与springboot集成实现动态刷新配置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值