• 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
    
    • 1
    • 2

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    优先级学习

    很简单,写一个简单的测试用例注入Environment对象排查,或者在项目启动时增加注入等方式,进行断点排查

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UnitTest {
    
        @Resource
        private Environment environment;
    
        @Test
        public void test() {
            System.out.println("test");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    断点发现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类型返回。例如:
        // 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    工具类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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    配置绑定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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    问题来了,按照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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    配置加载时机

    阿波罗配置

    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}");
    
    • 1
    • 2

    思考

    阿波罗的上下文初始化时的处理逻辑是否可以优化?在上下文初始化时(com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext))如果发现已存在配置,也应该确认其配置是否第一优先级,复用com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor#ensureBootstrapPropertyPrecedence逻辑

  • 相关阅读:
    【原创】浅谈指针(十二)关于static(上)
    磁通量概述
    分析逆向案例九——奥鹏教育教师登录密码加密
    利用epoll创建自己的后台服务,实现对各个客户端网络通信(含示例代码)
    【机器学习】Python常见用法汇总
    大数据培训CombineTextInputFormat案例实操
    nodejs篇 内置模块events 常用api
    【论文阅读】Progressive Spatio-Temporal Prototype Matching for Text-Video Retrieval
    Leetcode219. 存在重复元素 II
    2310D必须在构造器中初化嵌套构的字段
  • 原文地址:https://blog.csdn.net/u010597819/article/details/127831248