Apollo 热更新配置

矫情吗;* 2024-04-06 12:04 207阅读 0赞

文章目录

  • 前言
  • 一、Apollo热更新配置
      1. @Value的方式
      1. @RefreshScope
      1. 监听apollo配置更新事件
  • 二、源码解析
      1. RefreshScope#refresh
      1. RefreshEndpoint
      1. ContextRefresher
      1. 配置刷新
      1. 配置类注册

前言

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

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

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

诞生背景:

  1. 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址……
  2. 对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制……
  3. 在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。
  4. Apollo配置中心应运而生!

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


一、Apollo热更新配置

1. @Value的方式

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

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

2. @RefreshScope

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

  1. @Configuration
  2. @EnableConfigurationProperties(ConfigProperties.class)
  3. @ConfigurationProperties(prefix = "config")
  4. @Setter
  5. @Getter
  6. @RefreshScope
  7. public class ConfigProperties {
  8. private int configValue;
  9. }

3. 监听apollo配置更新事件

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

  1. @Slf4j
  2. @Component
  3. public class SpringBootApolloRefreshConfig {
  4. private final ConfigProperties configProperties;
  5. private final RefreshScope refreshScope;
  6. public SpringBootApolloRefreshConfig(ConfigProperties configProperties, RefreshScope refreshScope) {
  7. this. configProperties = configProperties;
  8. this.refreshScope = refreshScope;
  9. }
  10. @ApolloConfigChangeListener(value = {
  11. "namespace"}, interestedKeyPrefixes = {
  12. "key_prefix"}, interestedKeys = {
  13. "key_prefix.key"})
  14. public void onChange(ConfigChangeEvent changeEvent) {
  15. log.info("before refresh {}", configProperties.toString());
  16. refreshScope.refresh("configProperties");
  17. log.info("after refresh {}", configProperties.toString());
  18. }
  19. }
  • 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,

    1. ApplicationListener<ContextRefreshedEvent>, Ordered {
    2. ...
    3. @ManagedOperation(description = "Dispose of the current instance of bean name "
    4. + "provided and force a refresh on next method execution.")
    5. public boolean refresh(String name) {
    6. if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
    7. // User wants to refresh the bean with this name but that isn't the one in the
    8. // cache...
    9. name = SCOPED_TARGET_PREFIX + name;
    10. }
    11. // Ensure lifecycle is finished if bean was disposable
    12. if (super.destroy(name)) {
    13. // 发布配置刷新事件
    14. this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
    15. return true;
    16. }
    17. return false;
    18. }

    }

2. RefreshEndpoint

  1. @Endpoint(id = "refresh")
  2. public class RefreshEndpoint {
  3. private ContextRefresher contextRefresher;
  4. public RefreshEndpoint(ContextRefresher contextRefresher) {
  5. this.contextRefresher = contextRefresher;
  6. }
  7. @WriteOperation
  8. public Collection<String> refresh() {
  9. // 触发刷新环境变量
  10. Set<String> keys = this.contextRefresher.refresh();
  11. return keys;
  12. }
  13. }

3. ContextRefresher

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

  • #refresh 触发入口

    public class ContextRefresher {

  1. ...
  2. private ConfigurableApplicationContext context;
  3. private RefreshScope scope;
  4. public ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
  5. this.context = context;
  6. this.scope = scope;
  7. }
  8. protected ConfigurableApplicationContext getContext() {
  9. return this.context;
  10. }
  11. protected RefreshScope getScope() {
  12. return this.scope;
  13. }
  14. // 刷新环境变量
  15. public synchronized Set<String> refresh() {
  16. Set<String> keys = refreshEnvironment();
  17. this.scope.refreshAll();
  18. return keys;
  19. }
  20. public synchronized Set<String> refreshEnvironment() {
  21. // 获取旧的环境变量配置
  22. Map<String, Object> before = extract(
  23. this.context.getEnvironment().getPropertySources());
  24. // 更新最新环境变量
  25. addConfigFilesToEnvironment();
  26. // 获取变更的变量key
  27. Set<String> keys = changes(before,
  28. extract(this.context.getEnvironment().getPropertySources())).keySet();
  29. // 发布环境变量变更事件
  30. this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  31. return keys;
  32. }
  33. }

