前言

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

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

  • application (应用)
  • environment (环境)
  • cluster (集群)
  • namespace (命名空间) 同时,Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo

诞生背景:

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

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

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

Apollo配置中心应运而生!

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


一、Apollo热更新配置

Apollo 热更新工作原理

  1. 客户端与服务端的通信:Apollo 客户端会注册到 Apollo 配置中心服务端,并监听配置更新。
  2. 长轮询:Apollo 客户端通过长轮询的方式定期检查配置是否有更新。当配置中心的配置更新时,服务端会通知所有相关的客户端。
  3. 配置更新:一旦接收到更新通知,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());
   }

}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