热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

SpringBoot源码解析Logging,Environment启动

本文通过阅读SpringBoot源码,分享SpringBoot中Logging,Environme

本文通过阅读SpringBoot源码,分享SpringBoot中Logging,Environment组件的启动过程。

如果大家在使用SpringBoot过程中,遇到日志配置无效,Environment中获取属性错误,希望本文可以给你们一个解决问题的思路。
源码分析基于spring boot 2.1
(源码解析类文章建议在PC端阅读)

Logging

Logging组件通过ApplicationListener启动,对应的处理类为LoggingApplicationListener(spring-boot.jar中的spring.factories配置了)
LoggingApplicationListener#onApplicationStartingEvent

private void onApplicationStartingEvent(ApplicationStartingEvent event{
    // #1
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());  
    this.loggingSystem.beforeInitialize();
}

#1
根据应用引入的日志框架,加载对应的日志框架LoggingSystem。
LoggingSystem#get

public static LoggingSystem get(ClassLoader classLoader) {
    String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
    if (StringUtils.hasLength(loggingSystem)) {
        if (NONE.equals(loggingSystem)) {
            return new NoOpLoggingSystem();
        }
        return get(classLoader, loggingSystem);
    }
    return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader)) // #1
            .map((entry) -> get(classLoader, entry.getValue())).findFirst() // #2
            .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));    
}

#1
LoggingSystem#SYSTEMS中存放了SpringBoot中Logback,Log4j2,Java Util Logging几个日志框架适配器的路径,检查这些类适配器是否存在以判断这些日志框架是否引入。
#2
构造LoggingSystem,取第一个结果。

LoggingApplicationListener#initialize

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    new LoggingSystemProperties(environment).apply();
    // #1
    this.logFile = LogFile.get(environment);    
    if (this.logFile != null) {
        this.logFile.applyToSystemProperties();
    }
    // #2
    initializeEarlyLoggingLevel(environment);   
    // #3
    initializeSystem(environment, this.loggingSystem, this.logFile);
    // #4   
    initializeFinalLoggingLevels(environment, this.loggingSystem);  
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

#1
从environment读取logging.file,logging.path配置,构建LogFile
#2
读取Environment中debug,trace的配置到springBootLogging属性
#3

(1) Environment中存在logging.config配置,使用该配置读取配置文件,初始化LoggingSystem。否则执行(2)步骤
(2) 存在logging.file或logging.path,使用#1
中构造的LogFile读取对应的配置文件,初始化LoggingSystem。否则执行(3)步骤
(3) 如果能从默认配置路径中读取日志配置文件,则初始化LoggingSystem。否则执行(4)步骤
每个日志框架默认路径不同,如Logback会查找如下路径
logback-test.groovy, logback-test.xml, logback.groovy, logback.xml
logback-test-spring.groovy, logback-test-spring.xml, logback-spring.groovy, logback-spring.xml
(4) 使用默认方案,不同日志框架默认处理方案也不同,SpringBoot中默认使用Logback,该日志框架从Environment中获取logging.pattern.level,logging.pattern.dateformat配置,用于初始化LoggingSystem。
#4
使用#1
步骤加载的springBootLogging属性设置LoggingLevel,并处理logging.group配置的LoggingLevel。
这里的日志级别会覆盖#3
步骤中配置文件的日志级别。

Environment

Environment代表当前应用运行环境,管理配置属性数据,并提供Profile特性,即可以根据环境得到相应配置属性数据。

Environment的查询
Environment的实现类都继承了AbstractEnvironment,
AbstractEnvironment#propertySources是一个MutablePropertySources,它实际上是一个属性源PropertySources列表。
AbstractEnvironment#propertyResolver是一个属性解析器ConfigurablePropertyResolver,负责从属性源中查询对应属性。
AbstractEnvironment也实现了PropertyResolver。

AbstractEnvironment#getProperty -> PropertySourcesPropertyResolver#getProperty

protected  getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders{
    if (this.propertySources != null) {
        // #1   
        for (PropertySource propertySource : this.propertySources) {
            ...
            Object value = propertySource.getProperty(key);
            if (value != null) {
                if (resolveNestedPlaceholders && value instanceof String) {
                    // #2
                    value = resolveNestedPlaceholders((String) value);  
                }
                logKeyFound(key, propertySource, value);
                // #3
                return convertValueIfNecessary(value, targetValueType); 
            }
        }
    }
    ...
    return null;
}

