Apollo:1.6.0,springboot:spring-boot-2.1.6.RELEASE,spring-cloud:2.1.1.RELEASE
qa环境:apollo修改分库分表配置未生效,排查原因是因为项目中application-qa.properties配置导致,小朋友你是不是有很多问号?
apollo/properties配置
my.list.key=1,2,3
spring读取配置
@Value("${my.list.key:0}"
private List<Integer> myListKey
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客户端配置流程
二者复用同一处代码如下
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类型返回。例如:
// 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);
}
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);
}
阿波罗配置
spring配置(例如:application.properties)
ApolloApplicationContextInitializer与ConfigFileApplicationListener二者的postProcessEnvironment优先级,兜底ConfigFileApplicationListener更高(见下图)
那么阿波罗配置与application-qa.properties配置的优先级谁高谁低?
因为spring-cloud默认会启动bootstrap应用上下文,启动时spring配置的兜底名称为bootstrap,而我们项目的配置是application-qa.properties,因此有如下逻辑:
破案还算顺利_很久没有接触web上下文环境了,总结下spring-cloud-context-2.1.1.RELEASE中web上下文启动流程简述
注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逻辑