SpringBoot整合Apollo1.6.0 (做法+设计思路)

SpringBoot整合Apollo1.6.0

  • 一. 概述
    • 1.1 模拟场景
  • 二. 集成
    • 2.1 依赖
    • 2.2 Apollo启动配置
    • 2.3 Config工厂
    • 2.4 Config 自定义扩展类
    • 2.5 Apollo自定义配置类
    • 2.6 业务代码中读取配置的方式
  • 三. 设计思路
    • 3.1 读取Apollo配置的思路
    • 3.2 Apollo自定义配置类的设计思路

一. 概述

本文记录了Spring Boot与Apollo的整合方式,Spring boot的版本为2.1.9.RELEASE,Apollo的版本为1.6.0。

1.1 模拟场景

以下测试案例中,我在Apollo内创建了两个项目,分别是BasePlatform和Enterprise,BasePlatform模拟顶层项目,对外提供公共基础配置(common.properties)。Enterprise模拟实际的开发项目,不仅拥有私有的配置(enterprise-server.properties),还依赖了公共基础配置(common.properties)。

Enterprise项目在将第三方产品集成至Spring Boot中时,既可以使用@Configuration配置类,也可以使用*.xml的形式(比如spring-dao.xml)。

配置中允许出现加密字符串,假设形如 >>>param123<<<的配置项的值为加密后的字符串。

项目:
SpringBoot整合Apollo1.6.0 (做法+设计思路)_第1张图片
BasePlatform:

Enterprise:

二. 集成

2.1 依赖

maven:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.9.RELEASE</version>
</parent>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.23</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.2 Apollo启动配置

注入Apollo相关启动参数的方式多种多样,比如将配置写在application.properties中,或是写到jvm参数内。值得注意的是,后者的优先级比前者高,后者的配置将覆盖前者的配置。

  • application.properties
## 应用全局唯一的身份标识
app.id=Enterprise
# Apollo Meta Server 地址
apollo.meta=http://10.211.55.4:8080
# 自定义本地配置文件缓存路径
apollo.cacheDir=./config
# 设置在应用启动阶段就加载 Apollo 配置
apollo.bootstrap.enabled=true
# 注入当前项目需要的namespace  可以写多个,用英文逗号隔开即可 如 common,enterprise-server
apollo.bootstrap.namespaces=common
# 使Apollo的加载顺序放到日志系统加载之前
apollo.bootstrap.eagerLoad.enabled=false
  • jvm参数
-Dapp.id=Enterprise -Dapollo.meta=http://10.211.55.4:8080 -Dapollo.cacheDir=./config -Dapollo.bootstrap.enabled=true -Dapollo.bootstrap.eagerLoad.enabled=true -Dapollo.bootstrap.namespaces=common

2.3 Config工厂

public class ConfigFactory {
    private static volatile ConfigFactory configFactory = null;

    private ConcurrentHashMap<String, ApolloCustomConfig> map = new ConcurrentHashMap<>();

    private ConfigFactory() {

    }

    public static ConfigFactory getInstance() {
        if (configFactory == null) {
            synchronized (ConfigFactory.class) {
                if (configFactory == null) {
                    configFactory = new ConfigFactory();
                }
            }
        }
        return configFactory;
    }

    /**
     * 获取指定的配置项集合
     *
     * @param namespace 集合名称
     * @return 指定配置的集合
     */
    @Nullable
    public ApolloCustomConfig getConfig(@NotNull String namespace) {
        if (CollectionUtils.isEmpty(map)) {
            throw new RuntimeException("namespace not exist");
        }
        return map.get(namespace);
    }

    /**
     * 新增配置项集合
     *
     * @param namespace 配置项集合名称
     * @param config    配置项集合
     */
    public synchronized void addConfig(@NotEmpty String namespace, @NotNull ApolloCustomConfig config) {
        map.remove(namespace);
        map.put(namespace, config);
    }
}

2.4 Config 自定义扩展类

我对Config进行了扩展,实现了包括参数解密和解析引用参数的功能。

import com.apollo.factory.ConfigFactory;
import com.ctrip.framework.apollo.Config;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import sun.misc.BASE64Decoder;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
public class ApolloCustomConfig {
    private static final Logger logger = LoggerFactory.getLogger(ApolloCustomConfig.class);
    private static boolean decryptionEnabled;

