Apollo 客户端集成 SpringBoot 的源码分析(1)-启动时配置获取

1. Apollo 配置中心简介

Apollo 是一个开源的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到客户端,适用于微服务配置管理场景。读者如有兴趣可查看官方文档

2. 客户端集成 SpringBoot 源码分析

Apollo 集成 SpringBoot 的处理大致可以分为两个步骤,本文基于 Apollo 1.8.2 版本,主要分析客户端拉取远端配置的流程

  1. 获取远端配置,将其包装到 SpringBoot 框架的 PropertySource 中,完成配置的嵌入
  2. 配置属性的 Bean 对象注入,可参考 Apollo 客户端集成 SpringBoot 的源码分析(2)- 配置属性的注入更新

在这里插入图片描述

2.1 ApolloApplicationContextInitializer 的主要处理

  1. 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
    
  2. WebFlux 服务启动流程 中笔者提到了 EnvironmentPostProcessor/ApplicationContextInitializer 实现类的接口方法回调的时机,可知道 ApolloApplicationContextInitializer#postProcessEnvironment() 的回调时机比较靠前,其源码如下

    1. 可以看到此处对处理主要是检查 apollo.bootstrap.eagerLoad.enabled 属性是否允许在 Spring 框架启动前期阶段加载 Apollo 配置,如允许的话,继续检查 apollo.bootstrap.enabled 属性确定是否开启了 Apollo 的启动加载开关
    2. 如检查通过,则调用 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);
     }
    
    }
    
  3. ApolloApplicationContextInitializer#initialize() 方法也可以被 ApplicationContextInitializer#initialize() 回调方法触发,其主要处理如下

    1. 因为有两个不同的触发点,所以该方法首先检查 Spring 的 Environment 环境中是否已经有了 key 为 ApolloBootstrapPropertySources 的目标属性,有的话就不必往下处理,直接 return
    2. 从 Environment 环境中获取 apollo.bootstrap.namespaces 属性配置的启动命名空间字符串,如果没有的话就取默认的 application 命名空间
    3. 按逗号分割处理配置的启动命名空间字符串,然后调用 ConfigService#getConfig() 依次拉取各个命名空间的远端配置,下节详细分析这部分
    4. 调用ConfigPropertySourceFactory#getConfigPropertySource() 缓存从远端拉取的配置,并将其包装为 PropertySource,最终将所有拉取到的远端配置聚合到一个以 ApolloBootstrapPropertySources 为 key 的属性源包装类 CompositePropertySource 的内部
    5. 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);
    }
    
  4. ConfigPropertySourceFactory.getConfigPropertySource() 方法负责封装 Apollo 配置为 Spring 的 PropertySource,其处理逻辑很清晰

    1. 将 Apollo 的 Config 配置封装为继承自 Spring 内置的 EnumerablePropertySource 类的 ConfigPropertySource 对象
    2. 将新生成的 ConfigPropertySource 对象缓存到内部列表,以备后续为每个配置实例添加配置变化监听器使用
      public ConfigPropertySource getConfigPropertySource(String name, Config source) {
     ConfigPropertySource configPropertySource = new ConfigPropertySource(name, source);
    
     configPropertySources.add(configPropertySource);
    
     return configPropertySource;
    }
    

