Apollo 凭什么能 “干掉” 本地配置?

案例

案例一:本地propreties配置文件配置配置项

定义了一个 HelloController,这里面通过 @Value 注解注入了一个 hello.msg 配置的值。在 ApolloStudyApplication 启动类上通过注解 @EnableApolloConfig 开启 Apollo 配置。代码如下:

java 复制代码
@SpringBootApplication  
@EnableApolloConfig  
public class ApolloStudyApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(ApolloStudyApplication.class, args);  
    }  
  
}


@RestController  
public class HelloController {  
    @Value("${hello.msg}")  
    private String msg;  
  
  
    @GetMapping("/hello")  
    public String hello() {  
        return msg;  
    }  
}

application.properties 中的配置如下,在 Apollo 配置中心未配置该配置项

shell 复制代码
server.port=9091  
spring.application.name=apollostudy  
app.id=SampleApp  
apollo.config-service=http://192.168.33.123:8080
hello.msg=hello-local
apollo.bootstrap.eagerLoad.enabled=true  
apollo.bootstrap.enabled=true

然后访问 Controller,可以看到是可以获取到配置文件中配置的值的。结果如下:

案例二:Apollo和本地propreties配置文件都配置配置项

首先在 Apollo 配置中心将 hello.msg 的先配置为「你好」,然后访问 Controller,结果如下:

然后在 Apollo 配置中将 hello.msg 的先配置为「hello」,然后访问 Controller,结果如下:

从上面的两个案例可以看到当本地配置文件和 Apollo 配置中心都有相同的配置的时候,Apollo 配置中心的值会覆盖本地配置文件的值;当 Apollo 配置中心的值变化时,通过 @Value 注解注入该值的地方可以感知到变化并及时更新。

那 Apollo 配置中心的值是如何做到覆盖本地配置的值并能够及时更新的呢?接下来将从源码的角度分析一下原理。先说下结论:

Apollo 配置客户端在启动的时候会构造一个 PropertySource,并将这个对象放到 Environment 对象的最前面 ,而 Spring 查找属性的时候是按照顺序先后查找的,如果在前面的 PropertySource 中查找到了就直接返回了,Apollo 正是通过放在最前面这个操作来实现覆盖的。

Apollo 配置客户端在启动的时候会获取所有有 @Value 注解修饰的字段并缓存起来,然后通过长轮询获取服务端的配置变更,然后通过反射的方式更新字段的值,从而实现感知变化并及时更新的。

源码分析

Spring 中提供了 EnvironmentPostProcessor 接口,它有一个 postProcessEnvironment() 方法,它的核心作用是允许你在应用程序启动过程中,在 ApplicationContext 被刷新之前修改 Environment 对象 。前面的文章搞懂这两个组件,Spring 配置问题少一半!中介绍过 xxx.properties 配置文件的内容最终会被加载并放置到 Environment 中保存的,而 Apollo 就是利用 postProcessEnvironment() 可以修改 Environment 对象来实现配置的覆盖。代码如下:

java 复制代码
@FunctionalInterface
public interface EnvironmentPostProcessor {

    void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);

}

Apollo 提供了一个 ApolloApplicationContextInitializer 类实现了 EnvironmentPostProcessor 接口,在它的 postProcessEnvironment() 方法中会判断 apollo.bootstrap.eagerLoad.enabledapollo.bootstrap.enabled 配置项是否为 true,如果为 true 才会继续往下进行初始化。实际的初始化在 initialize() 方法中实现的。 在该方法中将每个 namespace 的配置包装为一个 Config 对象,然后再基于它创建一个 ProperySource 对象,然后添加到 CompositePropertySource 对象中,最后CompositePropertySource 对象添加到 Environment 对象的最前面,这个就是实现 Apollo 配置覆盖本地配置文件配置的关键。代码如下:

java 复制代码
public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {  
  // 省略代码

  // 查看apollo.bootstrap.eagerLoad.enabled配置项是否配置为true,为true才会继续往下走
  Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);
    System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, String.valueOf(eagerLoadEnabled));
    if (!eagerLoadEnabled) {
      return;
    }

    // 查看apollo.bootstrap.enabled配置项是否配置为true,为true才会继续往下走
    Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);
    System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, String.valueOf(bootstrapEnabled));
    if (bootstrapEnabled) {
      DeferredLogger.enable();
      // 调用initialize()方法
      initialize(configurableEnvironment);
    }  
  
}

protected void initialize(ConfigurableEnvironment environment) {
    final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);
    if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
      //already initialized, replay the logs that were printed before the logging system was initialized
      DeferredLogger.replayTo();
      if (configUtil.isOverrideSystemProperties()) {
        // ensure ApolloBootstrapPropertySources is still the first
        PropertySourcesUtil.ensureBootstrapPropertyPrecedence(environment);
      }
      return;
    }

    String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
    System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, namespaces);
    logger.debug("Apollo bootstrap namespaces: {}", namespaces);
    List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);

	// 这里创建一个 CompositePropertySource 对象
    CompositePropertySource composite;
    if (configUtil.isPropertyNamesCacheEnabled()) {
      composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
    } else {
      composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
    }
    for (String namespace : namespaceList) {
      // 这里将每一个namespace的配置包装为一个Config对象
      Config config = ConfigService.getConfig(namespace);
      // 然后基于Config对象构造一个ProperySource对象添加到CompositePropertySource中
      composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
    }
    if (!configUtil.isOverrideSystemProperties()) {
      if (environment.getPropertySources().contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
        environment.getPropertySources().addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, composite);
        return;
      }
    }
    // 这行代码很关键,这里将CompositePropertySource添加到了environment的最前面
    environment.getPropertySources().addFirst(composite);
  }