4. 配置刷新

  • #onApplicationEvent 方法监听环境变更事件
  • #rebind 方法遍历所有加 RefreshScope 注解的配置类,刷新配置

    @Component
    @ManagedResource
    public class ConfigurationPropertiesRebinder

    1. implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
  1. private ConfigurationPropertiesBeans beans;
  2. private ApplicationContext applicationContext;
  3. private Map<String, Exception> errors = new ConcurrentHashMap<>();
  4. /**
  5. * beans中包含所有加上RefreshScope注解的配置类实例
  6. */
  7. public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
  8. this.beans = beans;
  9. }
  10. ...
  11. /**
  12. * 遍历每一个加了RefreshScope注解的配置类,刷新配置
  13. */
  14. @ManagedOperation
  15. public void rebind() {
  16. this.errors.clear();
  17. for (String name : this.beans.getBeanNames()) {
  18. rebind(name);
  19. }
  20. }
  21. @ManagedOperation
  22. public boolean rebind(String name) {
  23. if (!this.beans.getBeanNames().contains(name)) {
  24. return false;
  25. }
  26. if (this.applicationContext != null) {
  27. try {
  28. Object bean = this.applicationContext.getBean(name);
  29. if (AopUtils.isAopProxy(bean)) {
  30. bean = ProxyUtils.getTargetObject(bean);
  31. }
  32. if (bean != null) {
  33. // TODO: determine a more general approach to fix this.
  34. // see https://github.com/spring-cloud/spring-cloud-commons/issues/571
  35. if (getNeverRefreshable().contains(bean.getClass().getName())) {
  36. return false; // ignore
  37. }
  38. // 销毁旧的配置类实例
  39. this.applicationContext.getAutowireCapableBeanFactory()
  40. .destroyBean(bean);
  41. // 根据新的环境变量,重新初始化配置类
  42. this.applicationContext.getAutowireCapableBeanFactory()
  43. .initializeBean(bean, name);
  44. return true;
  45. }
  46. }
  47. catch (RuntimeException e) {
  48. this.errors.put(name, e);
  49. throw e;
  50. }
  51. catch (Exception e) {
  52. this.errors.put(name, e);
  53. throw new IllegalStateException("Cannot rebind to " + name, e);
  54. }
  55. }
  56. return false;
  57. }
  58. @ManagedAttribute
  59. public Set<String> getNeverRefreshable() {
  60. String neverRefresh = this.applicationContext.getEnvironment().getProperty(
  61. "spring.cloud.refresh.never-refreshable",
  62. "com.zaxxer.hikari.HikariDataSource");
  63. return StringUtils.commaDelimitedListToSet(neverRefresh);
  64. }
  65. @ManagedAttribute
  66. public Set<String> getBeanNames() {
  67. return new HashSet<>(this.beans.getBeanNames());
  68. }
  69. /**
  70. * 监听环境变量变更事件,重新绑定刷新配置
  71. */
  72. @Override
  73. public void onApplicationEvent(EnvironmentChangeEvent event) {
  74. if (this.applicationContext.equals(event.getSource())
  75. // Backwards compatible
  76. || event.getKeys().equals(event.getSource())) {
  77. rebind();
  78. }
  79. }
  80. }

5. 配置类注册

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

    @Component
    public class ConfigurationPropertiesBeans

    1. implements BeanPostProcessor, ApplicationContextAware {
  1. private Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();
  2. private ApplicationContext applicationContext;
  3. private ConfigurableListableBeanFactory beanFactory;
  4. private String refreshScope;
  5. private boolean refreshScopeInitialized;
  6. private ConfigurationPropertiesBeans parent;
  7. ...
  8. /**
  9. * 在Bean初始化之前,会调用这里,用于注册配置类对象
  10. */
  11. @Override
  12. public Object postProcessBeforeInitialization(Object bean, String beanName)
  13. throws BeansException {
  14. // 判断Bean是否加上了RefreshScope注解
  15. if (isRefreshScoped(beanName)) {
  16. return bean;
  17. }
  18. // 判断Bean是否加上了ConfigurationProperties注解
  19. ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
  20. .get(this.applicationContext, bean, beanName);
  21. // 将加上RefreshScope注解的配置类对象放在map中,用于刷新配置
  22. if (propertiesBean != null) {
  23. this.beans.put(beanName, propertiesBean);
  24. }
  25. return bean;
  26. }
  27. private boolean isRefreshScoped(String beanName) {
  28. if (this.refreshScope == null && !this.refreshScopeInitialized) {
  29. this.refreshScopeInitialized = true;
  30. for (String scope : this.beanFactory.getRegisteredScopeNames()) {
  31. if (this.beanFactory.getRegisteredScope(
  32. scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) {
  33. this.refreshScope = scope;
  34. break;
  35. }
  36. }
  37. }
  38. if (beanName == null || this.refreshScope == null) {
  39. return false;
  40. }
  41. return this.beanFactory.containsBeanDefinition(beanName) && this.refreshScope
  42. .equals(this.beanFactory.getBeanDefinition(beanName).getScope());
  43. }
  44. @Override
  45. public Object postProcessAfterInitialization(Object bean, String beanName)
  46. throws BeansException {
  47. return bean;
  48. }
  49. public Set<String> getBeanNames() {
  50. return new HashSet<String>(this.beans.keySet());
  51. }
  52. }

发表评论

表情:
评论列表 (有 0 条评论,207人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Apollo配置中心使用及更新

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

    相关 更新

    虽然有插件开发,但热更新少不了。 ![Center][] 当我们需要更新插件的时候使用的是插件开发,当我们需要更新宿主程序就需要使用热更新 热更新使用框架bsdif