2.2 远端配置的拉取

  1. 在上一节中我们已经提到远程配置的拉取由 ConfigService#getConfig() 完成,其实这个方法只是入口,大致处理如下:

    1. s_instance.getManager() 实际通过 ApolloInjector 去获取 ConfigManager实例, ApolloInjector 其实采用了 Java 中的 ServiceLoader 机制,此处不作讨论,读者有兴趣可自行搜索
    2. 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;
    }
    
  2. DefaultConfigManager#getConfig() 方法处理逻辑较为清晰,重点如下:

    1. 首先从缓存中获取配置,缓存中没有则从远程拉取,注意此处在 synchronized 代码块内部也判了一次空,采用了双重检查锁机制
    2. 远程拉取配置首先需要通过 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;
    }
    
  3. DefaultConfigFactory#create() 方法看上去比较简单,其实这里涉及到 Apollo 中 Config 的层次设计,是三层套娃的开端

    1. 此处首先根据 DefaultConfigFactory#determineFileFormat() 确定本地配置缓存文件的格式,从源码可知道默认返回 ConfigFileFormat.Properties,则将默认调用到 new DefaultConfig(namespace, createLocalConfigRepository(namespace)) 创建 DefaultConfig 对象
    2. DefaultConfigFactory#createLocalConfigRepository() 方法在创建 LocalFileConfigRepository 对象时,又会调用 DefaultConfigFactory#createRemoteConfigRepository() 方法
    3. DefaultConfigFactory#createRemoteConfigRepository() 方法会创建出 RemoteConfigRepository 对象

    注意,此处创建的各个对象通过组合实现了层级的划分

    1. 配置相关对象的创建时序为 RemoteConfigRepository --> LocalFileConfigRepository --> DefaultConfig
      DefaultConfig —> 持有 LocalFileConfigRepository, LocalFileConfigRepository --> 持有 RemoteConfigRepository
      DefaultConfig —> 监听 LocalFileConfigRepository 变化, LocalFileConfigRepository --> 监听 RemoteConfigRepository 变化
    2. 配置变化传播时序为 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);
    }
    
    
  4. 首先来看 RemoteConfigRepository 的构造方法,源码粗略一看很简单

    1. 首先是初始化各个内部属性,比较重要的是 RemoteConfigLongPollService 对象注入,该对象负责长轮拉远程配置
    2. 接下来是调用 RemoteConfigRepository#trySync() 尝试拉取远程配置
    3. 接着调用 RemoteConfigRepository#schedulePeriodicRefresh() 开启定时任务,该任务定期拉取远程配置
    4. 最后调用 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();
    }
    
  5. 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;
    }
    
  6. RemoteConfigRepository#sync() 方法的核心是处理如下:

    1. 从缓存中取出配置实例 ApolloConfig,在调用 RemoteConfigRepository#loadApolloConfig() 获取当前配置
    2. 如果配置发生了变化,则调用 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();
     }
    }
    
  7. RemoteConfigRepository#loadApolloConfig() 方法比较长,不过大致处理流程并不难梳理

    1. 首先是本地 RateLimiter 令牌桶限流,最多等待 5 秒
    2. 准备请求服务端需要的数据,包括调用 RemoteConfigRepository#getConfigServices() 获取服务端列表
    3. 遍历服务端端列表,组装 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);
    }
    
    
  8. 回到步骤6RemoteConfigRepository#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);
       }
     }
    }
    
  9. 回到步骤4RemoteConfigRepository#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());
    }
    
  10. RemoteConfigRepository#scheduleLongPollingRefresh() 方法也比较简单,其实就是调用 RemoteConfigLongPollService#submit() 方法

    private void scheduleLongPollingRefresh() {
    remoteConfigLongPollService.submit(m_namespace, this);
    }
    
  11. 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));
    }
    }
    
  12. 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);
      } 
      ......
    }
    }
    
  13. 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);
        }
      }
    }
    }
    
  14. 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();
      }
    });
    }
    
  15. 至此远程仓库 RemoteConfigRepository 对象创建完成,远端指定的配置也拉到了本地,则回到步骤3,继续看本地文件仓库 LocalFileConfigRepository 对象的创建。以下为 LocalFileConfigRepository 带两个参数的构造方法,比较关键的步骤如下:

    1. 调用 LocalFileConfigRepository#findLocalCacheDir() 查找当前 appid 本地配置缓存文件,没有则创建一个
    2. 调用 LocalFileConfigRepository#setUpstreamRepository() 将创建完毕的 RemoteConfigRepository 对象设置为上游配置仓库
    3. 调用 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();
    }
    
  16. LocalFileConfigRepository#setUpstreamRepository()方法在设置上游仓库的时候主要做以下处理:

    1. 调用LocalFileConfigRepository#trySyncFromUpstream() 方法尝试与上游仓库配置同步
    2. 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);
    }
    
  17. LocalFileConfigRepository#trySyncFromUpstream() 方法如下,此处简单介绍下处理流程

    1. 这个方法核心逻辑是调用 LocalFileConfigRepository#updateFileProperties() 方法使用上游仓库的配置更新本地文件
    2. 可以看到最终将配置持久化为文件的方法是 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();
    }
    }
    
  18. 回到步骤9LocalFileConfigRepository#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);
    }
    }
    
  19. 至此本地文件仓库 LocalFileConfigRepository 对象创建完成,远端配置保存到了本地缓存文件中,则回到步骤3,继续看DefaultConfig对象的构造方法,这个过程中核心处理如下

    1. 首先调用 DefaultConfig#loadFromResource() 方法加载本地 META-INF/config/文件夹下前缀与入参 namespace 一致的配置文件
    2. 调用 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();
    }
    
  20. DefaultConfig#initialize() 方法主要做两步处理,至此远程配置拉取暂告段落

    1. 调用 DefaultConfig#initialupdateConfigize() 更新配置
    2. 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);
    }
    }
    
    
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值