本文记录了Spring Boot与Apollo的整合方式,Spring boot的版本为2.1.9.RELEASE,Apollo的版本为1.6.0。
以下测试案例中,我在Apollo内创建了两个项目,分别是BasePlatform和Enterprise,BasePlatform模拟顶层项目,对外提供公共基础配置(common.properties)。Enterprise模拟实际的开发项目,不仅拥有私有的配置(enterprise-server.properties),还依赖了公共基础配置(common.properties)。
Enterprise项目在将第三方产品集成至Spring Boot中时,既可以使用@Configuration配置类,也可以使用*.xml的形式(比如spring-dao.xml)。
配置中允许出现加密字符串,假设形如 >>>param123<<<的配置项的值为加密后的字符串。
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>
注入Apollo相关启动参数的方式多种多样,比如将配置写在application.properties中,或是写到jvm参数内。值得注意的是,后者的优先级比前者高,后者的配置将覆盖前者的配置。
## 应用全局唯一的身份标识
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
-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
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);
}
}
我对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);
}
}
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;
}
}
// 未加密 来自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
获取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取出,加以改造,并放入工厂单例类的共享变量内。我这样做的原因有如下四点:
我在自定义的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()这两个方法。
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中申明了类路径的类,都属于外部的扩展属性配置。
大致的流程为:
对应的监听器是ConfigFileApplicationListener,当监听器接收到上述的事件后,会按照getOrder()的数值大小,依次加载并实例化外部的扩展属性配置类,并遍历调用这些类的postProcessEnvironment()方法。
值得注意的是,ConfigFileApplicationListener自己本身也实现了EnvironmentPostProcessor,同样会执行自身的postProcessEnvironment()方法,这个方法会从application.properties和jvm启动参数中读取配置信息并添加至environment,最后将environment绑定到Spring应用的上下文中。它的执行顺序等级为Ordered.HIGHEST_PRECEDENCE + 10。
回到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()方法
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是怎样将配置文件从远端拉取到本地的呢? 继续向下跟踪。
public static Config getConfig(String namespace) {
return s_instance.getManager().getConfig(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);
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()
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));
}
RemoteConfigRepository createRemoteConfigRepository(String namespace) {
return new RemoteConfigRepository(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()方法
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;
}
protected synchronized void sync() {
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
try {
ApolloConfig previous = m_configCache.get();
// 加载远端Apollo配置
ApolloConfig current = 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的类进行分类,接着分别进行排序和初始化操作。
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要高。