    private Config config;

    private Config getConfig() {
        return config;
    }

    public ApolloCustomConfig(Config config) {
        // 解密参数是否开启
        decryptionEnabled = Boolean.parseBoolean(config.getProperty("apollo.items.decryption.enable", "true"));
        this.config = config;
    }

    /**
     * 获取配置
     *
     * @param key          配置项的名称
     * @param defaultValue 默认值
     * @return 配置项的实际值
     */
    public String getProperty(String key, String defaultValue) {
        String property = config.getProperty(key, defaultValue);
        if (decryptionEnabled && key!=null && property.startsWith(">>>") && property.endsWith("<<<")) {
            property = property.replaceAll(">>>", "").replaceAll("<<<", "");
            property = decrypt(property);
        }

        return property == null ? defaultValue : property;
    }

    /**
     * 解密
     *
     * @param cipherText 密文
     * @return 明文
     */
    public static String decrypt(String cipherText) {
        // 这里只是模拟解密的动作 我们可以自定义加解密的算法
        BASE64Decoder decoder = new BASE64Decoder();
        try {
            return new String(decoder.decodeBuffer(cipherText));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return cipherText;
    }

    /**
     * 获取配置 当前配置中含有${} 比如有配置: druid.datasource.url=jdbc:mysql://${mysql.ip}:${mysql.port}/db_name
     * 此处的${mysql.ip}和${mysql.port}来自于可能其它namespace
     *
     * @param key               配置项的名称
     * @param defaultValue      默认值
     * @param namespaceNames     占位符相关配置所在的namespace的名称
     * @return 配置项的实际值
     */
    public String getProperty(String key, String defaultValue, String... namespaceNames) {
        if(namespaceNames != null && namespaceNames.length>0) {
            List<ApolloCustomConfig> preferenceConfigs = new ArrayList<>();
            for(String namespaceName : namespaceNames) {
                preferenceConfigs.add(ConfigFactory.getInstance().getConfig(namespaceName));
            }
            return getProperty(key, defaultValue, preferenceConfigs);
        }
        return getProperty(key, defaultValue);
    }

    /**
     * 获取配置
     *
     * @param key               配置项的名称
     * @param defaultValue      默认值
     * @param preferenceConfigs 占位符相关配置所在的namespace配置对象
     * @return 配置项的实际值
     */
    private String getProperty(String key, String defaultValue, List<ApolloCustomConfig> preferenceConfigs) {
        // 含有占位符的配置项的值
        String unTranslateValue = getProperty(key, defaultValue);
        // 不含占位符的配置项的实际值
        String translateValue = unTranslateValue;

        if (unTranslateValue.contains("${")) {
            // 由于freeMaker会将A.B错误的解析成"A对象的B属性",因此提前将"."替换成"commacomma"
            // "commacomma"可以是任意值(注意: 应该避免在配置项的值中出现过,所以尽可能的选一个独一无二的值吧)
            unTranslateValue = unTranslateValue.replaceAll("\\.", "commacomma");
            StringWriter translateValueWriter = new StringWriter();
            try {
                Template template = new Template("template", new StringReader(unTranslateValue),
                        new Configuration(Configuration.VERSION_2_3_23));
                template.process(collectConfigMap(preferenceConfigs), translateValueWriter);
                translateValue = translateValueWriter.toString().replaceAll("commacomma", "\\.");
            } catch (TemplateException | IOException e) {
                log.error("翻译含有${xxx}的配置时出现异常");
            }
        }
        return translateValue;
    }

    /**
     * 将Config中的key和value取出,拼接成Map 方便freeMaker解析
     *
     * @param preferenceConfigs 占位符相关配置所在的namespace配置对象
     * @return <配置项的名称, 配置项的值>
     */
    private Map<String, String> collectConfigMap(List<ApolloCustomConfig> preferenceConfigs) {
        if (preferenceConfigs != null) {
            Map<String, String> map = new HashMap<>();
            for (ApolloCustomConfig preferenceConfig : preferenceConfigs) {
                // 获取传入Config下,所有配置项的名称
                Set<String> propertyNames = preferenceConfig.getConfig().getPropertyNames();
                if (!CollectionUtils.isEmpty(propertyNames)) {
                    map.putAll(propertyNames.stream().collect(Collectors.toMap(
                            e -> e.replaceAll("\\.", "commacomma"),
                            e -> preferenceConfig.getProperty(e, ""),
                            (e1, e2) -> e1)));
                }
            }
            return map;
        }

        return new HashMap<>(0);
    }
	
	public Integer  getIntProperty(String key, Integer defaultValue) {
        return config.getIntProperty(key, defaultValue);
    }

    public Long getLongProperty(String key, Long defaultValue) {
        return config.getLongProperty(key, defaultValue);
    }

    public Boolean getBooleanProperty(String key, Boolean defaultValue) {
        return config.getBooleanProperty(key, defaultValue);
    }
}

2.5 Apollo自定义配置类

import com.apollo.factory.ConfigFactory;
import com.apollo.spi.ApolloCustomConfig;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;

// 不对@EnableApolloConfig填写任何参数,则Apollo默认加载namespace为application
// 的配置信息。如果你没有为application添加任何配置信息,但在代码中却错误的使用了
// @EnableApolloConfig,那么在项目启动时,apollo会不断的加载application,报错并
// 抛出警告
@EnableApolloConfig({"common", "enterprise-server"})
@Configuration
public class ApolloConfiguration implements BeanFactoryPostProcessor, PriorityOrdered, EnvironmentAware {
    @Autowired
    ConfigurableEnvironment environment;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ConfigFactory configFactory = ConfigFactory.getInstance();

