Apollo客户端初始化和热更新源码分析

1.Apollo 客户端初始化

由于笔者公司对配置的管理统一使用的都是Apollo,之前没怎么接触过,导致在实际使用中碰到多个小问题,所以抽空照着源码对客户端的初始化加载和热更新的代码走了一遍。以下是梳理的过程,并附了一个遇到问题和原因。

配置加载时序图

初始化入口

老规矩,springboot项目的源码一般都从spring.factories开始:

进ApolloAutoConfiguration:

可以看到主要注入了一个bean,ConfigPropertySourcesProcessor

ConfigPropertySourcesProcessor
scala 复制代码
public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor
    implements BeanDefinitionRegistryPostProcessor {
​
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
  
    Map<String, Object> propertySourcesPlaceholderPropertyValues = new HashMap<>();
 
    propertySourcesPlaceholderPropertyValues.put("order", 0);
    
// 1. 注册Spring的PropertySourcesPlaceholderConfigurer,Spring框架中的类,用于处理bean中和属性相关的placeholder
    BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(),
        PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues);
    
    2. ApolloConfig属性级别注解和ApolloConfigChangeListener方法级别注解的支持
    BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
        ApolloAnnotationProcessor.class);
   
    3. 解析@Value注解,并抽象为SpringValue,注册到单例的SpringValueRegistry中保存
    BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class);
   
    4. 方法和属性级别的ApolloJsonValue注解的支持
    BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(),
        ApolloJsonValueProcessor.class);
    
    5. xml的beanDefination处理,抽象每个Field的信息为SpringValueDefinition,保存到SpringValueDefinitionProcessor的一个map中
    processSpringValueDefinition(registry);
  }
  }

可以看到,ConfigPropertySourcesProcessor通过实现BeanDefinitionRegistryPostProcessor接口,在Spring容器初始化阶段对配置处理进行增强。忽略 1 和 5,主要注册了以下和配置相关的处理器(ApolloProcessor子类),后文再解释这些处理器的功能,先看继承类

PropertySourcesProcessor

ConfigPropertySourcesProcessor同时继承了PropertySourcesProcessor,而PropertySourcesProcessor实现了BeanFactoryPostProcessor看实现的postProcessBeanFactory方法:

scss 复制代码
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    //配置初始化
    initializePropertySources();
    //开启配置自动更新
    initializeAutoUpdatePropertiesFeature(beanFactory);
  }

主要是两个方法,initializePropertySources和initializeAutoUpdatePropertiesFeature,下面一一查看:

1.initializePropertySources()

重点是获取配置代码: Config config = ConfigService.getConfig(namespace) -》DefaultConfigManager#getConfig -》DefaultConfigFactory#create -》DefaultConfigFactory#createLocalConfigRepository

-》DefaultConfigFactory#createRemoteConfigRepository

三个配置获取入口,内部都是调用 sync方法:

java 复制代码
protected synchronized void sync() {
  Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
​
  try {
  //旧的配置
    ApolloConfig previous = m_configCache.get();
    //获取远程配置
    ApolloConfig current = loadApolloConfig();
​
    //reference equals means HTTP 304
    if (previous != current) {
      ("[ARCH_APOLLO_CLIENT]Remote Config refreshed!");
      m_configCache.set(current);
    //刷新配置
      this.fireRepositoryChange(m_namespace, this.getConfig());
    }
​
    if (current != null) {
      Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),
          current.getReleaseKey());
    }
​
    transaction.setStatus(Transaction.SUCCESS);
  } catch (Throwable ex) {
    transaction.setStatus(ex);
    throw ex;
  } finally {
    transaction.complete();
  }
}

刷新配置内部方法,最终触发了监听事件(热更新的入口,后面讲解):

2.initializeAutoUpdatePropertiesFeature(beanFactory)

scss 复制代码
private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
    if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled() ||
        !AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {
      return;
    }
    //生成监听器,包含 spring的配置Environment对象
    AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new AutoUpdateConfigChangeListener(
        environment, beanFactory);
​
    List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
    //遍历配置,添加监听器
    for (ConfigPropertySource configPropertySource : configPropertySources) {
      configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
    }
  }
ApolloProcessor

