Apollo 热更新配置


前言

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

Apollo支持4个维度管理Key-Value格式的配置:

诞生背景:

随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址……

对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制……

在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。

Apollo配置中心应运而生!

简单来说,Apollo 是一个配置中心,可以非常方便管理公司所有项目配置,并支持热更新。本文主要讲解 Apollo 在 Spring 环境下的热更新操作。


一、Apollo热更新配置

1. @Value的方式

这种方式不需要改任何代码,支持热更新

@Value(value = "${config_key}")
private int configValue;

2. @RefreshScope

定义配置类,配置类中加上 @RefreshScope 注解,加上改注解的 Bean 能在运行时被实时刷新,即销毁然后重新创建,所有引用到该 Bean 的地方在下次访问时都会指向新的对象

@Configuration
@EnableConfigurationProperties(ConfigProperties.class)
@ConfigurationProperties(prefix = "config")
@Setter
@Getter
@RefreshScope
public class ConfigProperties {
    private int configValue;
}

3. 监听apollo配置更新事件

在方法上加 @ApolloConfigChangeListener 注解,监听 Apollo 配置变更事件。

@Slf4j
@Component
public class SpringBootApolloRefreshConfig {

    private final ConfigProperties configProperties;
    private final RefreshScope refreshScope;

    public SpringBootApolloRefreshConfig(ConfigProperties configProperties, RefreshScope refreshScope) {
        this. configProperties = configProperties;
        this.refreshScope = refreshScope;
    }

    @ApolloConfigChangeListener(value = {"namespace"}, interestedKeyPrefixes = {"key_prefix"}, interestedKeys = {"key_prefix.key"})
    public void onChange(ConfigChangeEvent changeEvent) {
        log.info("before refresh {}", configProperties.toString());
        refreshScope.refresh("configProperties");
        log.info("after refresh {}", configProperties.toString());
    }
}
  • ApolloConfigChangeListener 注解,value 中的值为感兴趣的 namespace,interestedKeyPrefixes 中的值为感兴趣的 key 前缀(不指定表示全部),interestedKeys 表示具体对哪些 key 感兴趣(不指定表示全部)。
  • 当检测到 Apollo 配置变更时,refreshScope.refresh("beanName") 刷新配置。

二、源码解析

1. RefreshScope#refresh

方法如下:

  • 调父类 GenericScope#destroy 清空缓存(对应的 BeanLifecycleWrapper 缓存)
  • 发布 RefreshScopeRefreshedEvent 事件,该事件会最终会触发/refresh actuator endpoint
@ManagedResource
public class RefreshScope extends GenericScope implements ApplicationContextAware,
      ApplicationListener<ContextRefreshedEvent>, Ordered {
    ...
    @ManagedOperation(description = "Dispose of the current instance of bean name "
          + "provided and force a refresh on next method execution.")
    public boolean refresh(String name) {
       if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
          // User wants to refresh the bean with this name but that isn't the one in the
          // cache...
          name = SCOPED_TARGET_PREFIX + name;
       }
       // Ensure lifecycle is finished if bean was disposable
       if (super.destroy(name)) {
          // 发布配置刷新事件
          this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
          return true;
       }
       return false;
    }
}

2. RefreshEndpoint

@Endpoint(id = "refresh")
public class RefreshEndpoint {

   private ContextRefresher contextRefresher;

   public RefreshEndpoint(ContextRefresher contextRefresher) {
      this.contextRefresher = contextRefresher;
   }

   @WriteOperation
   public Collection<String> refresh() {
      // 触发刷新环境变量
      Set<String> keys = this.contextRefresher.refresh();
      return keys;
   }

}

3. ContextRefresher

刷新环境变量&发布变更事件

  • #refresh 触发入口
public class ContextRefresher {

   ...
   
   private ConfigurableApplicationContext context;

   private RefreshScope scope;

   public ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
      this.context = context;
      this.scope = scope;
   }

   protected ConfigurableApplicationContext getContext() {
      return this.context;
   }

   protected RefreshScope getScope() {
      return this.scope;
   }

   // 刷新环境变量
   public synchronized Set<String> refresh() {
      Set<String> keys = refreshEnvironment();
      this.scope.refreshAll();
      return keys;
   }

   public synchronized Set<String> refreshEnvironment() {
      // 获取旧的环境变量配置
      Map<String, Object> before = extract(
            this.context.getEnvironment().getPropertySources());
      // 更新最新环境变量
      addConfigFilesToEnvironment();
      // 获取变更的变量key
      Set<String> keys = changes(before,
            extract(this.context.getEnvironment().getPropertySources())).keySet();
      // 发布环境变量变更事件
      this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
      return keys;
   }
}