        // 启动时加载的配置 -Dapollo.bootstrap.namespaces=common
        // 因此可以获取到namespace为"common"的所有配置信息
        CompositePropertySource bootstrapSources = (CompositePropertySource) environment.getPropertySources().get("ApolloBootstrapPropertySources");
        if(bootstrapSources != null) {
            bootstrapSources.getPropertySources().forEach(e->{
                if("common".equals(e.getName())) {
                    configFactory.addConfig("common", new ApolloCustomConfig((Config)e.getSource()));
                }
            });
        }

        // 非启动时加载的配置 (当然了,这些配置一定在@EnableApolloConfig({})声明的配置之内)
        // 因此可以获取到namespace为"enterprise-server"的所有配置
        CompositePropertySource remoteSources = (CompositePropertySource) environment.getPropertySources().get("ApolloPropertySources");
        if(remoteSources != null) {
            remoteSources.getPropertySources().forEach(e->{
                if("enterprise-server".equals(e.getName())) {
                    configFactory.addConfig("enterprise-server", new ApolloCustomConfig((Config)e.getSource()));
                }
            });
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 100;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = (ConfigurableEnvironment)environment;
    }
}

2.6 业务代码中读取配置的方式

// 未加密 来自enterprise-server.properties
StringemployeeName = ConfigFactory.getInstance().getConfig("enterprise-server").getProperty("employee.name", "小李");
// 加密 来自enterprise-server.properties
Integer employeeAge = ConfigFactory.getInstance().getConfig("enterprise-server").getIntProperty("employee.age", 23);

// 未加密 来自common.properties
String mysqlPassword = ConfigFactory.getInstance().getConfig("common").getProperty("mysql.password.enterprise", "667788");

System.out.println(employeeName);//输出: 小马
System.out.println(employeeAge);//输出: 26
System.out.println(mysqlPassword);//输出: 123456

三. 设计思路

3.1 读取Apollo配置的思路

获取Apollo配置的方式多种多样。在阐述我自己的设计思路之前,先向大家抛砖引玉,给出两种实现方案。

方法1: 通过在配置类上加@Configuration和@EnableApolloConfig注解,接着在类中使用@Value()的方式获取配置项的值。

@EnableApolloConfig
@Configuration
public class Test {
    @Value("${param1}")
    private String param1;
    
    @Value("${param2}")
    private String param2;
}

方法2: 是@EnableApolloConfig与@ApolloConfig搭配使用。

@Configuration
@EnableApolloConfig
public class AppConfig {
  @Bean
  public TestApolloAnnotationBean testApolloAnnotationBean() {
    return new TestApolloAnnotationBean();
  }
}
public class TestApolloAnnotationBean {
  @ApolloConfig
  private Config config; //inject config for namespace application
  @ApolloConfig("application")
  private Config anotherConfig; //inject config for namespace application
  @ApolloConfig("FX.apollo")
  private Config yetAnotherConfig; //inject config for namespace FX.apollo
  @ApolloConfig("application.yml")
  private Config ymlConfig; //inject config for namespace application.yml
 