#1
遍历所有的属性源propertySources
#2
 嵌套解析属性值(属性值可以使用占位符引用其他属性值)
#3
类型转换,将配置属性转换为对应的类型
可以看到,属性源PropertySource的顺序很重要,如果多个PropertySource存在同样的属性,只有前面PropertySource的属性值生效。
如果你发现查询到的属性值不是自己设置的值,可能属性被覆盖了。

Environment的构造
SpringApplication#prepareEnvironment负责构造Environment

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments)
 
{
    // #1
    ConfigurableEnvironment environment = getOrCreateEnvironment(); 
    // #2
    configureEnvironment(environment, applicationArguments.getSourceArgs());    
    // #3
    ConfigurationPropertySources.attach(environment);   
    // #4
    listeners.environmentPrepared(environment); 
    // #5
    bindToSpringApplication(environment);   
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    // #6
    ConfigurationPropertySources.attach(environment);   
    return environment;
}

#1
根据Application的环境构造对应的Environment,SERVLET应用,使用的是StandardServletEnvironment
#2
使用SpringApplication#run方法的args参数构造属性源SimpleCommandLinePropertySource
#3
添加数据源ConfigurationPropertySourcesPropertySource
#4
触发ApplicationEnvironmentPreparedEvent事件,负责处理该事件的ApplicationListener也会添加PropertySource
#5
通过Binder机制,将属性源中spring.main开头的属性绑定到SpringApplication的属性上
#6
重新构造ConfigurationPropertySourcesPropertySource

AbstractEnvironment的构造函数会调用AbstractEnvironment#customizePropertySources方法,该方法可以调整AbstractEnvironment#propertySources的内容。我们依次看一下各个AbstractEnvironment子类的customizePropertySources方法。
StandardServletEnvironment#customizePropertySources

protected void customizePropertySources(MutablePropertySources propertySources) {
    // #1
    propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));   
    // #2
    propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));  
    if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {  
        propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME)); // #3
    }
    super.customizePropertySources(propertySources);
}

#1
添加属性源servletConfigInitParams,通过ServletConfig#getInitParameter()读取属性值
#2
添加属性源servletContextInitParams,通过ServletContext#getInitParameter()读取属性值
#3
如果在JNDI环境中,添加添加属性源jndiProperties
这里servletConfigInitParams,servletContextInitParams的StubPropertySource只是在添加属性源列表中占位,StandardServletEnvironment#initPropertySources方法才将其替换为真正的PropertySource。

StandardEnvironment#customizePropertySources

protected void customizePropertySources(MutablePropertySources propertySources) {
    // #1
    propertySources.addLast(
            new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));   
    // #2           
    propertySources.addLast(
            new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));  
}

#1
添加属性源systemProperties,通过System.getProperties()读取属性值
#2
添加属性源systemEnvironment,通过System.getenv()读取属性值

还有一个很重要的,SpringApplication#prepareEnvironment方法#2
步骤 -> SpringApplication#configureEnvironment -> SpringApplication#configurePropertySources,将添加一个SimpleCommandLinePropertySource到属性源列表最开始位置,该属性源读取SpringBoot启动命令行参数,此时它的优先级最高。

SpringApplication#prepareEnvironment方法#3
步骤,添加一个ConfigurationPropertySourcesPropertySource到属性源列表开始位置,这个类实际上也是一个属性源集合,它将Environment中所有其他属性源转化为ConfigurationPropertySource并作为自己的属性源。ConfigurationPropertySource是一个特殊属性源,它查询属性的结果都是ConfigurationProperty,ConfigurationProperty是对属性数据的封装,包含了name,value,origin。
我们查询到到属性大部分都是通过ConfigurationPropertySourcesPropertySource查到的(它已经包含了servletConfigInitParams,servletContextInitParams,systemProperties,systemEnvironment等属性源)。

最后添加的是最常用的properties,yml等配置文件的属性源。他们是通过ConfigFileApplicationListener添加的。ConfigFileApplicationListener是一个ApplicationListener,处理ApplicationEnvironmentPreparedEvent事件。
ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent加载所有的EnvironmentPostProcessor,调用EnvironmentPostProcessor#postProcessEnvironment完成该工作。
而ConfigFileApplicationListener也是一个EnvironmentPostProcessor,
ConfigFileApplicationListener#postProcessEnvironment -> ConfigFileApplicationListener#addPropertySources -> Loader#load

