Apollo:1.6.0,springboot:spring-boot-2.1.6.RELEASE,spring-cloud:2.1.1.RELEASE
问题背景
qa环境:apollo修改分库分表配置未生效,排查原因是因为项目中application-qa.properties配置导致,小朋友你是不是有很多问号?
- apollo的配置优先级低于application的配置?
apollo配置简介
list类型配置
apollo/properties配置
my.list.key=1,2,3
spring读取配置
@Value("${my.list.key:0}"
private List<Integer> myListKey
map类型配置
apollo/properties配置
my.map.key={key:‘value’}
上面的配置常规情况下没问题,但是可能会遇到中文问题报错如下
因此推荐下面这种配置方式
my.map={“中文key”: “value”, }
spring读取配置
@Value("#{${my.map:null}}")
private Map<String, String> myMap;
@Value("#{${my.map:{\"05\":\"myValue\"}}}")
private Map<String, String> myMap;
优先级学习
很简单,写一个简单的测试用例注入Environment对象排查,或者在项目启动时增加注入等方式,进行断点排查
@RunWith(SpringRunner.class)
@SpringBootTest
public class UnitTest {
@Resource
private Environment environment;
@Test
public void test() {
System.out.println("test");
}
}
断点发现apollo配置的优先级时高于application配置的
问题排查
该问题出现其实是与我们当前项目环境相关,我们项目中使用自己封装的orm客户端。orm客户端的配置是如何读取配置?
orm客户端配置流程
- springboot自动配置启动时初始化配置OrmAutoConfigure
- 实现spring应用事件监听ApplicationListener,当收到配置发生变更事件(ConfigChangeEvent)时刷新配置
二者复用同一处代码如下
public InfraOrmConfig convert() {
InfraOrmConfig infraORMConfig = new InfraOrmConfig();
if (!PropertyUtil.containPropertyPrefix(environment, INFRA_ORM_PREFIX)) {
throw new OrmException("no infra orm config provider");
}
// handle代码将my.orm.biz0.config组装为Map<biz0,config>类型返回。例如:
// my.orm.biz0.config
// my.orm.biz1.config
// 返回结果 {biz0:config,biz1:config}
PropertyUtil.handle(environment, INFRA_ORM_PREFIX, Map.class)
.forEach((businessName, config) -> {
LinkedHashMap<String, String> nodeConfig = (LinkedHashMap<String, String>) config;
infraORMConfig.getConfigMap().put((String) businessName, parse((String) businessName, nodeConfig));
});
return infraORMConfig;
}
工具类handle代码如下,其实是通过反射的方式将配置组装为map,我们关心的优先级逻辑Binder(org.springframework.boot.context.properties.bind.Binder)
public static Object handle(final Environment environment, final String prefix, final Class<?> targetClass) {
Class<?> binderClass = Class.forName("org.springframework.boot.context.properties.bind.Binder");
Method getMethod = binderClass.getDeclaredMethod("get", Environment.class);
Method bindMethod = binderClass.getDeclaredMethod("bind", String.class, Class.class);
Object binderObject = getMethod.invoke(null, environment);
String prefixParam = prefix.endsWith(".") ? prefix.substring(0, prefix.length() - 1) : prefix;
Object bindResultObject = bindMethod.invoke(binderObject, prefixParam, targetClass);
Method resultGetMethod = bindResultObject.getClass().getDeclaredMethod("get");
return resultGetMethod.invoke(bindResultObject);
}
配置绑定Binder
org.springframework.boot.context.properties.bind.Binder,绑定类不详细讲,上面的工具类中可以看到入口是bind方法,实际底层查找配置的方法是findProperty,可以看到逻辑很简单,遍历查找配置,也就是说列表下标靠前的配置优先级更高
private ConfigurationProperty findProperty(ConfigurationPropertyName name, Context context) {
if (name.isEmpty()) {
return null;
}
for (ConfigurationPropertySource source : context.getSources()) {
ConfigurationProperty property = source.getConfigurationProperty(name);
if (property != null) {
return property;
}
}
return null;
}
问题来了,按照Binder的配置获取逻辑应该优先读到apollo配置才对啊?
问题原因
断点看下咯,问题原因出现了,与前面的截图比对可以发现,同一个environment对象但是propertySourceList配置列表的顺序不一致,导致二者对同一个key读取的配置结果不同。可以本地注入@Value(my.orm.config)对比框架中的ormConfig,验证结果确实不一致
orm客户端是在BeanDefinition注册时通过Environment回调获取的spring运行环境中的配置,此时阿波罗优先级低于application-qa.properties,原因已经定位,那么二者的配置优先级与我们看到的情况一致吗?为什么会出现优先级的变更?
配置加载
配置优先级变更原因
对比两张截图差别在于前者多了一个配置ApolloPropertySources,也就是说添加该配置时,优先级发生了变化,查看该配置添加的处理类:com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#initializePropertySources,发现确实阿波罗在该处理类中作了优先级处理来保障ApolloBootstrapPropertySources的优先级仍然为第一,代码如下
// add after the bootstrap property source or to the first
if (environment.getPropertySources()
.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
// ensure ApolloBootstrapPropertySources is still the first
ensureBootstrapPropertyPrecedence(environment);
environment.getPropertySources()
.addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
} else {
environment.getPropertySources().addFirst(composite);
}
配置加载时机
阿波罗配置
- Environment准备完成时:com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#postProcessEnvironment,前提是阿波罗开启了饥饿加载,配置key(APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED = “apollo.bootstrap.eagerLoad.enabled”)
- Environment准备完成后,应用上下文初始化时(刷新上下文之前):com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)
spring配置(例如:application.properties)
- Environment准备完成时回调postProcessEnvironment:org.springframework.boot.context.config.ConfigFileApplicationListener#postProcessEnvironment
ApolloApplicationContextInitializer与ConfigFileApplicationListener二者的postProcessEnvironment优先级,兜底ConfigFileApplicationListener更高(见下图)
配置加载方式
- 阿波罗:添加至头部
- spring
- 如果只有一个PropertySource
- 存在defaultProperties则添加至defaultProperties之前
- 否则添加至尾部
- 如果存在多个PropertySource,第二个PropertySource之后均紧随其后
- 如果只有一个PropertySource
问题流程梳理
那么阿波罗配置与application-qa.properties配置的优先级谁高谁低?
因为spring-cloud默认会启动bootstrap应用上下文,启动时spring配置的兜底名称为bootstrap,而我们项目的配置是application-qa.properties,因此有如下逻辑:
- bootstrap加载spring配置,不存在defaultProperties,因此加载至尾部,配置名称为bootstrap[-qa].properties,随后将bootstrap配置与默认配置合并为defaultProperties
- postProcessEnvironment加载阿波罗配置,默认非饥饿加载,不处理
- initialize加载阿波罗配置,加载配置至头部
- 阿波罗bean后置处理(PropertySourcesProcessor),确保阿波罗配置为头部,bootstrap未注册该bean,因此不会走此逻辑
- 实际应用application加载spring配置,存在defaultProperties,因此加载至defaultProperties之前
- 实际应用再次触发回调:postProcessEnvironment加载阿波罗配置,默认非饥饿加载,不处理
- 回调初始化对象,排序后最高优先级初始化对象:AncestorInitializer,合并bootstrap上下文中的Environment至应用上下文尾部,即阿波罗配置被追加至了尾部
- 实际应用再次触发回调:initialize加载阿波罗配置,已存在配置,不重新加载直接返回
- orm客户端自动配置InfraOrmAutoConfigure,回调setEnvironment,读取Environment初始化orm相关对象(此时阿波罗的优先级低于应用配置)
- 实际应用再次触发回调:阿波罗bean后置处理(PropertySourcesProcessor),确保阿波罗配置为头部,修改阿波罗配置优先级为第一优先
总结
- 阿波罗在spring上下文刷新前加载配置至Environment环境配置的第一优先级位置:com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)
- 阿波罗在spring BeanFactory后置动作回调时再次确认阿波罗配置优先级为第一优先级(com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#postProcessBeanFactory),如果不是第一优先级则调整为第一优先级
破案还算顺利_很久没有接触web上下文环境了,总结下spring-cloud-context-2.1.1.RELEASE中web上下文启动流程简述
spring-cloud启动流程
- SpringApplication.run执行,configName=兜底application,应用sources为项目中定义的Application类
- prepareEnvironment,此时创建环境为StandardServletEnvironment
- 回调environmentPrepared广播事件org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
- BootstrapApplicationListener监听器监听到ApplicationEnvironmentPreparedEvent事件org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent,兜底bootstrap为启用(spring.cloud.bootstrap.enabled=true),启动bootstrap应用上下文(spring-cloud-context-2.1.1.RELEASE-sources.jar!/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java:203)
- SpringApplication.run再次执行,非web环境,configName=兜底bootstrap(见下方:注1),应用sources为BootstrapImportSelectorConfiguration类(此类不包含bean扫描注解,仅处理自定义的类定义:BootstrapImportSelector)
- prepareEnvironment,此时创建环境为StandardEnvironment
- 刷新上下文
- 添加Ancestor初始化对象(使bootstrap上下文成为应用程序上下文的父上下文):org.springframework.cloud.bootstrap.BootstrapApplicationListener#addAncestorInitializer,如果应用程序上下文存在AncestorInitializer对象,则将其parent指向bootstrap上下文,否则为其新增AncestorInitializer对象(parent指向bootstrap上下文)
- 合并默认配置merge为defaultProperties(org.springframework.cloud.bootstrap.BootstrapApplicationListener#mergeDefaultProperties)
- 初始化回调(AncestorInitializer):reorderSources重排序Sources,将应用上下文的parent设置为bootstrap(org.springframework.context.support.GenericApplicationContext#setParent->org.springframework.context.support.AbstractApplicationContext#setParent),如果父上下文Environment对象为ConfigurableEnvironment类型,则合并org.springframework.core.env.AbstractEnvironment#merge:将父ConfigurableEnvironment追加至当前Environment尾部,并追加父上下文中的activeProfiles/defaultProfiles配置
- 刷新上下文
注1:spring-cloud兜底configName配置org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent
String configName = environment
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
思考
阿波罗的上下文初始化时的处理逻辑是否可以优化?在上下文初始化时(com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext))如果发现已存在配置,也应该确认其配置是否第一优先级,复用com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#ensureBootstrapPropertyPrecedence逻辑