  /**
   * ApolloJsonValue annotated on fields example, the default value is specified as empty list - []
   * 
* jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}] */
@ApolloJsonValue("${jsonBeanProperty:[]}") private List<JsonBean> anotherJsonBeans; @Value("${batch:100}") private int batch; //config change listener for namespace application @ApolloConfigChangeListener private void someOnChange(ConfigChangeEvent changeEvent) { //update injected value of batch if it is changed in Apollo if (changeEvent.isChanged("batch")) { batch = config.getIntProperty("batch", 100); } } //config change listener for namespace application @ApolloConfigChangeListener("application") private void anotherOnChange(ConfigChangeEvent changeEvent) { //do something } //config change listener for namespaces application, FX.apollo and application.yml @ApolloConfigChangeListener({"application", "FX.apollo", "application.yml"}) private void yetAnotherOnChange(ConfigChangeEvent changeEvent) { //do something } //example of getting config from Apollo directly //this will always return the latest value of timeout public int getTimeout() { return config.getIntProperty("timeout", 200); } //example of getting config from injected value //the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above public int getBatch() { return this.batch; } private static class JsonBean{ private String someString; private int someInt; } }

我没有使用上述两种配置方式,而是新建自定义配置类,借助BeanFactoryPostProcessor,在Apollo从远端拉取配置信息并添加至Environment之后,PropertySourcesPlaceholderConfigurer初始化之前,将配置Config从Environment取出,加以改造,并放入工厂单例类的共享变量内。我这样做的原因有如下四点:

  1. 通过工厂模式+单例模式的形式对配置进行了统一的管理,毕竟即便是@Bean创建出的对象,通过@Autowired注入时,从整体系统层面看,也并非是单例的(Spring多容器下,每个容器中都存在这个对象)。
  2. 便于为Config对象扩展功能。直接通过@ApolloConfig(“xxxxx”)的方式获取Config,除非通过spi,重新实现Config,ConfigFactory,Injector,否则很难对Config进行扩展。因此,我选择了创建自己的Config,充分利用Apollo提供的Config,通过组合而非继承的方式实现扩展。(这样做也符合面向对象设计原则中的合成复用原则)
  3. 方便后期配置中心扩展,比如在未来项目可能会集成Nacos配置中心,如果使用@ApolloConfig(“xxxxx”)的方式获取Config,再通过Config getProperty()来获取配置,岂不是把代码都写死了,倘若配置需要转移到Nacos,势必要修改业务代码,这显然严重的违背开闭原则。而现在,我把配置放到了Config工厂,外部通过参数的方式获取目标配置,以后只需要修改工厂方法,在不改动业务代码的前提下,就能完成配置的转移了。
  4. 如果有其它不在Apollo中托管的配置信息需要在系统时注入到系统中,并且希望在@Value("${}")或spring.xml文件内的${}中生效,则只需要在自定义配置类中,提前将待托管的配置信息注入到environment中,那么在PropertySourcesPlaceholderConfigurer初始化时,其postProcessBeanFactory()方法内,environment对象就能读取到这些配置信息了。(可能的场景: 厂商甲负责的系统A在启动时,需要请求厂商乙负责的系统B,获取经过业务逻辑计算出的一些配置信息)

3.2 Apollo自定义配置类的设计思路

我在自定义的ApolloConfiguration类中实现了BeanFactoryPostProcessor、PriorityOrdered以及EnvironmentAware,那么为什么要实现这些接口呢,原因与Apollo在Spring初始化过程中所做的动作密切相关。

让我们直接从Spring的引导和启动方法 SpringApplication run()开始追踪吧。

SpringApplication

public ConfigurableApplicationContext run(String... args) {
        //记录程序运行时间
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        //Spring 应用的上下文
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        // 获取 SpringApplicationRunListeners
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            // 创建 ApplicationArguments 对象
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            // 加载属性配置
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
           ...
            // 应用上下文的准备阶段
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            // 刷新应用上下文(自动装配,初始化 IOC 容器)
            refreshContext(context);
            ...
        }
  }

重点关注加载属性配置prepareEnvironment()和准备应用上下文prepareContext()这两个方法。

