文章目录
1. Apollo 配置中心简介
Apollo 是一个开源的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到客户端,适用于微服务配置管理场景。读者如有兴趣可查看官方文档
2. 客户端集成 SpringBoot 源码分析
Apollo 集成 SpringBoot 的处理大致可以分为两个步骤,本文基于 Apollo 1.8.2 版本,主要分析客户端拉取远端配置的流程
- 获取远端配置,将其包装到 SpringBoot 框架的
PropertySource
中,完成配置的嵌入- 配置属性的 Bean 对象注入,可参考 Apollo 客户端集成 SpringBoot 的源码分析(2)- 配置属性的注入更新
2.1 ApolloApplicationContextInitializer 的主要处理
-
在 SpringBoot 自动配置原理源码分析 中笔者提到过,
spring.factories 文件
是 SpringBoot 中实现 SPI 机制的重要组成,Apollo 客户端与 SpringBoot 的集成就借助了这个机制。Apollo 客户端 jar 包中的META-INF/spring.factories 文件
配置如下,本文主要关注ApolloApplicationContextInitializer
的处理流程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
-
在 WebFlux 服务启动流程 中笔者提到了
EnvironmentPostProcessor/ApplicationContextInitializer
实现类的接口方法回调的时机,可知道ApolloApplicationContextInitializer#postProcessEnvironment()
的回调时机比较靠前,其源码如下- 可以看到此处对处理主要是检查
apollo.bootstrap.eagerLoad.enabled
属性是否允许在 Spring 框架启动前期阶段加载 Apollo 配置,如允许的话,继续检查apollo.bootstrap.enabled
属性确定是否开启了 Apollo 的启动加载开关 - 如检查通过,则调用
ApolloApplicationContextInitializer#initialize()
方法去完成 Apollo 启动配置的初始化
public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) { // should always initialize system properties like app.id in the first place initializeSystemProperty(configurableEnvironment); Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false); //EnvironmentPostProcessor should not be triggered if you don't want Apollo Loading before Logging System Initialization if (!eagerLoadEnabled) { return; } Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false); if (bootstrapEnabled) { initialize(configurableEnvironment); } }
- 可以看到此处对处理主要是检查
-
ApolloApplicationContextInitializer#initialize()
方法也可以被ApplicationContextInitializer#initialize()
回调方法触发,其主要处理如下- 因为有两个不同的触发点,所以该方法首先检查 Spring 的 Environment 环境中是否已经有了 key 为
ApolloBootstrapPropertySources
的目标属性,有的话就不必往下处理,直接 return - 从 Environment 环境中获取
apollo.bootstrap.namespaces
属性配置的启动命名空间字符串,如果没有的话就取默认的 application 命名空间 - 按逗号分割处理配置的启动命名空间字符串,然后调用
ConfigService#getConfig()
依次拉取各个命名空间的远端配置,下节详细分析这部分 - 调用
ConfigPropertySourceFactory#getConfigPropertySource()
缓存从远端拉取的配置,并将其包装为PropertySource
,最终将所有拉取到的远端配置聚合到一个以ApolloBootstrapPropertySources
为 key 的属性源包装类CompositePropertySource
的内部 - 将
CompositePropertySource
属性源包装类添加到 Spring 的 Environment 环境中,注意是插入在属性源列表的头部,因为取属性的时候其实是遍历这个属性源列表来查找,找到即返回,所以出现同名属性是以前面的为准
protected void initialize(ConfigurableEnvironment environment) { if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) { //already initialized return; } 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 = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME); for (String namespace : namespaceList) { Config config = ConfigService.getConfig(namespace); composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config)); } environment.getPropertySources().addFirst(composite); }
- 因为有两个不同的触发点,所以该方法首先检查 Spring 的 Environment 环境中是否已经有了 key 为
-
ConfigPropertySourceFactory.getConfigPropertySource()
方法负责封装 Apollo 配置为 Spring 的PropertySource
,其处理逻辑很清晰- 将 Apollo 的 Config 配置封装为继承自 Spring 内置的
EnumerablePropertySource
类的ConfigPropertySource
对象 - 将新生成的
ConfigPropertySource
对象缓存到内部列表,以备后续为每个配置实例添加配置变化监听器使用
public ConfigPropertySource getConfigPropertySource(String name, Config source) { ConfigPropertySource configPropertySource = new ConfigPropertySource(name, source); configPropertySources.add(configPropertySource); return configPropertySource; }
- 将 Apollo 的 Config 配置封装为继承自 Spring 内置的
2.2 远端配置的拉取
-
在上一节中我们已经提到远程配置的拉取由
ConfigService#getConfig()
完成,其实这个方法只是入口,大致处理如下:s_instance.getManager()
实际通过ApolloInjector
去获取ConfigManager实例
,ApolloInjector
其实采用了 Java 中的 ServiceLoader 机制,此处不作讨论,读者有兴趣可自行搜索- ConfigManager 其实只有一个实现类,此处最终将调用到
DefaultConfigManager#getConfig()
方法
public static Config getConfig(String namespace) { 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; }
-
DefaultConfigManager#getConfig()
方法处理逻辑较为清晰,重点如下:- 首先从缓存中获取配置,缓存中没有则从远程拉取,注意此处在 synchronized 代码块内部也判了一次空,采用了双重检查锁机制
- 远程拉取配置首先需要通过
ConfigFactoryManager#getFactory()
方法获取ConfigFactory
实例,再通过ConfigFactory#create()
去实际地进行拉取操作。此处 Factory 的创建也使用了 ServiceLoader 机制,暂不讨论,可知最后实际调用到DefaultConfigFactory#create()
public Config getConfig(String namespace) { Config config = m_configs.get(namespace); if (config == null) { synchronized (this) { config = m_configs.get(namespace); if (config == null) { ConfigFactory factory = m_factoryManager.getFactory(namespace); config = factory.create(namespace); m_configs.put(namespace, config); } } } return config; }
-
DefaultConfigFactory#create()
方法看上去比较简单,其实这里涉及到 Apollo 中 Config 的层次设计,是三层套娃的开端- 此处首先根据
DefaultConfigFactory#determineFileFormat()
确定本地配置缓存文件的格式,从源码可知道默认返回ConfigFileFormat.Properties
,则将默认调用到new DefaultConfig(namespace, createLocalConfigRepository(namespace))
创建DefaultConfig
对象 DefaultConfigFactory#createLocalConfigRepository()
方法在创建LocalFileConfigRepository
对象时,又会调用DefaultConfigFactory#createRemoteConfigRepository()
方法DefaultConfigFactory#createRemoteConfigRepository()
方法会创建出RemoteConfigRepository
对象
注意,此处创建的各个对象通过组合实现了层级的划分
- 配置相关对象的创建时序为 RemoteConfigRepository --> LocalFileConfigRepository --> DefaultConfig
DefaultConfig —> 持有 LocalFileConfigRepository, LocalFileConfigRepository --> 持有 RemoteConfigRepository
DefaultConfig —> 监听 LocalFileConfigRepository 变化, LocalFileConfigRepository --> 监听 RemoteConfigRepository 变化 - 配置变化传播时序为 RemoteConfigRepository --> LocalFileConfigRepository --> DefaultConfig --> ConfigChangeListener
// 1. 创建 DefaultConfig 对象 public Config create(String namespace) { ConfigFileFormat format = determineFileFormat(namespace); if (ConfigFileFormat.isPropertiesCompatible(format)) { return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format)); } return new DefaultConfig(namespace, createLocalConfigRepository(namespace)); } ConfigFileFormat determineFileFormat(String namespaceName) { String lowerCase = namespaceName.toLowerCase(); for (ConfigFileFormat format : ConfigFileFormat.values()) { if (lowerCase.endsWith("." + format.getValue())) { return format; } } return ConfigFileFormat.Properties; } // 2. 创建 LocalFileConfigRepository 对象 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); } return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace)); } // 3. 创建 RemoteConfigRepository 对象 RemoteConfigRepository createRemoteConfigRepository(String namespace) { return new RemoteConfigRepository(namespace); }
- 此处首先根据
-
首先来看
RemoteConfigRepository
的构造方法,源码粗略一看很简单- 首先是初始化各个内部属性,比较重要的是
RemoteConfigLongPollService
对象注入,该对象负责长轮拉远程配置 - 接下来是调用
RemoteConfigRepository#trySync()
尝试拉取远程配置 - 接着调用
RemoteConfigRepository#schedulePeriodicRefresh()
开启定时任务,该任务定期拉取远程配置 - 最后调用
RemoteConfigRepository#scheduleLongPollingRefresh()
开启长轮询任务
public RemoteConfigRepository(String namespace) { m_namespace = namespace; m_configCache = new AtomicReference<>(); m_configUtil = ApolloInjector.getInstance(ConfigUtil.class); m_httpUtil = ApolloInjector.getInstance(HttpUtil.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(); this.scheduleLongPollingRefresh(); }
- 首先是初始化各个内部属性,比较重要的是
-
RemoteConfigRepository#trySync()
方法实际继承自抽象类AbstractConfigRepository#trySync()
, 可以看到内部逻辑简练,就是调用子类实现的AbstractConfigRepository#sync()
抽象方法,此处也即是调用RemoteConfigRepository#sync()
方法protected boolean trySync() { try { 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#sync()
方法的核心是处理如下:- 从缓存中取出配置实例 ApolloConfig,在调用
RemoteConfigRepository#loadApolloConfig()
获取当前配置 - 如果配置发生了变化,则调用
RemoteConfigRepository#fireRepositoryChange()
将其传播给当前配置仓库对象的监听者
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) { logger.debug("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(); } }
- 从缓存中取出配置实例 ApolloConfig,在调用
-
RemoteConfigRepository#loadApolloConfig()
方法比较长,不过大致处理流程并不难梳理- 首先是本地 RateLimiter 令牌桶限流,最多等待 5 秒
- 准备请求服务端需要的数据,包括调用
RemoteConfigRepository#getConfigServices()
获取服务端列表 - 遍历服务端端列表,组装 HTTP 请求,依次对列表中的服务端发起请求,不过一旦获取到结果即返回
private ApolloConfig loadApolloConfig() { if (!m_loadConfigRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) { //wait at most 5 seconds try { TimeUnit.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 sleep Throwable exception = null; List<ServiceDTO> configServices = getConfigServices(); String url = null; retryLoopLabel: for (int i = 0; i < maxRetries; i++) { List<ServiceDTO> randomConfigServices = Lists.newLinkedList(configServices); Collections.shuffle(randomConfigServices); //Access the server which notifies the client first if (m_longPollServiceDto.get() != null) { randomConfigServices.add(0, m_longPollServiceDto.getAndSet(null)); } 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 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<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class); m_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."); return m_configCache.get(); } ApolloConfig result = response.getBody(); logger.debug("Loaded config for {}: {}", m_namespace, result); return result; } catch (ApolloConfigStatusCodeException ex) { ApolloConfigStatusCodeException statusCodeException = ex; //config not found if (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 sleep onErrorSleepTime = 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); }
-
回到步骤6,
RemoteConfigRepository#fireRepositoryChange()
实际是父类实现,AbstractConfigRepository#fireRepositoryChange()
方法,实现很简单,就是遍历RepositoryChangeListener
监听器列表依次回调接口方法,这里是配置变化传播的源头。这一步会回调到LocalFileConfigRepository#onRepositoryChange()
,下文我们会提到protected void fireRepositoryChange(String namespace, Properties newProperties) { for (RepositoryChangeListener listener : m_listeners) { try { listener.onRepositoryChange(namespace, newProperties); } catch (Throwable ex) { Tracer.logError(ex); logger.error("Failed to invoke repository change listener {}", listener.getClass(), ex); } } }
-
回到步骤4,
RemoteConfigRepository#schedulePeriodicRefresh()
的逻辑很简单,就是使用定时任务线程池定时调用RemoteConfigRepository#trySync()
方法去同步远端配置private void schedulePeriodicRefresh() { logger.debug("Schedule periodic refresh with interval: {} {}", m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); m_executorService.scheduleAtFixedRate( new Runnable() { @Override public 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); } }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); }
-
RemoteConfigRepository#scheduleLongPollingRefresh()
方法也比较简单,其实就是调用RemoteConfigLongPollService#submit()
方法private void scheduleLongPollingRefresh() { remoteConfigLongPollService.submit(m_namespace, this); }
-
RemoteConfigLongPollService#submit()
方法的核心逻辑就是调用RemoteConfigLongPollService#startLongPolling()
开始长轮询,这个方法的实质其实就是在单线程线程池中调用RemoteConfigLongPollService#doLongPollingRefresh()
方法注意
RemoteConfigLongPollService#submit()
方法会将调用方RemoteConfigRepository
对象入参缓存下来,后续如果长轮询确认配置发生变化需要将这个信息通知到RemoteConfigRepository
public 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; } private void startLongPolling() { if (!m_longPollStarted.compareAndSet(false, true)) { //already started return; } 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() { @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) { m_longPollStarted.set(false); ApolloConfigException exception = new ApolloConfigException("Schedule long polling refresh failed", ex); Tracer.logError(exception); logger.warn(ExceptionUtil.getDetailMessage(exception)); } }
-
RemoteConfigLongPollService#doLongPollingRefresh()
方法的核心处理逻辑与步骤7类似,但是需注意此处有一个 while 循环,只要条件满足会一直请求服务端拉取配置。当服务端正常返回了配置后,则调用RemoteConfigLongPollService#notify()
方法通知配置变化所谓长轮询,就是服务端收到客户端先根据条件判断是否可以立即响应,如果不能立即响应就 hold 连接一段时间,直到超时或者满足条件再给客户端一个响应
例如 Apollo 服务端长轮询处理就是先检查配置是否发生变化,如果配置有变化,就立即返回当前配置给客户端,否则就 hold 请求一段时间,直到超时再返回一个 304 响应,表示配置没有变化。Apollo 中的长轮询使用 DeferredResult 实现,读者如有兴趣可参考Spring WebMVC 源码分析(3)-异步请求 DeferredResult 的原理private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) { final Random random = new Random(); ServiceDTO lastServiceDto = null; while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) { if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) { //wait at most 5 seconds try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { } } Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification"); String url = null; try { if (lastServiceDto == null) { List<ServiceDTO> configServices = getConfigServices(); lastServiceDto = configServices.get(random.nextInt(configServices.size())); } url = assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter, m_notifications); logger.debug("Long polling from {}", url); 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); final HttpResponse<List<ApolloConfigNotification>> response = m_httpUtil.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()); notify(lastServiceDto, response.getBody()); } //try to load balance if (response.getStatusCode() == 304 && random.nextBoolean()) { lastServiceDto = null; } m_longPollFailSchedulePolicyInSecond.success(); transaction.addData("StatusCode", response.getStatusCode()); transaction.setStatus(Transaction.SUCCESS); } ...... } }
-
RemoteConfigLongPollService#notify()
方法的核心就是回调步骤11缓存的RemoteConfigRepository#onLongPollNotified()
通知配置变化信息private void notify(ServiceDTO lastServiceDto, List<ApolloConfigNotification> notifications) { if (notifications == null || notifications.isEmpty()) { return; } for (ApolloConfigNotification notification : notifications) { String namespaceName = notification.getNamespaceName(); //create a new list to avoid ConcurrentModificationException List<RemoteConfigRepository> toBeNotified = Lists.newArrayList(m_longPollNamespaces.get(namespaceName)); 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 it toBeNotified.addAll(m_longPollNamespaces .get(String.format("%s.%s", namespaceName, ConfigFileFormat.Properties.getValue()))); for (RemoteConfigRepository remoteConfigRepository : toBeNotified) { try { remoteConfigRepository.onLongPollNotified(lastServiceDto, remoteMessages); } catch (Throwable ex) { Tracer.logError(ex); } } } }
-
RemoteConfigRepository.onLongPollNotified()
的处理很简单,就是向线程池提交任务,调用RemoteConfigRepository.trySync()
方法同步远端配置,这样服务端配置发生变化时,客户端就可以及时拉取最新配置了public void onLongPollNotified(ServiceDTO longPollNotifiedServiceDto, ApolloNotificationMessages remoteMessages) { m_longPollServiceDto.set(longPollNotifiedServiceDto); m_remoteMessages.set(remoteMessages); m_executorService.submit(new Runnable() { @Override public void run() { m_configNeedForceRefresh.set(true); trySync(); } }); }
-
至此远程仓库
RemoteConfigRepository
对象创建完成,远端指定的配置也拉到了本地,则回到步骤3,继续看本地文件仓库LocalFileConfigRepository
对象的创建。以下为LocalFileConfigRepository
带两个参数的构造方法,比较关键的步骤如下:- 调用
LocalFileConfigRepository#findLocalCacheDir()
查找当前 appid 本地配置缓存文件,没有则创建一个 - 调用
LocalFileConfigRepository#setUpstreamRepository()
将创建完毕的RemoteConfigRepository
对象设置为上游配置仓库 - 调用
LocalFileConfigRepository#trySync()
方法尝试同步配置,实际最后调用到LocalFileConfigRepository#sync()
方法
public LocalFileConfigRepository(String namespace, ConfigRepository upstream) { m_namespace = namespace; m_configUtil = ApolloInjector.getInstance(ConfigUtil.class); this.setLocalCacheDir(findLocalCacheDir(), false); this.setUpstreamRepository(upstream); this.trySync(); }
- 调用
-
LocalFileConfigRepository#setUpstreamRepository()
方法在设置上游仓库的时候主要做以下处理:- 调用
LocalFileConfigRepository#trySyncFromUpstream()
方法尝试与上游仓库配置同步 upstreamConfigRepository.addChangeListener(this)
将本地仓库作为监听者添加到上游仓库的监听者列表中,如果上游仓库配置有变化,就会进入步骤8提到的监听器通知流程
public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) { if (upstreamConfigRepository == null) { return; } //clear previous listener if (m_upstream != null) { m_upstream.removeChangeListener(this); } m_upstream = upstreamConfigRepository; trySyncFromUpstream(); upstreamConfigRepository.addChangeListener(this); }
- 调用
-
LocalFileConfigRepository#trySyncFromUpstream()
方法如下,此处简单介绍下处理流程- 这个方法核心逻辑是调用
LocalFileConfigRepository#updateFileProperties()
方法使用上游仓库的配置更新本地文件 - 可以看到最终将配置持久化为文件的方法是
LocalFileConfigRepository#persistLocalCacheFile()
,此处不作具体分析
private boolean trySyncFromUpstream() { if (m_upstream == null) { return false; } try { updateFileProperties(m_upstream.getConfig(), m_upstream.getSourceType()); return true; } catch (Throwable ex) { Tracer.logError(ex); logger .warn("Sync config from upstream repository {} failed, reason: {}", m_upstream.getClass(), ExceptionUtil.getDetailMessage(ex)); } return false; } private synchronized void updateFileProperties(Properties newProperties, ConfigSourceType sourceType) { this.m_sourceType = sourceType; if (newProperties.equals(m_fileProperties)) { return; } this.m_fileProperties = newProperties; persistLocalCacheFile(m_baseDir, m_namespace); } // 持久化本地配置缓存文件 void persistLocalCacheFile(File baseDir, String namespace) { if (baseDir == null) { return; } File file = assembleLocalCacheFile(baseDir, namespace); OutputStream out = null; Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "persistLocalConfigFile"); transaction.addData("LocalConfigFile", file.getAbsolutePath()); try { out = new FileOutputStream(file); m_fileProperties.store(out, "Persisted by DefaultConfig"); transaction.setStatus(Transaction.SUCCESS); } catch (IOException ex) { ApolloConfigException exception = new ApolloConfigException( String.format("Persist local cache file %s failed", file.getAbsolutePath()), ex); Tracer.logError(exception); transaction.setStatus(exception); logger.warn("Persist local cache file {} failed, reason: {}.", file.getAbsolutePath(), ExceptionUtil.getDetailMessage(ex)); } finally { if (out != null) { try { out.close(); } catch (IOException ex) { //ignore } } transaction.complete(); } }
- 这个方法核心逻辑是调用
-
回到步骤9,
LocalFileConfigRepository#trySync()
方法最后调用到LocalFileConfigRepository#sync()
方法,显然逻辑清晰,不做进一步分析protected void sync() { //sync with upstream immediately boolean syncFromUpstreamResultSuccess = trySyncFromUpstream(); if (syncFromUpstreamResultSuccess) { return; } Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncLocalConfig"); Throwable exception = null; try { transaction.addData("Basedir", m_baseDir.getAbsolutePath()); m_fileProperties = this.loadFromLocalCacheFile(m_baseDir, m_namespace); m_sourceType = ConfigSourceType.LOCAL; transaction.setStatus(Transaction.SUCCESS); } catch (Throwable ex) { Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex)); transaction.setStatus(ex); exception = ex; //ignore } finally { transaction.complete(); } if (m_fileProperties == null) { m_sourceType = ConfigSourceType.NONE; throw new ApolloConfigException( "Load config from local config failed!", exception); } }
-
至此本地文件仓库
LocalFileConfigRepository
对象创建完成,远端配置保存到了本地缓存文件中,则回到步骤3,继续看DefaultConfig
对象的构造方法,这个过程中核心处理如下- 首先调用
DefaultConfig#loadFromResource()
方法加载本地META-INF/config/
文件夹下前缀与入参 namespace 一致的配置文件 - 调用
DefaultConfig#initialize()
方法初始化配置
public DefaultConfig(String namespace, ConfigRepository configRepository) { m_namespace = namespace; m_resourceProperties = loadFromResource(m_namespace); m_configRepository = configRepository; m_configProperties = new AtomicReference<>(); m_warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute initialize(); }
- 首先调用
-
DefaultConfig#initialize()
方法主要做两步处理,至此远程配置拉取暂告段落- 调用
DefaultConfig#initialupdateConfigize()
更新配置 m_configRepository.addChangeListener(this)
将自身添加到配置仓库的监听器列表,具体来说就是LocalFileConfigRepository
如果有配置变化就会回调DefaultConfig#onRepositoryChange()
方法,将新的配置属性传递下去
private void initialize() { try { updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType()); } catch (Throwable ex) { Tracer.logError(ex); logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.", m_namespace, ExceptionUtil.getDetailMessage(ex)); } finally { //register the change listener no matter config repository is working or not //so that whenever config repository is recovered, config could get changed m_configRepository.addChangeListener(this); } }
- 调用