本文通过阅读SpringBoot源码,分享SpringBoot中Logging,Environment组件的启动过程。
如果大家在使用SpringBoot过程中,遇到日志配置无效,Environment中获取属性错误,希望本文可以给你们一个解决问题的思路。
源码分析基于spring boot 2.1
(源码解析类文章建议在PC端阅读)
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代表当前应用运行环境,管理配置属性数据,并提供Profile特性,即可以根据环境得到相应配置属性数据。
Environment的查询
Environment的实现类都继承了AbstractEnvironment,
AbstractEnvironment#propertySources是一个MutablePropertySources,它实际上是一个属性源PropertySources列表。
AbstractEnvironment#propertyResolver是一个属性解析器ConfigurablePropertyResolver,负责从属性源中查询对应属性。
AbstractEnvironment也实现了PropertyResolver。
AbstractEnvironment#getProperty -> PropertySourcesPropertyResolver#getProperty
protected
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更简洁清晰,有兴趣的同学可以自行阅读代码。
文章最后,附图一张