  • prepareEnvironment(listeners,applicationArguments) 方法
private ConfigurableEnvironment prepareEnvironment(
            SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // 创建 ConfigurableEnvironment 对象
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 配置 ConfigurableEnvironment
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 发布 ApplicationEnvironmentPreparedEvent 事件
        listeners.environmentPrepared(environment);
        // 将 ConfigurableEnvironment 绑定到 SpringApplication 中
        bindToSpringApplication(environment);
        if (this.webApplicationType == WebApplicationType.NONE) {
            environment = new EnvironmentConverter(getClassLoader())
                    .convertToStandardEnvironmentIfNecessary(environment);
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
    }

prepareEnvironment()用来加载属性配置,执行完成后,所有的environment属性都会加载进来,不仅包括application.properties,还包括外部的扩展属性配置。

那么外部属性配置是如何加载的呢?这里利用了配置文件扩展接口EnvironmentPostProcessor,只要是实现了EnvironmentPostProcessor接口,并且在META-INF/spring.factories中申明了类路径的类,都属于外部的扩展属性配置。
大致的流程为:

  1. 创建ConfigurableEnvironment对象。
  2. 配置ConfigurableEnvironment environment变量。
  3. 发布 ApplicationEnvironmentPreparedEvent 事件。

对应的监听器是ConfigFileApplicationListener,当监听器接收到上述的事件后,会按照getOrder()的数值大小,依次加载并实例化外部的扩展属性配置类,并遍历调用这些类的postProcessEnvironment()方法。

值得注意的是,ConfigFileApplicationListener自己本身也实现了EnvironmentPostProcessor,同样会执行自身的postProcessEnvironment()方法,这个方法会从application.properties和jvm启动参数中读取配置信息并添加至environment,最后将environment绑定到Spring应用的上下文中。它的执行顺序等级为Ordered.HIGHEST_PRECEDENCE + 10。

  • ApolloApplicationContextInitializer

回到Apollo,在Apollo中有一个名为ApolloApplicationContextInitializer的类,它实现了postProcessEnvironment()方法。

public class ApolloApplicationContextInitializer implements
    ApplicationContextInitializer<ConfigurableApplicationContext> , EnvironmentPostProcessor, Ordered {
	 public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {

    // 初始化Apollo系统级别的配置,
    // 如appi.id,apollo.cluster,apollo.cacheDir, apollo.meta等等
    // 实际上就是我们在jvm中定义的那些apollo的基础参数
    initializeSystemProperty(configurableEnvironment);

	// 是否需要使Apollo的加载顺序放到日志系统加载之前
    Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);
    
    if (!eagerLoadEnabled) {
      return;
    }
	
	//是否需要在Spring的启动阶段就加载Apollo的相关配置到environment
    Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);

    if (bootstrapEnabled) {
      initialize(configurableEnvironment);
    }
  }
}

由于有ConfigFileApplicationListener的帮助,该方法可以从现有的envioronment中读取Apollo的系统级参数,并根据启动项判断是否需要在当前阶段加载Apollo相关配置信息到environment。

再来看看initialize()方法

  • initialize(ConfigurableEnvironment environment)
  protected void initialize(ConfigurableEnvironment environment) {

    // 如果environment中包含了ApolloBootstrapPropertySources (Apollo启动参数) 则表明已经做过初始化操作了
    if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
      //已经做过初始化操作了,直接返回即可
      return;
    }

    // 需要在系统启动阶段读取并加载配置的namespace名称集合
    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);
  }

看到ConfigService.getConfig(namespace)方法,可能产生一系列的疑惑:为什么ConfigService.getConfig就能读取到指定namespace的配置信息?Apollo是怎样将配置文件从远端拉取到本地的呢? 继续向下跟踪。

  • ConfigService getConfig(String namespace)
public static Config getConfig(String namespace) {
    return s_instance.getManager().getConfig(namespace);
  }
  • DefaultConfigManager getConfig(String namespace)
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;
  }

注意factory.create(namespace);

  • DefaultConfigFactory create(String namespace)
public Config create(String namespace) {
    ConfigFileFormat format = determineFileFormat(namespace);
    if (ConfigFileFormat.isPropertiesCompatible(format)) {
       // 配置文件类型是.YAML或者YML的初始化方法
      return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
    }
    // 其它类型的配置文件初始化方法
    return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
  }

