热门标签 | 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更简洁清晰,有兴趣的同学可以自行阅读代码。

文章最后,附图一张




推荐阅读
  • [转] JavaScript中in操作符(for..in)、Object.keys()和Object.getOwnPropertyNames()的区别
    ECMAScript将对象的属性分为两种:数据属性和访问器属性。每一种属性内部都有一些特性,这里我们只关注对象属性的[[Enumerable]]特征,它表示是否通过for-in循环 ... [详细]
  • 本文介绍了如何通过修改Android应用的配置文件和编写布局与Activity代码,利用DOM模式将用户输入的数据保存为XML文件。 ... [详细]
  • Shiro功能拓展:登录失败重试次数限制
    本文详细介绍了如何在Apache Shiro框架中实现对用户登录失败重试次数的限制,通过自定义密码匹配器来增强系统的安全性。该方法不仅能够有效防止暴力破解攻击,还能确保合法用户的账户安全。 ... [详细]
  • 本文档详细介绍了Robot Framework的基础知识、安装配置方法及其实用技巧。从环境搭建到编写第一个测试用例,涵盖了一系列实用的操作指南和最佳实践。 ... [详细]
  • 应用程序配置详解
    本文介绍了配置文件的关键特性及其在不同场景下的应用,重点探讨了Machine.Config和Web.Config两种主要配置文件的用途和配置方法。文章还详细解释了如何利用XML格式的配置文件来调整应用程序的行为,包括自定义配置、错误处理、身份验证和授权设置。 ... [详细]
  • 手把手教你构建简易JSON解析器
    本文将带你深入了解JSON解析器的构建过程,通过实践掌握JSON解析的基本原理。适合所有对数据解析感兴趣的开发者。 ... [详细]
  • 本文深入探讨了Java注解的基本概念及其在现代Java开发中的应用。文章不仅介绍了如何创建和使用自定义注解,还详细讲解了如何利用反射机制解析注解,以及Java内建注解的使用场景。 ... [详细]
  • Redis 教程01 —— 如何安装 Redis
    本文介绍了 Redis,这是一个由 Salvatore Sanfilippo 开发的键值存储系统。Redis 是一款开源且高性能的数据库,支持多种数据结构存储,并提供了丰富的功能和特性。 ... [详细]
  • 本文介绍了如何有效解决在Java编程中遇到的 'element cannot be mapped to a null key' 错误,通过具体的代码示例展示了问题的根源及解决方案。 ... [详细]
  • scrapyredis分布式爬虫 ... [详细]
  • 本文介绍如何创建一个简单的Android桌面小部件,通过显示两个文本框来展示基本功能。提供代码下载链接及详细步骤。 ... [详细]
  • 导读上一篇讲了zsh的常用字符串操作,这篇开始讲更为琐碎的转义字符和格式化输出相关内容。包括转义字符、引号、print、printf的使用等等。其中很多内容没有必要记忆,作为手册参 ... [详细]
  • ThinkPHP 文件缓存组件详解与应用
    本文深入探讨了ThinkPHP框架中的文件缓存类实现,提供了详细的代码示例和使用说明,旨在帮助开发者更好地理解和利用这一功能来优化应用程序性能。 ... [详细]
  • 本文介绍了 Python 中 *args 和 **kwargs 的使用方法,以及如何通过 lambda 表达式、map 和 filter 函数处理数据。同时,探讨了 enumerate 和 zip 函数的应用,并展示了如何使用生成器函数处理大数据集。 ... [详细]
  • 开发笔记:异步实时搜索jquery select插件
    开发笔记:异步实时搜索jquery select插件 ... [详细]
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社区 版权所有