public void load() {
    this.profiles = new LinkedList<>();
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>();
    // #1
    initializeProfiles();   
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        // #2
        if (profile != null && !profile.isDefaultProfile()) {
            addProfileToEnvironment(profile.getName()); 
        }
        // #3
        load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); 
        this.processedProfiles.add(profile);    
    }
    // #4
    resetEnvironmentProfiles(this.processedProfiles);   
    // #5
    load(nullthis::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));    
    addLoadedPropertySources(); // #6
}

#1
添加初始的profile,包括null(读取所有配置文件)和default
#2
添加profile到Environment中
#3
加载配置文件以及配置文件中配置的profile
#4
使用上一步加载到的profile重置Environment的Profiles
#5
使用Environment的Profiles再次加载配置文件
#6
将前面加载的属性源添加到Environment属性源列表末尾

#3
步骤,在spring.config.location配置的目录下,使用PropertySourceLoader加载不同的配置文件(从spring.factories中获取PropertySourceLoader的实现类),默认有PropertiesPropertySourceLoader,YamlPropertySourceLoader,可以读取properties,xml,yml,yaml格式的配置文件。
注意:这里最终调用loadForFileExtension,该方法根据profile加载对应的配置文件

另外,SystemEnvironmentPropertySourceEnvironmentPostProcessor也是一个EnvironmentPostProcessor,它会使用OriginAwareSystemEnvironmentPropertySource替换原来的systemEnvironment,该类可以适配spring.profiles.active,spring_profiles_active,spring-profiles-active等属性名,使他们可以查询到SPRING_PROFILES_ACTIVE的系统属性。

常用属性源优先级:SpringBoot启动命令行参数 > ServletConfig/ServletContext > 系统属性 > properties,yml等配置文件

最后,说一下Binder机制,它是从SpringBoot 2开始提供的功能,负责处理对象与ConfigurationPropertySource之间的绑定,并且可以方便地进行类型转换,以及提供回调方法介入绑定的各个阶段。
看一个Binder的最简单用法,properties文件如下

redis.host=127.0.0.1
redis.port=637

使用Binder绑定属性

RedisConfig config = Binder.get(context.getEnvironment())
                .bind("redis", Bindable.of(RedisConfig.class))
                .get();

这样就可以将properties配置文件的属性绑定到RedisConfig#host,RedisConfig#port属性中,

@ConfigurationProperties也是通过Binder机制实现。
@EnableConfigurationProperties使@ConfigurationProperties生效,它通过@Import引入了EnableConfigurationPropertiesRegistrar,
EnableConfigurationPropertiesRegistrar实现了ImportBeanDefinitionRegistrar,向Spring上下文注册了ConfigurationPropertiesBindingPostProcessor,BoundConfigurationProperties,ConfigurationPropertiesBeanDefinitionValidator,ConfigurationBeanFactoryMetadata等功能类。
而ConfigurationPropertiesBindingPostProcessor会对@ConfigurationProperties标注的类,使用ConfigurationPropertiesBinder将配置属性数据与bean属性绑定,ConfigurationPropertiesBinder最终使用Binder对象来完成工作。

EnableConfigurationPropertiesRegistrar是SpringBoot2.2开始使用的类,比SpringBoot2.1使用的EnableConfigurationPropertiesImportSelector更简洁清晰,有兴趣的同学可以自行阅读代码。

文章最后,附图一张




推荐阅读
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • web.py开发web 第八章 Formalchemy 服务端验证方法
    本文介绍了在web.py开发中使用Formalchemy进行服务端表单数据验证的方法。以User表单为例,详细说明了对各字段的验证要求,包括必填、长度限制、唯一性等。同时介绍了如何自定义验证方法来实现验证唯一性和两个密码是否相等的功能。该文提供了相关代码示例。 ... [详细]
  • 本文介绍了Android中的assets目录和raw目录的共同点和区别,包括获取资源的方法、目录结构的限制以及列出资源的能力。同时,还解释了raw目录中资源文件生成的ID,并说明了这些目录的使用方法。 ... [详细]
author-avatar
2yuheng
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有