SpringBoot源码解析 -- Logging,Environment启动

SpringBoot深入理解 -- @AliasFor注解的作用
SpringBoot源码解析 -- SpringBoot启动过程
SpringBoot源码解析 -- AutoConfigure的实现原理
SpringBoot源码解析 -- @ComponentScan的实现原理
SpringBoot源码解析 -- @Value,@Autowired实现原理
SpringBoot源码解析 -- Tomcat,SpringMVC启动
SpringBoot源码解析 -- Logging,Environment启动

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

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

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 <T> T getProperty(String key, Class<T> 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(null, this::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更简洁清晰,有兴趣的同学可以自行阅读代码。

如果您觉得本文不错,欢迎关注我的微信公众号,您的关注是我坚持的动力!