4. 配置刷新

  • #onApplicationEvent 方法监听环境变更事件
  • #rebind 方法遍历所有加 RefreshScope 注解的配置类,刷新配置
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
      implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

   private ConfigurationPropertiesBeans beans;

   private ApplicationContext applicationContext;

   private Map<String, Exception> errors = new ConcurrentHashMap<>();

   /**
    * beans中包含所有加上RefreshScope注解的配置类实例
    */
   public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
      this.beans = beans;
   }

   ...

   /**
    * 遍历每一个加了RefreshScope注解的配置类,刷新配置
    */
   @ManagedOperation
   public void rebind() {
      this.errors.clear();
      for (String name : this.beans.getBeanNames()) {
         rebind(name);
      }
   }

   @ManagedOperation
   public boolean rebind(String name) {
      if (!this.beans.getBeanNames().contains(name)) {
         return false;
      }
      if (this.applicationContext != null) {
         try {
            Object bean = this.applicationContext.getBean(name);
            if (AopUtils.isAopProxy(bean)) {
               bean = ProxyUtils.getTargetObject(bean);
            }
            if (bean != null) {
               // TODO: determine a more general approach to fix this.
               // see https://github.com/spring-cloud/spring-cloud-commons/issues/571
               if (getNeverRefreshable().contains(bean.getClass().getName())) {
                  return false; // ignore
               }
               // 销毁旧的配置类实例
               this.applicationContext.getAutowireCapableBeanFactory()
                     .destroyBean(bean);
               // 根据新的环境变量,重新初始化配置类
               this.applicationContext.getAutowireCapableBeanFactory()
                     .initializeBean(bean, name);
               return true;
            }
         }
         catch (RuntimeException e) {
            this.errors.put(name, e);
            throw e;
         }
         catch (Exception e) {
            this.errors.put(name, e);
            throw new IllegalStateException("Cannot rebind to " + name, e);
         }
      }
      return false;
   }

   @ManagedAttribute
   public Set<String> getNeverRefreshable() {
      String neverRefresh = this.applicationContext.getEnvironment().getProperty(
            "spring.cloud.refresh.never-refreshable",
            "com.zaxxer.hikari.HikariDataSource");
      return StringUtils.commaDelimitedListToSet(neverRefresh);
   }

   @ManagedAttribute
   public Set<String> getBeanNames() {
      return new HashSet<>(this.beans.getBeanNames());
   }

   /**
    * 监听环境变量变更事件,重新绑定刷新配置
    */
   @Override
   public void onApplicationEvent(EnvironmentChangeEvent event) {
      if (this.applicationContext.equals(event.getSource())
            // Backwards compatible
            || event.getKeys().equals(event.getSource())) {
         rebind();
      }
   }

}

5. 配置类注册

  • 该类实现了BeanPostProcessor接口,在Bean初始化之前会进入postProcessBeforeInitialization方法
  • 加上RefreshScope注解的配置类会注册进beans map中,用于刷新配置
@Component
public class ConfigurationPropertiesBeans
      implements BeanPostProcessor, ApplicationContextAware {

   private Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();

   private ApplicationContext applicationContext;

   private ConfigurableListableBeanFactory beanFactory;

   private String refreshScope;

   private boolean refreshScopeInitialized;

   private ConfigurationPropertiesBeans parent;

   ...

   /**
    * 在Bean初始化之前,会调用这里,用于注册配置类对象
    */
   @Override
   public Object postProcessBeforeInitialization(Object bean, String beanName)
         throws BeansException {
      // 判断Bean是否加上了RefreshScope注解
      if (isRefreshScoped(beanName)) {
         return bean;
      }
      // 判断Bean是否加上了ConfigurationProperties注解
      ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
            .get(this.applicationContext, bean, beanName);
      // 将加上RefreshScope注解的配置类对象放在map中,用于刷新配置
      if (propertiesBean != null) {
         this.beans.put(beanName, propertiesBean);
      }
      return bean;
   }

   private boolean isRefreshScoped(String beanName) {
      if (this.refreshScope == null && !this.refreshScopeInitialized) {
         this.refreshScopeInitialized = true;
         for (String scope : this.beanFactory.getRegisteredScopeNames()) {
            if (this.beanFactory.getRegisteredScope(
                  scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) {
               this.refreshScope = scope;
               break;
            }
         }
      }
      if (beanName == null || this.refreshScope == null) {
         return false;
      }
      return this.beanFactory.containsBeanDefinition(beanName) && this.refreshScope
            .equals(this.beanFactory.getBeanDefinition(beanName).getScope());
   }

   @Override
   public Object postProcessAfterInitialization(Object bean, String beanName)
         throws BeansException {
      return bean;
   }

   public Set<String> getBeanNames() {
      return new HashSet<String>(this.beans.keySet());
   }

}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Apollo提供了热更新的功能,可以在不重启应用的情况下更新配置。在代码中,你需要使用Apollo提供的Java客户端来获取配置,并且使用注解@ApolloConfig和@ApolloJsonValue来实现自动更新。具体的步骤如下: 1. 添加依赖 在项目的pom.xml文件中添加如下依赖: ```xml <dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>1.6.1</version> </dependency> ``` 2. 配置Apollo 在应用启动时,需要配置Apollo客户端,示例代码如下: ```java //指定Apollo Meta Server地址 System.setProperty("apollo.meta", "http://config-service-url"); //指定应用ID System.setProperty("app.id", "your-app-id"); //创建Apollo客户端 Config config = ConfigService.getConfig(); ``` 3. 获取配置 使用Apollo客户端获取配置,示例代码如下: ```java @ApolloConfig private Config config; //获取配置 String value = config.getProperty("key", "default-value"); ``` 4. 实现自动更新 使用@ApolloJsonValue注解来自动更新配置,示例代码如下: ```java @ApolloConfig private Config config; //自动更新配置 @ApolloJsonValue("${key:default-value}") private String value; ``` 这样,当配置发生变化时,value的值会自动更新。 需要注意的是,@ApolloJsonValue注解只能用于String类型的字段,如果需要更新其他类型的配置,需要手动处理更新逻辑。 以上就是使用Apollo实现热更新的基本步骤,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值