apollo分布式配置优先级学习

Apollo:1.6.0,springboot:spring-boot-2.1.6.RELEASE,spring-cloud:2.1.1.RELEASE

问题背景

qa环境:apollo修改分库分表配置未生效,排查原因是因为项目中application-qa.properties配置导致,小朋友你是不是有很多问号?

  1. 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’}

上面的配置常规情况下没问题,但是可能会遇到中文问题报错如下1

因此推荐下面这种配置方式

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配置的
2

问题排查

该问题出现其实是与我们当前项目环境相关,我们项目中使用自己封装的orm客户端。orm客户端的配置是如何读取配置?
orm客户端配置流程

  1. springboot自动配置启动时初始化配置OrmAutoConfigure
  2. 实现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,验证结果确实不一致
3

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);
}

配置加载时机

阿波罗配置

  1. Environment准备完成时:com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#postProcessEnvironment,前提是阿波罗开启了饥饿加载,配置key(APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED = “apollo.bootstrap.eagerLoad.enabled”)
  2. Environment准备完成后,应用上下文初始化时(刷新上下文之前):com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)

spring配置(例如:application.properties)

  1. Environment准备完成时回调postProcessEnvironment:org.springframework.boot.context.config.ConfigFileApplicationListener#postProcessEnvironment

ApolloApplicationContextInitializer与ConfigFileApplicationListener二者的postProcessEnvironment优先级,兜底ConfigFileApplicationListener更高(见下图)4

配置加载方式

  1. 阿波罗:添加至头部
  2. spring
    1. 如果只有一个PropertySource
      1. 存在defaultProperties则添加至defaultProperties之前
      2. 否则添加至尾部
    2. 如果存在多个PropertySource,第二个PropertySource之后均紧随其后

问题流程梳理

那么阿波罗配置与application-qa.properties配置的优先级谁高谁低?
因为spring-cloud默认会启动bootstrap应用上下文,启动时spring配置的兜底名称为bootstrap,而我们项目的配置是application-qa.properties,因此有如下逻辑:

  1. bootstrap加载spring配置,不存在defaultProperties,因此加载至尾部,配置名称为bootstrap[-qa].properties,随后将bootstrap配置与默认配置合并为defaultProperties
  2. postProcessEnvironment加载阿波罗配置,默认非饥饿加载,不处理
  3. initialize加载阿波罗配置,加载配置至头部
  4. 阿波罗bean后置处理(PropertySourcesProcessor),确保阿波罗配置为头部,bootstrap未注册该bean,因此不会走此逻辑
  5. 实际应用application加载spring配置,存在defaultProperties,因此加载至defaultProperties之前
  6. 实际应用再次触发回调:postProcessEnvironment加载阿波罗配置,默认非饥饿加载,不处理
  7. 回调初始化对象,排序后最高优先级初始化对象:AncestorInitializer,合并bootstrap上下文中的Environment至应用上下文尾部,即阿波罗配置被追加至了尾部
  8. 实际应用再次触发回调:initialize加载阿波罗配置,已存在配置,不重新加载直接返回
  9. orm客户端自动配置InfraOrmAutoConfigure,回调setEnvironment,读取Environment初始化orm相关对象(此时阿波罗的优先级低于应用配置)
  10. 实际应用再次触发回调:阿波罗bean后置处理(PropertySourcesProcessor),确保阿波罗配置为头部,修改阿波罗配置优先级为第一优先

总结

  1. 阿波罗在spring上下文刷新前加载配置至Environment环境配置的第一优先级位置:com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)
  2. 阿波罗在spring BeanFactory后置动作回调时再次确认阿波罗配置优先级为第一优先级(com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#postProcessBeanFactory),如果不是第一优先级则调整为第一优先级

破案还算顺利_很久没有接触web上下文环境了,总结下spring-cloud-context-2.1.1.RELEASE中web上下文启动流程简述

spring-cloud启动流程

  1. SpringApplication.run执行,configName=兜底application,应用sources为项目中定义的Application类
  2. prepareEnvironment,此时创建环境为StandardServletEnvironment
  3. 回调environmentPrepared广播事件org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
  4. 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)
    1. SpringApplication.run再次执行,非web环境,configName=兜底bootstrap(见下方:注1),应用sources为BootstrapImportSelectorConfiguration类(此类不包含bean扫描注解,仅处理自定义的类定义:BootstrapImportSelector)
    2. prepareEnvironment,此时创建环境为StandardEnvironment
    3. 刷新上下文
    4. 添加Ancestor初始化对象(使bootstrap上下文成为应用程序上下文的父上下文):org.springframework.cloud.bootstrap.BootstrapApplicationListener#addAncestorInitializer,如果应用程序上下文存在AncestorInitializer对象,则将其parent指向bootstrap上下文,否则为其新增AncestorInitializer对象(parent指向bootstrap上下文)
    5. 合并默认配置merge为defaultProperties(org.springframework.cloud.bootstrap.BootstrapApplicationListener#mergeDefaultProperties)
  5. 初始化回调(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配置
  6. 刷新上下文

注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逻辑

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值