一文读懂Apollo客户端配置加载流程

本文基于 apollo-client 2.1.0 版本源码进行分析

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

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

  1. application (应用)
  2. environment (环境)
  3. cluster (集群)
  4. namespace (命名空间)

同时,Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo

一. SpringBoot集成Apollo

1.1 引入Apollo客户端依赖

<dependency><groupId>com.ctrip.framework.apollo</groupId><artifactId>apollo-client</artifactId><version>2.1.0</version>
</dependency>

1.2 配置apollo

#Apollo 配置
app:id: apollo-test                            #应用ID
apollo:meta: http://10.10.10.12:8080            #DEV环境配置中心地址autoUpdateInjectedSpringProperties: true   #是否开启 Spring 参数自动更新bootstrap:enabled: true                            #是否开启 Apollonamespaces: application.yaml                 #设置 NamespaceeagerLoad:enabled: true                         #将 Apollo 加载提到初始化日志系统之前
  • app.id:AppId是应用的身份信息,是配置中心获取配置的一个重要信息。

  • apollo.bootstrap.enabled:在应用启动阶段,向Spring容器注入被托管的 application.properties 文件的配置信息。

  • apollo.bootstrap.eagerLoad.enabled:将 Apollo 配置加载提到初始化日志系统之前。将Apollo配置加载提到初始化日志系统之前从1.2.0版本开始,如果希望把日志相关的配置(如 1ogging.level.root=info1ogback-spring.xml 中的参数)也放在Apollo管理,来使Apollo的加载顺序放到日志系统加载之前,不过这会导致Apollo的启动过程无法通过日志的方式输出(因为执行Apollo加载的时的日志输出便没有任何内容)。

1.3 启动项目

启动项目后,我们更改 apollo 中的配置,SpringBoot中的配置会自动更新:

 [Apollo-Config-1] c.f.a.s.p.AutoUpdateConfigChangeListener : Auto update apollo changed value successfully, new value: hahhahaha12311, key: test.hello, beanName: mongoController, field: cn.bigcoder.mongo.mongodemo.web.MongoController.hello

二. SpringBoot如何在启动时加载Apollo配置

2.1 ApolloApplicationContextInitializer

spring.factories 文件 是 SpringBoot 中实现 SPI 机制的重要组成,在这个文件中可以定义SpringBoot各种扩展点的实现类。Apollo 客户端 与 SpringBoot 的集成就借助了这个机制,apollo-client 包中的 META-INF/spring.factories 文件配置如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration
org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
org.springframework.boot.env.EnvironmentPostProcessor=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

ApolloApplicationContextInitializer 实现了 ApplicationContextInitializerEnvironmentPostProcessor 两个扩展点,使得 apollo-client 能在Spring容器启动前从Apollo Server中加载配置。

  • EnvironmentPostProcessor:当我们想在Bean中使用配置属性时,那么我们的配置属性必须在Bean实例化之前就放入到Spring到Environment中。即我们的接口需要在 application context refreshed 之前进行调用,而 EnvironmentPostProcessor 正好可以实现这个功能。
  • ApplicationContextInitializer:是Spring框架原有的概念,这个类的主要目的就是在 ConfigurableApplicationContext 类型(或者子类型)的 ApplicationContext 做refresh之前,允许我们对 ConfigurableApplicationContext 的实例做进一步的设置或者处理。

两者虽都实现在 Application Context 做 refresh 之前加载配置,但是 EnvironmentPostProcessor 的扩展点相比 ApplicationContextInitializer 更加靠前,使得 Apollo 配置加载能够提到初始化日志系统之前。

ApolloApplicationContextInitializer.postProcessEnvironment 扩展点:

// com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#postProcessEnvironment  
/**** 为了早在Spring加载日志系统阶段之前就加载Apollo配置,这个EnvironmentPostProcessor可以在ConfigFileApplicationListener成功之后调用。* 处理顺序是这样的: 加载Bootstrap属性和应用程序属性----->加载Apollo配置属性---->初始化日志系** @param configurableEnvironment* @param springApplication*/@Overridepublic void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {// should always initialize system properties like app.id in the first placeinitializeSystemProperty(configurableEnvironment);// 获取 apollo.bootstrap.eagerLoad.enabled 配置Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);// 如果你不想在日志系统初始化之前进行阿波罗加载,就不应该触发EnvironmentPostProcessorif (!eagerLoadEnabled) {// 如果未开启提前加载,则 postProcessEnvironment 扩展点直接返回,不加载配置return;}// 是否开启了 apollo.bootstrap.enabled 参数,只有开启了才会在Spring启动阶段加载配置Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);if (bootstrapEnabled) {DeferredLogger.enable();// 初始化Apollo配置,内部会加载Apollo Server配置initialize(configurableEnvironment);}}

ApolloApplicationContextInitializer.initialize 扩展点:

//com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)@Overridepublic void initialize(ConfigurableApplicationContext context) {ConfigurableEnvironment environment = context.getEnvironment();// 判断是否配置了 apollo.bootstrap.enabled=trueif (!environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false)) {logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);return;}logger.debug("Apollo bootstrap config is enabled for context {}", context);// 初始化Apollo配置,内部会加载Apollo Server配置initialize(environment);}

两个扩展点最终都会调用 ApolloApplicationContextInitializer#initialize(ConfigurableEnvironment environment) 方法初始化 apollo client,并加载远端配置:

//com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.core.env.ConfigurableEnvironment) /*** 初始化Apollo配置** @param environment*/protected void initialize(ConfigurableEnvironment environment) {final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {// 已经初始化,重播日志系统初始化之前打印的日志DeferredLogger.replayTo();if (configUtil.isOverrideSystemProperties()) {// 确保ApolloBootstrapPropertySources仍然是第一个,如果不是会将其调整为第一个,这样从Apollo加载出来的配置拥有更高优先级PropertySourcesUtil.ensureBootstrapPropertyPrecedence(environment);}// 因为有两个不同的触发点,所以该方法首先检查 Spring 的 Environment 环境中是否已经有了 key 为 ApolloBootstrapPropertySources 的目标属性,有的话就不必往下处理,直接 returnreturn;}// 获取配置的命名空间参数String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);logger.debug("Apollo bootstrap namespaces: {}", namespaces);// 使用","切分命名参数List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);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) {// 从远端拉去命名空间对应的配置Config config = ConfigService.getConfig(namespace);// 调用ConfigPropertySourceFactory#getConfigPropertySource() 缓存从远端拉取的配置,并将其包装为 PropertySource,// 最终将所有拉取到的远端配置聚合到一个以 ApolloBootstrapPropertySources 为 key 的属性源包装类 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 属性源包装类添加到 Spring 的 Environment 环境中,注意是插入在属性源列表的头部,// 因为取属性的时候其实是遍历这个属性源列表来查找,找到即返回,所以出现同名属性时,前面的优先级更高environment.getPropertySources().addFirst(composite);}

流程如下:

  1. 因为有两个不同的触发点,所以该方法首先检查 Spring 的 Environment 环境中是否已经有了 key 为 ApolloBootstrapPropertySources 的目标属性,有的话就不必往下处理,直接 return。

  2. 从 Environment 环境中获取 apollo.bootstrap.namespaces 属性配置的启动命名空间字符串,如果没有的话就取默认的 application 命名空间。

  3. 按逗号分割处理配置的启动命名空间字符串,然后调用 ConfigService#getConfig() 依次拉取各个命名空间的远端配置,下节详细分析这部分

  4. 创建 CompositePropertySource 复合属性源,因为 apollo-client 启动时可以加载多个命名空间的配置,每个命名空间对应一个 PropertySource,而多个 PropertySource 就会被封装在 CompositePropertySource 对象中,若需要获取apollo中配置的属性时,就会遍历多个命名空间所对应的 PropertySource,找到对应属性后就会直接返回,这也意味着,先加载的 namespace 中的配置具有更高优先级:

    public class CompositePropertySource extends EnumerablePropertySource<Object> {private final Set<PropertySource<?>> propertySources = new LinkedHashSet<>();@Override@Nullablepublic Object getProperty(String name) {for (PropertySource<?> propertySource : this.propertySources) {Object candidate = propertySource.getProperty(name);if (candidate != null) {return candidate;}}return null;}
    }
    
  5. 调用 ConfigPropertySourceFactory#getConfigPropertySource() 缓存从远端拉取的配置,并将其包装为 PropertySource,最终将所有拉取到的远端配置聚合到一个以 ApolloBootstrapPropertySources 为 key 的属性源包装类 CompositePropertySource 的内部。

      public ConfigPropertySource getConfigPropertySource(String name, Config source) {// 将 Apollo 的 Config 配置封装为继承自 Spring 内置的 EnumerablePropertySource 类的 ConfigPropertySource 对象ConfigPropertySource configPropertySource = new ConfigPropertySource(name, source);// 将新生成的 ConfigPropertySource 对象缓存到内部列表,以备后续为每个配置实例添加配置变化监听器使用configPropertySources.add(configPropertySource);return configPropertySource;}
    
  6. CompositePropertySource 属性源包装类添加到 Spring 的 Environment 环境中,注意是插入在属性源列表的头部,因为取属性的时候其实是遍历这个属性源列表来查找,找到即返回,所以出现同名属性时,前面的优先级更高。这样在当本地配置文件和Apollo中配置了同名参数时会使得Apollo中的优先级更高。

2.2 从远端加载配置

ApolloApplicationContextInitializer.initialize 中会调用 ConfigService.getConfig() 加载远端命名空间配置。getConfig方法处理流程如下:

// com.ctrip.framework.apollo.ConfigService#getConfig/*** 获取名称空间的配置实例** @param namespace the namespace of the config* @return config instance*/public static Config getConfig(String namespace) {// s_instance.getManager() 实际通过 ApolloInjector 去获取 ConfigManager实例, ApolloInjector 其实采用了 Java 中的 ServiceLoader 机制,此处不作讨论,读者有兴趣可自行搜索return s_instance.getManager().getConfig(namespace);}private ConfigManager getManager() {if (m_configManager == null) {synchronized (this) {if (m_configManager == null) {m_configManager = ApolloInjector.getInstance(ConfigManager.class);}}}return m_configManager;}
  1. s_instance.getManager() 实际通过 ApolloInjector 去获取 ConfigManager 实例,ApolloInjector 其实采用了 Java 中的 ServiceLoader 机制,此处不作讨论,读者有兴趣可自行搜索
  2. ConfigManager 其实只有一个实现类,此处最终将调用到 DefaultConfigManager#getConfig() 方法。

DefaultConfigManager#getConfig() 方法处理逻辑较为清晰,重点如下:

  @Overridepublic Config getConfig(String namespace) {// 首先从缓存中获取配置,缓存中没有则从远程拉取,注意此处在 synchronized 代码块内部也判了一次空,采用了双重检查锁机制Config config = m_configs.get(namespace);if (config == null) {synchronized (this) {config = m_configs.get(namespace);// 加锁后再次判断if (config == null) {// 远程拉取配置首先需要通过 ConfigFactoryManager#getFactory() 方法获取 ConfigFactory 实例ConfigFactory factory = m_factoryManager.getFactory(namespace);// 再通过 ConfigFactory#create() 去实际地进行拉取操作。此处 Factory 的创建也使用了 ServiceLoader 机制,暂不讨论,可知最后实际调用到 DefaultConfigFactory#create()config = factory.create(namespace);// 将从远端拉取到的配置缓存m_configs.put(namespace, config);}}}
  1. 首先从缓存中获取配置,缓存中没有则从远程拉取,注意此处在 synchronized 代码块内部也判了一次空,采用了双重检查锁机制。
  2. 远程拉取配置首先需要通过 ConfigFactoryManager#getFactory() 方法获取 ConfigFactory 实例,这里实际上获取的是DefaultConfigFactory,再通过 DefaultConfigFactory#create() 去获取 Apollo Server 中的配置。

DefaultConfigFactory#create() 中会根据加载namespace类型,创建对应的 ConfigRepository

 //com.ctrip.framework.apollo.spi.DefaultConfigFactory#create @Overridepublic Config create(String namespace) {// 确定本地配置缓存文件的格式。对于格式不是属性的命名空间,必须提供文件扩展名,例如application.yamlConfigFileFormat format = determineFileFormat(namespace);ConfigRepository configRepository = null;if (ConfigFileFormat.isPropertiesCompatible(format) &&format != ConfigFileFormat.Properties) {// 如果是YML类型的配置configRepository = createPropertiesCompatibleFileConfigRepository(namespace, format);} else {// 如果是 Properties 类型的配置configRepository = createConfigRepository(namespace);}logger.debug("Created a configuration repository of type [{}] for namespace [{}]",configRepository.getClass().getName(), namespace);// 创建 DefaultConfig对象,并将当前 DefaultConfig 对象 对象注册进 configRepository 更新通知列表,这样configRepository中的配置发生变更时,就会通知 DefaultConfigreturn this.createRepositoryConfig(namespace, configRepository);}

我们就以 properties 配置类型为例,会调用 DefaultConfigFactory.createConfigRepository 创建 ConfigRepository

  // com.ctrip.framework.apollo.spi.DefaultConfigFactory#createConfigRepositoryConfigRepository createConfigRepository(String namespace) {if (m_configUtil.isPropertyFileCacheEnabled()) {// 默认是开启缓存机制的return createLocalConfigRepository(namespace);}return createRemoteConfigRepository(namespace);}

2.3 Apollo ConfigRepository 分层设计

Apollo ConfigRepository 适用于加载配置的接口,默认有两种实现:

  • LocalFileConfigRepository:从本地文件中加载配置。
  • RemoteConfigRepository:从远端Apollo Server加载配置。

在调用 DefaultConfigFactory#createConfigRepository 创建 ConfigRepository 时默认会创建多级对象,创建时的顺序为:RemoteConfigRepository --> LocalFileConfigRepository --> DefaultConfig

其中 DefaultConfig 持有 LocalFileConfigRepositoryLocalFileConfigRepository 持有 RemoteConfigRepository

DefaultConfig 监听 LocalFileConfigRepository 变化,LocalFileConfigRepository 监听 RemoteConfigRepository 变化。

创建流程如下:

  ConfigRepository createConfigRepository(String namespace) {if (m_configUtil.isPropertyFileCacheEnabled()) {// 默认是开启缓存机制的return createLocalConfigRepository(namespace);}return createRemoteConfigRepository(namespace);}LocalFileConfigRepository createLocalConfigRepository(String namespace) {if (m_configUtil.isInLocalMode()) {logger.warn("==== Apollo is in local mode! Won't pull configs from remote server for namespace {} ! ====",namespace);return new LocalFileConfigRepository(namespace);}// 创建 RemoteConfigRepository 和 LocalFileConfigRepository,并将 LocalFileConfigRepository 注册进 RemoteConfigRepository的变更通知列表中return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));}RemoteConfigRepository createRemoteConfigRepository(String namespace) {return new RemoteConfigRepository(namespace);}

Apollo 通过多层 ConfigRepository 设计实现如下配置加载机制,既保证了配置的实时性,又保证了Apollo Server出现故障时对接入的服务影响最小:

  1. 客户端和服务端保持了一个长连接(通过Http Long Polling实现),从而能第一时间获得配置更新的推送(RemoteConfigRepository)

  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

    • 这是一个fallback机制,为了防止推送机制失效导致配置不更新。客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
    • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property:apollo.refreshInterval来覆盖,单位为分钟
  3. 客户端会把从服务端获取到的配置在本地文件系统缓存一份在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置(LocalFileConfigRepository)

  4. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中(DefaultConfig)

2.4.1 RemoteConfigRepository

RemoteConfigRepository 实现 AbstractConfigRepository 抽象类,远程配置Repository。实现从Apollo Server拉取配置,并缓存在内存中。定时 + 实时刷新缓存:


构造方法

public class RemoteConfigRepository extends AbstractConfigRepository {private static final Logger logger = DeferredLoggerFactory.getLogger(RemoteConfigRepository.class);private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("=");private static final Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper();private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();private final ConfigServiceLocator m_serviceLocator;private final HttpClient m_httpClient;private final ConfigUtil m_configUtil;/*** 远程配置长轮询服务*/private final RemoteConfigLongPollService remoteConfigLongPollService;/*** 指向ApolloConfig的AtomicReference,拉取的远端配置缓存*/private volatile AtomicReference<ApolloConfig> m_configCache;private final String m_namespace;private final static ScheduledExecutorService m_executorService;private final AtomicReference<ServiceDTO> m_longPollServiceDto;private final AtomicReference<ApolloNotificationMessages> m_remoteMessages;/*** 加载配置的RateLimiter*/private final RateLimiter m_loadConfigRateLimiter;/*** 是否强制拉取缓存的标记* 若为true,则多一轮从Config Service拉取配置* 为true的原因:RemoteConfigRepository知道Config Service有配置刷新*/private final AtomicBoolean m_configNeedForceRefresh;/*** 失败定时重试策略*/private final SchedulePolicy m_loadConfigFailSchedulePolicy;private static final Gson GSON = new Gson();static {m_executorService = Executors.newScheduledThreadPool(1,ApolloThreadFactory.create("RemoteConfigRepository", true));}/*** Constructor.** @param namespace the namespace*/public RemoteConfigRepository(String namespace) {m_namespace = namespace;m_configCache = new AtomicReference<>();m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);m_httpClient = ApolloInjector.getInstance(HttpClient.class);m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);m_longPollServiceDto = new AtomicReference<>();m_remoteMessages = new AtomicReference<>();m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());m_configNeedForceRefresh = new AtomicBoolean(true);m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),m_configUtil.getOnErrorRetryInterval() * 8);// 尝试同步配置this.trySync();// 初始化定时刷新配置的任务this.schedulePeriodicRefresh();// 注册自己到RemoteConfigLongPollService中,实现配置更新的实时通知this.scheduleLongPollingRefresh();}
}

RemoteConfigRepository 构造方法中分别调用了 trySync() 尝试同步配置,schedulePeriodicRefresh() 初始化定时刷新配置的任务,scheduleLongPollingRefresh() 注册自己到 RemoteConfigLongPollService 中实现配置更新的实时通知。


trySync():

public abstract class AbstractConfigRepository implements ConfigRepository {protected boolean trySync() {try {// 调用实现类的sync方法sync();return true;} catch (Throwable ex) {Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));logger.warn("Sync config failed, will retry. Repository {}, reason: {}", this.getClass(), ExceptionUtil.getDetailMessage(ex));}return false;}
}

RemoteConfigRepository 构造方法中调用的 trySync 方法,最终会调用实现类的自己的 sync 方法:

  // com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync@Overrideprotected synchronized void sync() {Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");try {// 缓存的 Apollo服务端配置ApolloConfig previous = m_configCache.get();// 从Apollo Server加载配置ApolloConfig current = loadApolloConfig();//reference equals means HTTP 304if (previous != current) {logger.debug("Remote Config refreshed!");// 若不相等,说明更新了,设置到缓存中m_configCache.set(current);// 发布配置变更事件,实际上是回调 LocalFileConfigRepository.onRepositoryChangethis.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();}}
  1. 调用 loadApolloConfig() 方法加载远端配置信息。

      // com.ctrip.framework.apollo.internals.RemoteConfigRepository#loadApolloConfigprivate ApolloConfig loadApolloConfig() {// 限流if (!m_loadConfigRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {try {// 如果被限流则sleep 5sTimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {}}String appId = m_configUtil.getAppId();String cluster = m_configUtil.getCluster();String dataCenter = m_configUtil.getDataCenter();String secret = m_configUtil.getAccessKeySecret();Tracer.logEvent("Apollo.Client.ConfigMeta", STRING_JOINER.join(appId, cluster, m_namespace));//计算重试次数int maxRetries = m_configNeedForceRefresh.get() ? 2 : 1;long onErrorSleepTime = 0; // 0 means no sleepThrowable exception = null;//获得所有的Apollo Server的地址List<ServiceDTO> configServices = getConfigServices();String url = null;//循环读取配置重试次数直到成功 每一次都会循环所有的ServiceDTO数组retryLoopLabel:for (int i = 0; i < maxRetries; i++) {List<ServiceDTO> randomConfigServices = Lists.newLinkedList(configServices);// 随机所有的Config Service 的地址Collections.shuffle(randomConfigServices);// 优先访问通知配置变更的Config Service的地址 并且获取到时,需要置空,避免重复优先访问if (m_longPollServiceDto.get() != null) {randomConfigServices.add(0, m_longPollServiceDto.getAndSet(null));}//循环所有的Apollo Server的地址for (ServiceDTO configService : randomConfigServices) {if (onErrorSleepTime > 0) {logger.warn("Load config failed, will retry in {} {}. appId: {}, cluster: {}, namespaces: {}",onErrorSleepTime, m_configUtil.getOnErrorRetryIntervalTimeUnit(), appId, cluster, m_namespace);try {m_configUtil.getOnErrorRetryIntervalTimeUnit().sleep(onErrorSleepTime);} catch (InterruptedException e) {//ignore}}// 组装查询配置的地址url = assembleQueryConfigUrl(configService.getHomepageUrl(), appId, cluster, m_namespace,dataCenter, m_remoteMessages.get(), m_configCache.get());logger.debug("Loading config from {}", url);//创建HttpRequest对象HttpRequest request = new HttpRequest(url);if (!StringUtils.isBlank(secret)) {Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);request.setHeaders(headers);}Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "queryConfig");transaction.addData("Url", url);try {// 发起请求,返回HttpResponse对象HttpResponse<ApolloConfig> response = m_httpClient.doGet(request, ApolloConfig.class);// 设置是否强制拉取缓存的标记为falsem_configNeedForceRefresh.set(false);// 标记成功m_loadConfigFailSchedulePolicy.success();transaction.addData("StatusCode", response.getStatusCode());transaction.setStatus(Transaction.SUCCESS);if (response.getStatusCode() == 304) {logger.debug("Config server responds with 304 HTTP status code.");// 无新的配置, 直接返回缓存的 ApolloConfig 对象return m_configCache.get();}// 有新的配置,进行返回新的ApolloConfig对象ApolloConfig result = response.getBody();logger.debug("Loaded config for {}: {}", m_namespace, result);return result;} catch (ApolloConfigStatusCodeException ex) {ApolloConfigStatusCodeException statusCodeException = ex;//config not foundif (ex.getStatusCode() == 404) {String message = String.format("Could not find config for namespace - appId: %s, cluster: %s, namespace: %s, " +"please check whether the configs are released in Apollo!",appId, cluster, m_namespace);statusCodeException = new ApolloConfigStatusCodeException(ex.getStatusCode(),message);}Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(statusCodeException));transaction.setStatus(statusCodeException);exception = statusCodeException;if(ex.getStatusCode() == 404) {break retryLoopLabel;}} catch (Throwable ex) {Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));transaction.setStatus(ex);exception = ex;} finally {transaction.complete();}// if force refresh, do normal sleep, if normal config load, do exponential sleeponErrorSleepTime = m_configNeedForceRefresh.get() ? m_configUtil.getOnErrorRetryInterval() :m_loadConfigFailSchedulePolicy.fail();}}String message = String.format("Load Apollo Config failed - appId: %s, cluster: %s, namespace: %s, url: %s",appId, cluster, m_namespace, url);throw new ApolloConfigException(message, exception);}
    
  2. 如果配置发生变更,回调 LocalFileConfigRepository.onRepositoryChange方法,从而将最新配置同步到 LocalFileConfigRepository。而 LocalFileConfigRepository 在更新完本地文件缓存配置后,同样会回调 DefaultConfig.onRepositoryChange 同步内存缓存,具体源码我们在后文分析。


schedulePeriodicRefresh

  // com.ctrip.framework.apollo.internals.RemoteConfigRepository#schedulePeriodicRefreshprivate void schedulePeriodicRefresh() {logger.debug("Schedule periodic refresh with interval: {} {}",m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());m_executorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));logger.debug("refresh config for namespace: {}", m_namespace);// 同步配置trySync();Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);}// 默认每5分钟同步一次配置}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),m_configUtil.getRefreshIntervalTimeUnit());}

scheduleLongPollingRefresh()

  // com.ctrip.framework.apollo.internals.RemoteConfigRepository#scheduleLongPollingRefreshprivate void scheduleLongPollingRefresh() {//将自己注册到RemoteConfigLongPollService中,实现配置更新的实时通知//当RemoteConfigLongPollService长轮询到该RemoteConfigRepository的Namespace下的配置更新时,会回调onLongPollNotified()方法remoteConfigLongPollService.submit(m_namespace, this);}// com.ctrip.framework.apollo.internals.RemoteConfigRepository#onLongPollNotifiedpublic void onLongPollNotified(ServiceDTO longPollNotifiedServiceDto, ApolloNotificationMessages remoteMessages) {//设置长轮询到配置更新的Config Service 下次同步配置时,优先读取该服务m_longPollServiceDto.set(longPollNotifiedServiceDto);m_remoteMessages.set(remoteMessages);// 提交同步任务m_executorService.submit(new Runnable() {@Overridepublic void run() {// 设置是否强制拉取缓存的标记为truem_configNeedForceRefresh.set(true);//尝试同步配置trySync();}});}  
2.4.2 RemoteConfigLongPollService

RemoteConfigLongPollService 远程配置长轮询服务。负责长轮询 Apollo Server 的配置变更通知 /notifications/v2 接口。当有新的通知时,触发 RemoteConfigRepository.onLongPollNotified,立即轮询 Apollo Server 的配置读取/configs/{appId}/{clusterName}/{namespace:.+}接口。

构造方法

public class RemoteConfigLongPollService {private static final Logger logger = LoggerFactory.getLogger(RemoteConfigLongPollService.class);private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("=");private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();private static final long INIT_NOTIFICATION_ID = ConfigConsts.NOTIFICATION_ID_PLACEHOLDER;//90 seconds, should be longer than server side's long polling timeout, which is now 60 secondsprivate static final int LONG_POLLING_READ_TIMEOUT = 90 * 1000;/*** 长轮询ExecutorService*/private final ExecutorService m_longPollingService;/*** 是否停止长轮询的标识*/private final AtomicBoolean m_longPollingStopped;/*** 失败定时重试策略*/private SchedulePolicy m_longPollFailSchedulePolicyInSecond;/*** 长轮询的RateLimiter*/private RateLimiter m_longPollRateLimiter;/*** 是否长轮询已经开始的标识*/private final AtomicBoolean m_longPollStarted;/*** 长轮询的Namespace Multimap缓存* key:namespace的名字* value:RemoteConfigRepository集合*/private final Multimap<String, RemoteConfigRepository> m_longPollNamespaces;/*** 通知编号Map缓存* key:namespace的名字* value:最新的通知编号*/private final ConcurrentMap<String, Long> m_notifications;/*** 通知消息Map缓存* key:namespace的名字* value:ApolloNotificationMessages 对象*/private final Map<String, ApolloNotificationMessages> m_remoteNotificationMessages;//namespaceName -> watchedKey -> notificationIdprivate Type m_responseType;private static final Gson GSON = new Gson();private ConfigUtil m_configUtil;private HttpClient m_httpClient;private ConfigServiceLocator m_serviceLocator;private final ConfigServiceLoadBalancerClient configServiceLoadBalancerClient = ServiceBootstrap.loadPrimary(ConfigServiceLoadBalancerClient.class);/*** Constructor.*/public RemoteConfigLongPollService() {m_longPollFailSchedulePolicyInSecond = new ExponentialSchedulePolicy(1, 120); //in secondm_longPollingStopped = new AtomicBoolean(false);m_longPollingService = Executors.newSingleThreadExecutor(ApolloThreadFactory.create("RemoteConfigLongPollService", true));m_longPollStarted = new AtomicBoolean(false);m_longPollNamespaces =Multimaps.synchronizedSetMultimap(HashMultimap.<String, RemoteConfigRepository>create());m_notifications = Maps.newConcurrentMap();m_remoteNotificationMessages = Maps.newConcurrentMap();m_responseType = new TypeToken<List<ApolloConfigNotification>>() {}.getType();m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);m_httpClient = ApolloInjector.getInstance(HttpClient.class);m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);m_longPollRateLimiter = RateLimiter.create(m_configUtil.getLongPollQPS());}
}

submit

  // com.ctrip.framework.apollo.internals.RemoteConfigLongPollService#submitpublic boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) {// 将远程仓库缓存下来boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);if (!m_longPollStarted.get()) {// 若未启动长轮询定时任务,进行启动startLongPolling();}return added;}

startLongPolling

  // com.ctrip.framework.apollo.internals.RemoteConfigLongPollService#startLongPollingprivate void startLongPolling() {// CAS设置 m_longPollStarted 为 true,代表长轮询已启动if (!m_longPollStarted.compareAndSet(false, true)) {//already startedreturn;}try {final String appId = m_configUtil.getAppId();final String cluster = m_configUtil.getCluster();final String dataCenter = m_configUtil.getDataCenter();final String secret = m_configUtil.getAccessKeySecret();// 获得长轮询任务的初始化延迟时间,单位毫秒final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();// 提交长轮询任务 该任务会持续且循环执行m_longPollingService.submit(new Runnable() {@Overridepublic 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) {m_longPollStarted.set(false);ApolloConfigException exception =new ApolloConfigException("Schedule long polling refresh failed", ex);Tracer.logError(exception);logger.warn(ExceptionUtil.getDetailMessage(exception));}}

doLongPollingRefresh:

  // com.ctrip.framework.apollo.internals.RemoteConfigLongPollService#doLongPollingRefreshprivate void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {ServiceDTO lastServiceDto = null;// 循环执行,直到停止或线程中断while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {//wait at most 5 secondstry {// 若被限流,则等待5sTimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {}}Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");String url = null;try {// 获得Apollo Server的地址if (lastServiceDto == null) {lastServiceDto = this.resolveConfigService();}// 组装长轮询通知变更的地址url =assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,m_notifications);logger.debug("Long polling from {}", url);// 创建HttpRequest对象,并设置超时时间HttpRequest request = new HttpRequest(url);request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);if (!StringUtils.isBlank(secret)) {Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);request.setHeaders(headers);}transaction.addData("Url", url);// 发起请求,返回HttpResponse对象final HttpResponse<List<ApolloConfigNotification>> response =m_httpClient.doGet(request, m_responseType);logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);// 有新的通知,刷新本地的缓存if (response.getStatusCode() == 200 && response.getBody() != null) {updateNotifications(response.getBody());updateRemoteNotifications(response.getBody());transaction.addData("Result", response.getBody().toString());// 通知对应的RemoteConfigRepository们notify(lastServiceDto, response.getBody());}//try to load balance// 无新的通知,重置连接的Config Service的地址,下次请求不同的Config Service,实现负载均衡if (response.getStatusCode() == 304 && ThreadLocalRandom.current().nextBoolean()) {lastServiceDto = null;}// 标记成功m_longPollFailSchedulePolicyInSecond.success();transaction.addData("StatusCode", response.getStatusCode());transaction.setStatus(Transaction.SUCCESS);} catch (Throwable ex) {lastServiceDto = null;Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));transaction.setStatus(ex);long sleepTimeInSecond = m_longPollFailSchedulePolicyInSecond.fail();logger.warn("Long polling failed, will retry in {} seconds. appId: {}, cluster: {}, namespaces: {}, long polling url: {}, reason: {}",sleepTimeInSecond, appId, cluster, assembleNamespaces(), url, ExceptionUtil.getDetailMessage(ex));try {TimeUnit.SECONDS.sleep(sleepTimeInSecond);} catch (InterruptedException ie) {//ignore}} finally {transaction.complete();}}}

notify

  private void notify(ServiceDTO lastServiceDto, List<ApolloConfigNotification> notifications) {if (notifications == null || notifications.isEmpty()) {return;}for (ApolloConfigNotification notification : notifications) {String namespaceName = notification.getNamespaceName();// 创建新的RemoteConfigRepository数组,避免并发问题List<RemoteConfigRepository> toBeNotified =Lists.newArrayList(m_longPollNamespaces.get(namespaceName));// 获得远程的ApolloNotificationMessages对象并克隆ApolloNotificationMessages originalMessages = m_remoteNotificationMessages.get(namespaceName);ApolloNotificationMessages remoteMessages = originalMessages == null ? null : originalMessages.clone();//since .properties are filtered out by default, so we need to check if there is any listener for ittoBeNotified.addAll(m_longPollNamespaces.get(String.format("%s.%s", namespaceName, ConfigFileFormat.Properties.getValue())));// 循环RemoteConfigRepository进行通知for (RemoteConfigRepository remoteConfigRepository : toBeNotified) {try {// 回调 RemoteConfigRepository.onLongPollNotified 方法,让其重新拉取最新的配置remoteConfigRepository.onLongPollNotified(lastServiceDto, remoteMessages);} catch (Throwable ex) {Tracer.logError(ex);}}}}

至此 RemoteConfigRepository 从远端拉取配置的整个流程就已经分析完毕,Spring启动流程创建 RemoteConfigRepository 对象时会尝试第一次拉取namespace对应的配置,拉取完后会创建定时拉取任务和长轮询任务,长轮询任务调用 RemoteConfigLongPollService#startLongPolling 来实现,若服务端配置发生变更,则会回调 RemoteConfigRepository#onLongPollNotified 方法,在这个方法中会调用 RemoteConfigRepository#sync 方法重新拉取对应 namespace 的远端配置。

2.4.3 LocalFileConfigRepository

前文我们提到当服务端配置发生变更后,RemoteConfigRepository 会收到配置变更通知并调用 sync 方法同步配置,若配置发生变更,则会继续回调 LocalFileConfigRepository#onRepositoryChange

// LocalFileConfigRepository.onRepositoryChange@Overridepublic void onRepositoryChange(String namespace, Properties newProperties) {if (newProperties.equals(m_fileProperties)) {return;}Properties newFileProperties = propertiesFactory.getPropertiesInstance();newFileProperties.putAll(newProperties);// 将最新配置写入本地文件updateFileProperties(newFileProperties, m_upstream.getSourceType());// 回调 DefaultConfig.onRepositoryChange 方法this.fireRepositoryChange(namespace, newProperties);}
2.4.4 DefaultConfig

LocalFileConfigRepository 收到 RemoteConfigRepository 的配置变更通知并更新本地配置文件后,会继续回调 DefaultConfig#onRepositoryChange

 // com.ctrip.framework.apollo.internals.DefaultConfig#onRepositoryChange@Overridepublic synchronized void onRepositoryChange(String namespace, Properties newProperties) {// 如果属性配置未发生变更,则直接退出if (newProperties.equals(m_configProperties.get())) {return;}// 获取配置源类型,默认情况下 这里是 LocalFileConfigRepositoryConfigSourceType sourceType = m_configRepository.getSourceType();Properties newConfigProperties = propertiesFactory.getPropertiesInstance();newConfigProperties.putAll(newProperties);// 更新配置缓存,并计算实际发生变更的key, key为发生变更的配置key,value是发生变更的配置信息Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties,sourceType);//check double checked resultif (actualChanges.isEmpty()) {// 如果未发生属性变更,则直接退出return;}// 发送 属性变更给注册的 ConfigChangeListenerthis.fireConfigChange(m_namespace, actualChanges);Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);}

整体流程:

  1. 更新配置缓存,并计算实际发生变更的key,key为发生变更的配置key,value是发生变更的配置信息:

    例如我们变更 test.hello 配置以及新增一个 test.hello3 配置:

  2. 发送属性变更通知,注意在这里就不像 Resporsitory 层发送的是整个仓库的变更事件,而发送的是某一个属性变更的事件。Repository配置变更事件监听是实现 RepositoryChangeListener,属性变更事件监听是实现 ConfigChangeListener

三. Apollo如何实现Spring Bean配置属性的实时更新

在 SpringBoot 中使用 Apollo 客户端一般都需要启用 @EnableApolloConfig 注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ApolloConfigRegistrar.class)
public @interface EnableApolloConfig {/*** Apollo namespaces to inject configuration into Spring Property Sources.*/String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};/*** The order of the apollo config, default is {@link Ordered#LOWEST_PRECEDENCE}, which is Integer.MAX_VALUE.* If there are properties with the same name in different apollo configs, the apollo config with smaller order wins.* @return*/int order() default Ordered.LOWEST_PRECEDENCE;
}

@EnableApolloConfig 通过 @Import 注解注入了 ApolloConfigRegistrar 类,该类是Apollo组件注入的入口:

public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {private final ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {helper.registerBeanDefinitions(importingClassMetadata, registry);}@Overridepublic void setEnvironment(Environment environment) {this.helper.setEnvironment(environment);}}

该类实现了两个扩展点:

  • EnvironmentAware:凡注册到Spring容器内的bean,实现了EnvironmentAware接口重写setEnvironment方法后,在工程启动时可以获得application.properties的配置文件配置的属性值。
  • ImportBeanDefinitionRegistrar:该扩展点作用是通过自定义的方式直接向容器中注册bean。实现ImportBeanDefinitionRegistrar接口,在重写的registerBeanDefinitions方法中定义的Bean,就和使用xml中定义Bean效果是一样的。ImportBeanDefinitionRegistrar是Spring框架提供的一种机制,允许通过api代码向容器批量注册BeanDefinition。它实现了BeanFactoryPostProcessor接口,可以在所有bean定义加载到容器之后,bean实例化之前,对bean定义进行修改。使用ImportBeanDefinitionRegistrar,我们可以向容器中批量导入bean,而不需要在配置文件中逐个配置。

ApolloConfigRegistrar#setEnvironmentEnvironment 暂存下来;ApolloConfigRegistrar#registerBeanDefinitions 中调用 ApolloConfigRegistrarHelper.registerBeanDefinitions 注册了一系列Spring扩展点实例至Ioc容器:

  // com.ctrip.framework.apollo.spring.spi.DefaultApolloConfigRegistrarHelper#registerBeanDefinitions@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableApolloConfig.class.getName()));final String[] namespaces = attributes.getStringArray("value");final int order = attributes.getNumber("order");final String[] resolvedNamespaces = this.resolveNamespaces(namespaces);PropertySourcesProcessor.addNamespaces(Lists.newArrayList(resolvedNamespaces), order);Map<String, Object> propertySourcesPlaceholderPropertyValues = new HashMap<>();// to make sure the default PropertySourcesPlaceholderConfigurer's priority is higher than PropertyPlaceholderConfigurerpropertySourcesPlaceholderPropertyValues.put("order", 0);// PropertySourcesPlaceholderConfigurer是 SpringBoot 框架自身的占位符处理配置,占位符的处理主要是将 ${apollo.value} 这样的字符串解析出 关键字 apollo.value,再使用这个 key 通过 PropertySourcesPropertyResolver 从 PropertySource 中找到对应的属性值替换掉占位符BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class,propertySourcesPlaceholderPropertyValues);BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, AutoUpdateConfigChangeListener.class);// 用于拉取 @EnableApolloConfig 配置的 namespace 的远程配置BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class);// 用于处理 Apollo 的专用注解BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class);// 用于处理 @Value 注解标注的类成员变量和对象方法BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class);// 用于处理 XML 文件中的占位符BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class);}

PropertySourcesProcessor 是 Apollo 最关键的组件之一,并且其实例化优先级也是最高的,PropertySourcesProcessor#postProcessBeanFactory() 会在该类实例化的时候被回调,该方法的处理如下:

  // com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#postProcessBeanFactory@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {this.configUtil = ApolloInjector.getInstance(ConfigUtil.class);// 调用 PropertySourcesProcessor#initializePropertySources() 拉取远程 namespace 配置initializePropertySources();// 调用 PropertySourcesProcessor#initializeAutoUpdatePropertiesFeature() 给所有缓存在本地的 Config 配置添加监听器initializeAutoUpdatePropertiesFeature(beanFactory);}
  1. 调用 PropertySourcesProcessor#initializePropertySources() 拉取远程 namespace 配置:

  2. 调用 PropertySourcesProcessor#initializeAutoUpdatePropertiesFeature() 给所有缓存在本地的 Config 配置添加监听器

     // com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#initializeAutoUpdatePropertiesFeature private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {if (!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {return;}// 当收到配置变更回调后,会发送 ApolloConfigChangeEvent 事件ConfigChangeListener configChangeEventPublisher = changeEvent ->applicationEventPublisher.publishEvent(new ApolloConfigChangeEvent(changeEvent));List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();for (ConfigPropertySource configPropertySource : configPropertySources) {// 将配置变更监听器注册进 DefaultConfig中configPropertySource.addChangeListener(configChangeEventPublisher);}}
    

    ConfigPropertySource#addChangeListener() 方法如下,在上文中分析过 ConfigPropertySource 包装类,我们知道这里的 this.source.addChangeListener(listener) 实际调用的是 DefaultConfig#addChangeListener() 方法。在上文中我们了解DefaultConfig 收到来自 LocalFileConfigRepository 配置变更后,会计算出具体的属性变更信息,并回调ConfigChangeListener#onChange 方法,而在这里的定义中,onChange 方法会发送一个 ApolloConfigChangeEvent 类型的Spring事件:

    ConfigChangeListener configChangeEventPublisher = changeEvent ->applicationEventPublisher.publishEvent(new ApolloConfigChangeEvent(changeEvent));
    

DefaultApolloConfigRegistrarHelper#registerBeanDefinitions 会注册 AutoUpdateConfigChangeListener Bean进入Ioc容器,而该监听器就是用于监听 ApolloConfigChangeEvent 事件,当属性发生变更调用 AutoUpdateConfigChangeListener#onChange 方法:

 // com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener#onChange@Overridepublic void onChange(ConfigChangeEvent changeEvent) {Set<String> keys = changeEvent.changedKeys();if (CollectionUtils.isEmpty(keys)) {return;}for (String key : keys) {// 1. check whether the changed key is relevantCollection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);if (targetValues == null || targetValues.isEmpty()) {continue;}// 2. update the valuefor (SpringValue val : targetValues) {updateSpringValue(val);}}}

onChange 方法会调用 updateSpringValue 更新对应Bean的属性值:

  // com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener#updateSpringValueprivate 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);}}
  1. 首先调用 AutoUpdateConfigChangeListener#resolvePropertyValue() 方法借助 SpringBoot 的组件将 @Value 中配置的占位符替换为 PropertySource 中的对应 key 的属性值,此处涉及到 Spring 创建 Bean 对象时的属性注入机制,比较复杂,暂不作深入分析。
  2. 调用 SpringValue#update()方法实际完成属性值的更新。

SpringValue#update()方法其实就是使用反射机制运行时修改 Bean 对象中的成员变量,至此自动更新完成:

 // com.ctrip.framework.apollo.spring.property.SpringValue#update public void update(Object newVal) throws IllegalAccessException, InvocationTargetException {if (isField()) {injectField(newVal);} else {injectMethod(newVal);}}private void injectField(Object newVal) throws IllegalAccessException {Object bean = beanRef.get();if (bean == null) {return;}boolean accessible = field.isAccessible();field.setAccessible(true);field.set(bean, newVal);field.setAccessible(accessible);}

四. 总结

Apollo 启动时会在 ApolloApplicationContextInitializer 扩展点开始加载远端配置,而Apollo客户端获取配置采用多层设计 DefaultConfig->LocalFileConfigRepository->RemoteConfigRepository,最终由 RemoteConfigRepository 完成远端配置拉取

每一层的作用各不一样:

  • RemoteConfigRepository 负责拉取远端配置并通知 LocalFileConfigRepository 更新配置。
  • LocalFileConfigRepository 负责将远端配置缓存至本地文件,设计这一层主要是为了在Apollo Server 不可用时保证业务服务的可用性。当 LocalFileConfigRepository 配置发生变更时负责通知 DefaultConfig 更新配置。
  • DefaultConfig 负责缓存Apollo配置信息在内存中,当 DefaultConfig 配置发生变更时,会回调 AutoUpdateConfigChangeListener#onChange 方法更新Java Bean 中的属性。

Apollo 客户端为了能够实时更新 Apollo Server 中的配置,使用下列手段来实现服务端配置变更的感知:

  • 客户端和服务端保持了一个长连接(通过Http Long Polling实现),从而能第一时间获得配置更新的推送(RemoteConfigRepository

  • 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

    • 这是一个fallback机制,为了防止推送机制失效导致配置不更新。客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified

    • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定 System Property:apollo.refreshInterval 来覆盖,单位为分钟

参考文章:

Apollo 客户端集成 SpringBoot 的源码分析(1)-启动时配置获取_spring 无法实例化apolloapplicationcontextinitializer的解决-CSDN博客

Apollo 客户端集成 SpringBoot 的源码分析(2)-配置属性的注入更新-CSDN博客

Spring Boot 启动生命周期分析,每个组件的执行时序,扩展点分析等【建议收藏】(持续更新,见到一个分析一个) - 掘金 (juejin.cn)

apollo client 自动更新深入解析 - 掘金 (juejin.cn)

Apollo核心源码解析(二):Apollo Client轮询配置(ConfigRepository与RemoteConfigLongPollService)、配置中心通用设计模型_apollo客户端和服务端保持长连接的源码-CSDN博客

SpringBoot快速入门-ImportBeanDefinitionRegistrar详解 – 编程技术之美-IT之美 (itzhimei.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/16117.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

比勤奋更重要的是系统思考的能力

不要在接近你问题症状的地方寻找解决办法&#xff0c;要追溯过去&#xff0c;查找问题的根源。通常&#xff0c;最有效的活动是最微妙的。有时最好按兵不动&#xff0c;使系统自我修正&#xff0c;或让系统引导行动。有时会发现&#xff0c;最好的解决办法出现在完全出乎预料的…

HTML蓝色爱心

目录 写在前面 HTML入门 完整代码 代码分析 运行结果 系列推荐 写在后面 写在前面 最近好冷吖&#xff0c;小编给大家准备了一个超级炫酷的爱心&#xff0c;一起来看看吧&#xff01; HTML入门 HTML全称为HyperText Markup Language&#xff0c;是一种标记语言&#…

C++-指针

在C中&#xff0c;指针是至关重要的组成部分。它是C语言最强大的功能之一&#xff0c;也是最棘手的功能之一。 指针具有强大的能力&#xff0c;其本质是协助程序员完成内存的直接操纵。 指针&#xff1a;特定类型数据在内存中的存储地址&#xff0c;即内存地址。 指针变量的定…

2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(下):多智能体开发

传送门&#xff1a; 《2024.5组队学习——MetaGPT&#xff08;0.8.1&#xff09;智能体理论与实战&#xff08;上&#xff09;&#xff1a;MetaGPT安装、单智能体开发》《2024.5组队学习——MetaGPT&#xff08;0.8.1&#xff09;智能体理论与实战&#xff08;中&#xff09;&…

ModelBuilder之GDP空间化——批量值提取

一、前言 前面明确说到对于空间化过程中其实只有两个过程可以进行批量操作,一个是我们灯光指数提取过程和批量进行值提取,这里补充一点,对于灯光指数计算可以实现批量计算总灯光指数和平均灯光指数,综合灯光指数需要用平均灯光指数乘以面积占比求得,面积比就是(DN大于0的…

VS2022通过C++网络库Boost.asio搭建一个简单TCP异步服务器和客户端

基本介绍 上一篇博客我们介绍了通过Boost.asio搭建一个TCP同步服务器和客户端&#xff0c;这次我们再通过asio搭建一个异步通信的服务器和客户端系统&#xff0c;由于这是一个简单异步服务器&#xff0c;所以我们的异步特指异步服务器而不是异步客户端&#xff0c;同步服务器在…

BGP选路规则

配置地址&#xff0c;AS123使用ospf保证通讯&#xff0c;修改接口类型保证ospf学习环回20.0,30.0,100.0 地址时&#xff0c;是以24位掩码学习&#xff0c;R1&#xff0c;R2&#xff0c;R3都处于BGP边界&#xff0c;各自都需要宣告三者的私网环回 1&#xff0c; [R4]ip ip-prefi…

【Qnx 】Qnx IPC通信PPS

Qnx IPC通信PPS Qnx自带PPS服务&#xff0c;PPS全称Persistent Publish/Subscribe Service&#xff0c;就是常见的P/S通信模式。 Qnx PPS的通信模式是异步的&#xff0c;Publisher和Subscriber也无需关心对方是否存在。 利用Qnx提供的PPS服务&#xff0c;Publisher可以通知多…

嵌入式进阶——LED呼吸灯(PWM)

&#x1f3ac; 秋野酱&#xff1a;《个人主页》 &#x1f525; 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 PWM基础概念STC8H芯片PWMA应用PWM配置详解占空比 PWM基础概念 PWM全称是脉宽调制&#xff08;Pulse Width Modulation&#xff09…

Arduino下载与安装(Windows 10)

Arduino下载与安装(Windows 10) 官网 下载安装 打开官网&#xff0c;点击SOFTWARE&#xff0c;进入到软件下载界面&#xff0c;选择Windows 选择JUST DOWNLOAD 在弹出的界面中&#xff0c;填入电子邮件地址&#xff0c;勾选Privacy Policy&#xff0c;点击JUST DOWNLOAD即可 …

【脚本篇】---spyglass lint脚本

目录结构 sg_lint.tcl &#xff08;顶层&#xff09; #1.source env #date set WORK_HOME . set REPORT_PATH ${WORK_HOME}/reports puts [clock format [clock second] -format "%Y-%m-%d %H:%M:%S"] #2.generate source filelist #3.set top module puts "##…

qt-C++笔记之QThread使用

qt-C笔记之QThread使用 ——2024-05-26 下午 code review! 参考博文&#xff1a; qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面 qt-C笔记之QThread使用 文章目录 qt-C笔记之QThread使用一:Qt中几种多线程方法1.1. 使用 QThread 和 Lambda…

ubuntu server 24.04 网络 SSH等基础配置

1 安装参考上一篇: VMware Workstation 虚拟机安装 ubuntu 24.04 server 详细教程 服务器安装图形化界面-CSDN博客 2 网络配置 #安装 sudo apt install net-tools#查看 ifconfig #修改网络配置 sudo vim /etc/netplan/50-cloud-init.yaml network:version: 2ethernets:en…

飞鸡:从小训练飞行的鸡能飞行吗?为什么野鸡能飞吗?是同一品种吗?今天自由思考

鸡的飞行能力在很大程度上受到其生理结构的限制。尽管鸡有翅膀&#xff0c;但与能够长时间飞行的鸟类相比&#xff0c;鸡的翅膀相对较小&#xff0c;且胸部肌肉较弱。再加上鸡的身体较重&#xff0c;这些因素共同限制了鸡的飞行能力。通常&#xff0c;鸡只能进行短暂的、低空的…

【wiki知识库】01.wiki知识库前后端项目搭建(SpringBoot+Vue3)

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 &#x1f33c;环境准备 想要搭建自己的wiki知识库&#xff0c;要提前搭建好自己的开发环境&#xff0c;后端我使用的是SpringBoot&#xff0c;前端使用的是Vue3&#xff0c;采用前后端分离的技术实现。同时使用了Mysql数…

单工无线发射接收系统

1 绪论 随着无线电技术的发展,通讯方式也从传统的有线通讯逐渐转向无线通讯。由于传统的有线传输系统有配线的问题,较不便利,而无线通讯具有成本廉价、建设工程周期短、适应性好、扩展性好、设备维护容易实现等特点,故未来通讯方式将向无线传输系统方向发展。同时,实现系…

mfc140.dll丢失原因和mfc140.dll丢失修复办法分享

mfc140.dll是与微软基础类库&#xff08;Microsoft Foundation Classes, MFC&#xff09;紧密相关的动态链接库&#xff08;DLL&#xff09;文件。MFC是微软为C开发者设计的一个应用程序框架&#xff0c;用于简化Windows应用程序的开发工作。以下是mfc140.dll文件的一些关键属性…

栈的实现(C语言)

文章目录 前言1.栈的概念及结构2.栈的实现3.具体操作3.1.初始化栈(StackInit)和销毁栈(StackDestory)3.2.入栈(StackPush)和出栈(StackPop)3.3.获得栈的个数(StackSize)、获得栈顶元素(StackTop)以及判空(StackEmpty) 前言 前段时间我们学习过了链表和顺序表等相关操作&#x…

go-zero 实战(4)

中间件 在 userapi 项目中引入中间件。go项目中的中间可以处理请求之前和之后的逻辑。 1. 在 userapi/internal目录先创建 middlewares目录&#xff0c;并创建 user.go文件 package middlewaresimport ("github.com/zeromicro/go-zero/core/logx""net/http&q…