之前在看ConfigPropertySourcesProcessor中,提到的各种处理器,都是继承自ApolloProcessor,看下具体是干什么的?

ApolloProcessor是一个抽象类,实现了BeanPostProcessor接口,负责在Bean初始化之前(上面配置处理之后),注入Apollo的配置,并把Spring的@Value抽象成对象,注册到Apollo自己的一个单例SpringValueRegistry中,方便后续的配置更新。首先看下类继承关系:

BeanPostProcessor接口,说明这些对象在每一个bean被实例化的时候都有可能会被调用

  1. ApolloAnnotationProcessor ○ 处理ApolloConfig的属性级别注解和方法级别注解
  2. SpringValueProcessor ○ 解析@Value注解,将解析结果抽象为SpringValue对象,并注册到SpringValueRegistry中
  3. ApolloJsonValueProcessor ○ 支持方法和属性级别的ApolloJsonValue注解 这些处理器通过动态注册BeanDefinition的方式加载,确保配置处理功能的增强和扩展

关键关注processField和processMethod两个抽象方法,由子类实现属性和方法的处理方式,可以根据需要,对bean中每一个方法和属性进行处理。

SpringValueProcessor
scala 复制代码
public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor, BeanFactoryAware {
​
  
  @Override
  protected void processField(Object bean, String beanName, Field field) {
    // register @Value on field
    Value value = field.getAnnotation(Value.class);
    
    // 解析@Value的value属性,因为@Value可能有多个占位符等原因,获取到所有需要配置的key
    Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
   
    // 循环注册到springValueRegistry
    for (String key : keys) {
      SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
      springValueRegistry.register(beanFactory, key, springValue);
    }
  }
  
}
ApolloAnnotationProcessor

ApolloAnnotationProcessor主要是处理两个注解 ● 属性级别注解ApploConfig,负责注入阿波罗的Config对象,可以获取某个namespace下的Config ● 方法级别注解ApolloConfigChangeListener,负责向Config对象中添加监听器,当配置发生变化时可以收到通知

ini 复制代码
public class ApolloAnnotationProcessor extends ApolloProcessor {
​
  /**
   * 处理@ApploConfig(namespace)注解的属性,注入Apollo的Config
   */
  @Override
  protected void processField(Object bean, String beanName, Field field) {
    ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);
   
   // 校验字段必须是Config类型
    Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),
        "Invalid type: %s for field: %s, should be Config", field.getType(), field);
    String namespace = annotation.value();
    
    // 根据namespace获取阿波罗Config
    Config config = ConfigService.getConfig(namespace);
    
    // 反射设置这个field
    ReflectionUtils.makeAccessible(field);
    ReflectionUtils.setField(field, bean, config);
  }
​
  /**
   * 1、从method上找到ApolloConfigChangeListener注解
   * 2、创建ConfigChangeListener---目的是当配置发生变化时,反射调用ApolloConfigChangeListener注解的方法
   * 3、注册ConfigChangeListener到每个Namespace的Config上
   */
  @Override
  protected void processMethod(final Object bean, String beanName, final Method method) {
    ApolloConfigChangeListener annotation = AnnotationUtils
        .findAnnotation(method, ApolloConfigChangeListener.class);
    Class<?>[] parameterTypes = method.getParameterTypes();
    ReflectionUtils.makeAccessible(method);
    String[] namespaces = annotation.value();
    String[] annotatedInterestedKeys = annotation.interestedKeys();
    String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
    
    // 监听Apollo配置发生变化的Listener,当有配置发生变化会调用ConfigChangeListener的onChange方法
    ConfigChangeListener configChangeListener = new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        ReflectionUtils.invokeMethod(method, bean, changeEvent);
      }
    };
    
    Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
    Set<String> interestedKeyPrefixes = annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) : null;
    
    // 循环每个namespace,向他们的Config对象注册ConfigChangeListener
    for (String namespace : namespaces) {
      Config config = ConfigService.getConfig(namespace);
      if (interestedKeys == null && interestedKeyPrefixes == null) {
        config.addChangeListener(configChangeListener);
      } else {
        config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
      }
    }
  }
}

2.热更新

时序图

从上面各个处理器已经获取配置的代码也能看出阿波罗自动更新是通过监听器实现的,通过遍历所有配置添加的监听器,执行其onChange方法更新。主要有两类监听器:

