SpringBoot源码解析-LoggingEnvironment启动

38次阅读

共计 10002 个字符,预计需要花费 26 分钟才能阅读完成。

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

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

正文完
 0