Apollo 还提供了一个 ApolloProcessor 实现了 BeanPostProcessor 接口,在它的 postProcessBeforeInitialization() 方法中,会查找所有有 @Value 注解修饰的字段,然后提取它里面配置项的名称,然后构造 SpringValue 对象然后注册到 SpringValueRegistry 中,在这里相当于缓存了所有有 @Value 注解修饰的字段。代码如下:

java 复制代码
public abstract class ApolloProcessor implements BeanPostProcessor, PriorityOrdered {
    public Object postProcessBeforeInitialization(Object bean, String beanName)
          throws BeansException {
        Class<?> clazz = bean.getClass();

        for (Field field : findAllField(clazz)) {
          processField(bean, beanName, field);
        }

        // 省略代码
    
        return bean;  
    }
}

public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor, BeanFactoryAware {
    protected void processField(Object bean, String beanName, Field field) {
        // register @Value on field
        Value value = field.getAnnotation(Value.class);
        if (value == null) {
          return;
        }

        doRegister(bean, beanName, field, value);
    }
    private void doRegister(Object bean, String beanName, Member member, Value value) {
        Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
        if (keys.isEmpty()) {
          return;
        }

        for (String key : keys) {
          SpringValue springValue;
          if (member instanceof Field) {
            Field field = (Field) member;
            springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
          } else if (member instanceof Method) {
            Method method = (Method) member;
            springValue = new SpringValue(key, value.value(), bean, beanName, method, false);
          } else {
            logger.error("Apollo @Value annotation currently only support to be used on methods and fields, "
                + "but is used on {}", member.getClass());
            return;
          }
          springValueRegistry.register(beanFactory, key, springValue);
          logger.info("Monitoring {}", springValue);
        }
    }
}

Apollo 提供了一个 AutoUpdateConfigChangeListener 监听器,在它的 onChange() 方法中根据发生变化的配置项作为 key 从之前的缓存获取对应的 SpringValue 列表,即被 @Value 注解修饰的字段,然后通过反射的方式更新其值。代码如下:

java 复制代码
public class AutoUpdateConfigChangeListener implements ConfigChangeListener,
    ApplicationListener<ApolloConfigChangeEvent>, ApplicationContextAware {

  @Override
  public void onChange(ConfigChangeEvent changeEvent) {
    // 获取发生变化的配置项
    Set<String> keys = changeEvent.changedKeys();
    if (CollectionUtils.isEmpty(keys)) {
      return;
    }
    for (String key : keys) {
      // 获取配置项关联的@Value注解修饰的字段
      Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
      if (targetValues == null || targetValues.isEmpty()) {
        continue;
      }

      // 2. update the value
      for (SpringValue val : targetValues) {
        updateSpringValue(val);
      }
    }
  }

  private void updateSpringValue(SpringValue springValue) {
    try {
      Object value = resolvePropertyValue(springValue);
      // 更新字段的值
      springValue.update(value);

      logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,
          springValue);
    } catch (Throwable ex) {
      logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);
    }
  }
}

而获取变更则是通过长轮询来实现的。在 RemoteConfigLongPollService 中有一个 startLongPolling() 方法,它会提交一个轮询任务,这个轮询任务会周期性地拉取最新的变更信息。代码如下:

java 复制代码
private void startLongPolling(String sysAppId) {
    if (Boolean.TRUE.equals(m_longPollStarted.putIfAbsent(sysAppId, true))) {
      //already started
      return;
    }
    try {
      final String appId = sysAppId;
      final String cluster = m_configUtil.getCluster();
      final String dataCenter = m_configUtil.getDataCenter();
      final String secret = m_configUtil.getAccessKeySecret(appId);
      final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();
      m_longPollingService.submit(new Runnable() {
        @Override
        public void run() {
          if (longPollingInitialDelayInMills > 0) {
            try {
              logger.debug("Long polling will start in {} ms.", longPollingInitialDelayInMills);
              TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);
            } catch (InterruptedException e) {
              //ignore
            }
          }
          // 轮询一次
          doLongPollingRefresh(appId, cluster, dataCenter, secret);
        }
      });
    } catch (Throwable ex) {
      // 省略代码
    }
}
相关推荐
python_13617 分钟前
web请求和响应
java·spring·github
柏油34 分钟前
可视化 MySQL binlog 监听方案
数据库·后端·mysql
舒一笑1 小时前
Started TttttApplication in 0.257 seconds (没有 Web 依赖导致 JVM 正常退出)
jvm·spring boot·后端
M1A11 小时前
Java Enum 类:优雅的常量定义与管理方式(深度解析)
后端
AAA修煤气灶刘哥2 小时前
别再懵了!Spring、Spring Boot、Spring MVC 的区别,一篇讲透
后端·面试
ciku2 小时前
Spring AI Starter和文档解读
java·人工智能·spring
柏油2 小时前
MySQL 字符集 utf8 与 utf8mb4
数据库·后端·mysql
程序猿阿越2 小时前
Kafka源码(三)发送消息-客户端
java·后端·源码阅读