由默认的ConfigFactory创建config,ConfigFactory在spi包下,意味着可以扩展。createPropertiesCompatibleFileConfigRepository暂时不去管,我们直接看到非YML或YAML的配置的初始化方法
createLocalConfigRepository()

  • DefaultConfigFactory createLocalConfigRepository(String namespace)
LocalFileConfigRepository createLocalConfigRepository(String namespace) {
    // 是否使用本地模式 
    // 本地模式意味着不去远端的Apollo服务端拉取配置文件至本地
    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);
    }
    // 远端模式 
    // 从远端的Apollo服务端拉取配置文件至本地
    return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
  }
  • DefaultConfigFactory createRemoteConfigRepository(String namespace)
 RemoteConfigRepository createRemoteConfigRepository(String namespace) {
    return new RemoteConfigRepository(namespace);
  }
  • RemoteConfigRepository RemoteConfigRepository(String namespace)
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);
    gson = new Gson();
    this.trySync();
    this.schedulePeriodicRefresh();
    this.scheduleLongPollingRefresh();
  }

关注trySync()方法

  • AbstractConfigRepository trySync()
 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()
protected synchronized void sync() {
    Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");

    try {
      ApolloConfig previous = m_configCache.get();
      // 加载远端Apollo配置
      ApolloConfig current = loadApolloConfig();
      ...
  }
  • RemoteConfigRepository loadApolloConfig()
private ApolloConfig loadApolloConfig() {
	...
	String appId = m_configUtil.getAppId();
    String cluster = m_configUtil.getCluster();
    String dataCenter = m_configUtil.getDataCenter();
    String secret = m_configUtil.getAccessKeySecret();
    ...
    url = assembleQueryConfigUrl(configService.getHomepageUrl(), appId, cluster, m_namespace,
                dataCenter, m_remoteMessages.get(), m_configCache.get());
    ...
    HttpRequest request = new HttpRequest(url);
    ...
    HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);             
}

看到这里明白了,借助ConfigFileApplicationListener,从environment中读取到Apollo的基本信息,接着拼成url,再根据app.id等信息,向Apollo服务端发起get请求,获取指定namespace的配置参数。

既然在SpringApplication的prepareEnvironment()配置准备阶段就已经把远端Apollo配置信息注入到了environment中,那接下来的获取动作就十分方便了。我们只需要在自己创建一个自定义的类上实现EnvironmentAware接口,并读取Spring上下文中的environment,便可以很方便的获取Apollo的配置了。

CompositePropertySource sources = (CompositePropertySource) environment.getPropertySources().get("");

Apollo提供了两种配置类型,分别是ApolloBootstrapPropertySources和ApolloPropertySources,前者是启动配置,后者
是非启动配置。ApolloBootstrapPropertySources内的内容实际上就是apollo.bootstrap.namespaces中定义的namespace对应的配置。那么没有定义在apollo.bootstrap.namespaces内,却在@EnableApolloConfig()内声明的namespace对应的配置显然存放在ApolloPropertySources了。

我之所以实现BeanFactoryPostProcessor接口,主要是想在SpringApplication run()内加载属性配置后的refreshContext()阶段,利用这个钩子,初始化自己的自定义配置类,并从environment中获取Apollo配置信息。

除此之外,Spring refreshContext()初始化类时首先会对实现了BeanFactoryPostProcessor的类进行分类,接着分别进行排序和初始化操作。

  • PostProcessorRegistrationDelegate registerBeanPostProcessors
public static void registerBeanPostProcessors(
			ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
	...		
	List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
	...
	// 首先注册实现了BeanPostProcessors且实现了PriorityOrdered的类,并进行排序 
	// 排序的规则按照getOrder()的大小,值越小,优先级越高  Integer.MIN_VALUE的优先级最高
	sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
	// registerBeanPostProcessors方法会遍历并触发这些类的创建
	registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);
	// 接着,注册实现了BeanPostProcessors且实现了Ordered的类,并进行排序
	...
}			

从PropertySourcesPlaceholderConfigurer的类图中,不难发现它实现了PriorityOrder,因此在被registerBeanPostProcessors()处理时,会被归类到priorityOrderedPostProcessors的队列中。为了抢在PropertySourcesPlaceholderConfigurer之前初始化,所以我对自定义配置类也实现了PriorityOrder,并调整排序的权重,使其优先级比PropertySourcesPlaceholderConfigurer要高。

你可能感兴趣的:(#,配置中心)