1.注解配置更新监听器AutoUpdateConfigChangeListener 2.自定义监听器(@ApolloConfigChangeListener)

注解配置更新监听listener

typescript 复制代码
public class AutoUpdateConfigChangeListener implements ConfigChangeListener{
 
 @Override
  public void onChange(ConfigChangeEvent changeEvent) {
    //通过event拿到所有变更的keys
    Set<String> keys = changeEvent.changedKeys();
    if (CollectionUtils.isEmpty(keys)) {
      return;
    }
    //遍历keys,通过springValueRegistry.get(beanFactory, key) 拿到SpringValue集合对象
    for (String key : keys) {
      // 1. check whether the changed key is relevant
      Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
      if (targetValues == null || targetValues.isEmpty()) {
        continue;
      }
      //判断配置是否真的发生了变化
      // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
      if (!shouldTriggerAutoUpdate(changeEvent, key)) {
        continue;
      }
​
      // 遍历SpringValue集合,逐一通过反射改变字段的值
      for (SpringValue val : targetValues) {
        updateSpringValue(val);
      }
    }
  }
​
  /**
   * Check whether we should trigger the auto update or not.
   * <br />
   * For added or modified keys, we should trigger auto update if the current value in Spring equals to the new value.
   * <br />
   * For deleted keys, we will trigger auto update anyway.
   */
  private boolean shouldTriggerAutoUpdate(ConfigChangeEvent changeEvent, String changedKey) {
    ConfigChange configChange = changeEvent.getChange(changedKey);
​
    if (configChange.getChangeType() == PropertyChangeType.DELETED) {
      return true;
    }
    //比较environment中获取到的属性值与apollo中配置的新值
    return Objects.equals(environment.getProperty(changedKey), configChange.getNewValue());
  }
}

自定义listener

ini 复制代码
public class ApolloAnnotationProcessor extends ApolloProcessor {
​
  @Override
  protected void processMethod(final Object bean, String beanName, final Method method) {
    ApolloConfigChangeListener annotation = AnnotationUtils
        .findAnnotation(method, ApolloConfigChangeListener.class);
    if (annotation == null) {
      return;
    }
    Class<?>[] parameterTypes = method.getParameterTypes();
    Preconditions.checkArgument(parameterTypes.length == 1,
        "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
        method);
    Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
        "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
        method);
​
    ReflectionUtils.makeAccessible(method);
    String[] namespaces = annotation.value();
    String[] annotatedInterestedKeys = annotation.interestedKeys();
    Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
    // 创建listener  
    ConfigChangeListener configChangeListener = new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        ReflectionUtils.invokeMethod(method, bean, changeEvent);
      }
    };
    // 给config设置listener
    for (String namespace : namespaces) {
      Config config = ConfigService.getConfig(namespace);
​
      if (interestedKeys == null) {
        config.addChangeListener(configChangeListener);
      } else {
        config.addChangeListener(configChangeListener, interestedKeys);
      }
    }
  }
}

经过这段代码处理,如果有change事件,我们通过@ApolloConfigChangeListener自定义的listener就会收到消息了。

问题:无法热更新@value 的值

原因:jasypt框架com.ulisesbocchio.jasyptspringboot.wrapper.EncryptablePropertySourceWrapper类会对大部分PropertySource进行包装代理,最终委托给com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource进行配置值的返回。

经过debug发现本地的值最终是从com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource中获取,而此类中有一个cache, apollo配置变更时,此cache中存的仍是旧配置。只有监听到RefreshScopeRefreshedEvent和EnvironmentChangeEvent事件的时候才会刷新缓存。此类是jasypt相关包中的类,此包是与加解密相关的

另外,其实通过自定义监听器途径也不会更新,自定义的监听器主要更新了ConfigurationProperties的配置,这种配置类主要通过ConfigurationPropertiesRebinder对Bean进行更新最新的配置值。但是jasypt的缓存刷新执行顺序先于 ConfigurationPropertiesRebinder,所以无效。

参考:github.com/apolloconfi... 参考 2:zhuanlan.zhihu.com/p/598578085

相关推荐
㳺三才人子6 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.6 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl7 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记9 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒10 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰10 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程