精尽Spring Boot源码分析 – 日志系统

精尽Spring Boot源码分析 - 日志系统

该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读

Spring Boot 版本:2.2.x

最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 – 文章导读》 系列文章

如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~

该系列其他文章请查看:《精尽 Spring Boot 源码分析 – 文章导读》

概述

日志是一个系统必不可缺少的东西,记录了系统运行时的点点滴滴,便于我们了解自己系统的运行状态,在我们使用 Spring Boot 时,默认就已经提供了日志功能,使用 Logback 作为默认的日志框架。那么,接下来我们依赖来看看 Spring Boot 是如何初始化好日志系统的。

为什么 Spring Boot 默认的日志框架是 Logbasck 呢?

因为在 spring-boot-starter 模块中引入 spring-boot-starter-logging 模块,该 Starter 引入了 logback-classic 依赖。

Log 日志体系

在我们的日常工作中,可能看到项目中依赖的跟日志相关的 jar 包有很多,例如 commons-logginglog4jlog4j2sl4jlogback 等等,眼花缭乱。经常会碰到各种依赖冲入的问题,非常烦恼,例如这几个问题:

  1. Failed to load class org.slf4j.impl.StaticLoggerBinder,没找到日志实现,如果你觉得你已经添加了对应的日志实现依赖了,那应该检查一下版本是否兼容
  2. Multiple bindings,找到了多个日志实现,也可能是版本问题,slf4j 会找其中一个作为日志实现

如果想要正确地使用它们,有必要先理清它们之间的关系,我们可以来看看 Log 的发展史,首先从 Java Log 的发展历程开始说起:

  1. log4j(作者Ceki Gülcü)出来后,被开发者们广泛的应用(注意,这里是直接使用),当初是 Java 日志事实上的标准,并成为了 Apache 的项目
  2. Apache 要求把 log4j 并入到 jdk,SUN 表示拒绝,并在 jdk1.4 版本后增加了 JULjava.util.logging);
  3. 毕竟是 JDK 自带的,JUL 也被很多人使用。同时还有其他的日志组件,如 SimpleLog 等。这个时候如果有人想换成其他日志组件,如 log4j 换成 JUL,因为 API 完全不同,就需要改动代码,当然很多人不愿意呀;
  4. Apache 见此,开发了 JCL(Jakarta Commons Logging),即 commons-logging-xx.jar。它只提供一套通用的日志接口 API,并不提供日志的实现。很好的设计原则嘛,依赖抽象而非实现。这样一来,我们的应用程序可以在运行时选择自己想要的日志实现组件;
  5. 这样看上去也挺美好的,但是 log4j 的作者觉得 JCL 不好用,自己开发出一套 slf4j,它跟 JCL 类似,本身不替供日志的具体实现,只对外提供接口或门面。目的就是为了替代 JCL。同时,还开发出 logback,一个比 log4j 拥有更高性能的组件,目的是为了替代 log4j
  6. Apache 参考了 logback,并做了一系列优化,推出了一套 log4j2 日志框架。

对于性能没什么特别高要求的使用 Spring Boot 中默认的 logback 就可以了,如果想要使用 log4j2 可以参考我的 《MyBatis 使用手册》 这篇文章,有提到过。

回顾

回到前面的 《SpringApplication 启动类的启动过程》 这篇文章,Spring Boot 启动应用的入口和主流程都是在 SpringApplication#run(String.. args) 方法中。

在启动 Spring 应用的整个过程中,到了不同的阶段会发布不同类型的事件,例如最开始会发布一个 应用正在启动 的事件,对于不同类型的事件都是通过 EventPublishingRunListener 事件发布器来发布,里面有一个事件广播器,封装了几个 ApplicationListener 事件监听器,如下:

# Application Listeners
org.springframework.context.ApplicationListener=
org.springframework.boot.ClearCachesApplicationListener,
org.springframework.boot.builder.ParentContextCloserApplicationListener,
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,
org.springframework.boot.context.FileEncodingApplicationListener,
org.springframework.boot.context.config.AnsiOutputApplicationListener,
org.springframework.boot.context.config.ConfigFileApplicationListener,
org.springframework.boot.context.config.DelegatingApplicationListener,
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,
org.springframework.boot.context.logging.LoggingApplicationListener,
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

其中有一个 LoggingApplicationListener 对象,监听到不同事件后,会对日志系统进行一些相关的初始化工作

提示:Spring Boot 的 LoggingSystem 日志系统的初始化过程有点绕,嵌套的方法有点多,可参考序号耐心查看

LoggingApplicationListener

org.springframework.boot.context.logging.LoggingApplicationListener,Spring Boot 事件监听器,用于初始化日志系统

onApplicationEvent 方法

onApplicationEvent(ApplicationEvent 方法,处理监听到的事件

@Override
public void onApplicationEvent(ApplicationEvent event) {
    // 应用正在启动的事件
    if (event instanceof ApplicationStartingEvent) {
        onApplicationStartingEvent((ApplicationStartingEvent) event);
    }
    // Environment 环境已准备事件
    else if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    // 应用已准备事件
    else if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent((ApplicationPreparedEvent) event);
    }
    // Spring 上下文关闭事件
    else if (event instanceof ContextClosedEvent
            && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
        onContextClosedEvent();
    }
    // 应用启动失败事件
    else if (event instanceof ApplicationFailedEvent) {
        onApplicationFailedEvent();
    }
}

对于不同的事件调用不同的方法,事件的发布顺序也就是上面从上往下的顺序

1. onApplicationStartingEvent 方法

处理应用正在启动的事件

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    // <1> 创建 LoggingSystem 对象
    // 指定了类型则使用指定的,没有则尝试创建对应的对象,ClassLoader 中有对应的 Class 对象则创建(logback > log4j2 > java logging)
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    // <2> LoggingSystem 的初始化前置处理
    this.loggingSystem.beforeInitialize();
}

过程如下:

  1. 创建 LoggingSystem 对象,指定了类型则使用指定的,没有则尝试创建对应的对象,ClassLoader 中有对应的 Class 对象则创建(logback > log4j2 > java logging
  2. 调用 LoggingSystem 的 beforeInitialize() 方法,初始化前置处理

2. onApplicationEnvironmentPreparedEvent 方法

处理环境已准备事件

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    // <1> 如果还未明确 LoggingSystem 类型,那么这里继续创建 LoggingSystem 对象
    if (this.loggingSystem == null) {
        this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    }
    // <2> 初始化 LoggingSystem 对象,创建日志文件,设置日志级别
    initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}

过程如下:

  1. 如果还未明确 LoggingSystem 类型,那么这里继续创建 LoggingSystem 对象
  2. 调用 initialize(..) 方法,初始化 LoggingSystem 对象,创建日志文件,设置日志级别

3. initialize 方法

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    // <1> 根据 Environment 环境通过 LoggingSystemProperties 往 System 进行一些日志配置
    new LoggingSystemProperties(environment).apply();
    // <2> 根据 Environment 环境配置的日志名称和路径创建一个日志文件
    // 默认情况没有配置,这个对象也为 null,而是在打印第一个日志的时候会创建(如果不存在的话)
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        // <3> 往 System 添加日志文件的名称和路径
        this.logFile.applyToSystemProperties();
    }
    // <4> 创建一个日志分组对象
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    // <5> 初始化早期的 Spring Boot 日志级别(Debug 或者 Trace)
    initializeEarlyLoggingLevel(environment);
    // <6> 初始化 LoggingSystem 对象
    initializeSystem(environment, this.loggingSystem, this.logFile);
    // <7> 初始化最终的 Spring Boot 日志级别,逐个设置 Environment 配置的日志级别
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    // <8> 向 JVM 注册一个钩子,用于在 JVM 关闭时关闭日志系统
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

初始过程如下:

  1. 根据 Environment 环境通过 LoggingSystemProperties 往 System 进行一些日志配置

  2. 根据 Environment 环境配置的日志名称和路径创建一个日志文件,默认情况没有配置,这个对象也为 null,而是在打印第一个日志的时候会创建(如果不存在的话)

    // LogFile.java
    public static LogFile get(PropertyResolver propertyResolver) {
        // 获取 `logging.file.name` 指定的日志文件名称,也可以通过 `logging.file` 指定
        String file = getLogFileProperty(propertyResolver, FILE_NAME_PROPERTY, FILE_PROPERTY);
        // 获取 `logging.file.path` 指定的日志文件保存路径,也可以通过 `logging.path` 指定
        String path = getLogFileProperty(propertyResolver, FILE_PATH_PROPERTY, PATH_PROPERTY);
        // 创建一个日志文件
        if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
            return new LogFile(file, path);
        }
        return null;
    }
    
  3. 往 System 添加日志文件的名称和路径

  4. 创建一个日志分组对象

  5. 初始化早期的 Spring Boot 日志级别(Debug 或者 Trace)

    private void initializeEarlyLoggingLevel(ConfigurableEnvironment environment) {
        if (this.parseArgs && this.springBootLogging == null) {
            if (isSet(environment, "debug")) {
                this.springBootLogging = LogLevel.DEBUG;
            }
            if (isSet(environment, "trace")) {
                this.springBootLogging = LogLevel.TRACE;
            }
        }
    }
    
  6. 初始化 LoggingSystem 对象

    private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
        LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
        // <1> 找到 `logging.config` 指定的配置文件路径
        String logConfig = environment.getProperty(CONFIG_PROPERTY);
        // <2> 如果没配置文件,则不指定配置文件初始化 LoggingSystem 对象
        // 使用约定好的配置文件,或者使用默认配置
        if (ignoreLogConfig(logConfig)) {
            system.initialize(initializationContext, null, logFile);
        }
        // <3> 否则,指定配置文件初始化 LoggingSystem 对象
        else {
            try {
                system.initialize(initializationContext, logConfig, logFile);
            }
            catch (Exception ex) {
                // 抛出异常
            }
        }
    }
    
  7. 初始化最终的 Spring Boot 日志级别,逐个设置 Environment 配置的日志级别

  8. 向 JVM 注册一个钩子,用于在 JVM 关闭时关闭日志系统

可以看到需要通过 LoggingSystem 日志系统对象来初始化,后面会讲到

4. onApplicationPreparedEvent 方法

处理应用已准备事件

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
    // 往底层 IoC 容器注册几个 Bean:LoggingSystem、LogFile 和 LoggerGroups
    ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
    if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
        beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
    }
    if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
        beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
    }
    if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
        beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
    }
}

LoggingSystem

org.springframework.boot.logging.LoggingSystem 抽象类,Spring Boot 的日志系统对象,每个日志框架,都会对应一个实现类。如下图所示:

public abstract class LoggingSystem {

	private static final Map<String, String> SYSTEMS;

	static {
		Map<String, String> systems = new LinkedHashMap<>();
		systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
		systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
				"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
		SYSTEMS = Collections.unmodifiableMap(systems);
	}
}

1.1 get 方法

创建一个 LoggingSystem 日志系统对象,如下:

public static LoggingSystem get(ClassLoader classLoader) {
    // <1> 从系统参数 `org.springframework.boot.logging.LoggingSystem` 获得 LoggingSystem 类型
    String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
    // <2> 如果非空,说明配置了,那么创建一个该类型的 LoggingSystem 实例对象
    if (StringUtils.hasLength(loggingSystem)) {
        if (NONE.equals(loggingSystem)) {
            return new NoOpLoggingSystem();
        }
        return get(classLoader, loggingSystem);
    }
    // <3> 否则,没有配置,则通过顺序依次尝试创建对应类型的 LoggingSystem 实例对象
    // logback > log4j2 > java logging
    return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
            .map((entry) -> get(classLoader, entry.getValue())).findFirst()
            .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
}

过程如下:

  1. 从系统参数 org.springframework.boot.logging.LoggingSystem 获得 LoggingSystem 类型
  2. 如果非空,说明配置了,那么创建一个该类型的 LoggingSystem 实例对象
  3. 否则,没有配置,则通过顺序依次尝试创建对应类型的 LoggingSystem 实例对象,也就是在 static 代码块中初始化好的集合,logback > log4j2 > java logging

1.2 beforeInitialize 方法

初始化的前置操作,抽象方法,交由子类实现

/**
 * Reset the logging system to be limit output. This method may be called before
 * {@link #initialize(LoggingInitializationContext, String, LogFile)} to reduce
 * logging noise until the system has been fully initialized.
 */
public abstract void beforeInitialize();

2. initialize 方法

初始化操作,空方法,由子类来重写

/**
 * Fully initialize the logging system.
 * @param initializationContext the logging initialization context
 * @param configLocation a log configuration location or {@code null} if default
 * initialization is required
 * @param logFile the log output file that should be written or {@code null} for
 * console only output
 */
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
}

AbstractLoggingSystem

org.springframework.boot.logging.AbstractLoggingSystem 抽象类,继承 LoggingSystem 抽象类,作为一个基类

2.1 initialize 方法

重写父类的 initialize(..) 方法,提供模板化的初始化逻辑,如下:

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    // <1> 有自定义的配置文件,则使用指定配置文件进行初始化
    if (StringUtils.hasLength(configLocation)) {
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    // <2> 无自定义的配置文件,则使用约定配置文件进行初始化
    initializeWithConventions(initializationContext, logFile);
}

有指定的配置文件,则调用 initializeWithSpecificConfig(..) 方法, 使用指定配置文件进行初始化

没有自定义的配置文件,则调用 initializeWithConventions(..) 方法,使用约定配置文件进行初始化

2.1.1 initializeWithSpecificConfig 方法

initializeWithSpecificConfig(LoggingInitializationContext, String, LogFile) 方法,使用指定配置文件进行初始化

private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation,
        LogFile logFile) {
    // <1> 获得配置文件的路径(可能有占位符)
    configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
    // <2> 加载配置文件到日志系统中,抽象方法,子类实现
    loadConfiguration(initializationContext, configLocation, logFile);
}

先获取配置文件的路径(可能有占位符),然后调用 loadConfiguration(..) 抽象方法,加载配置文件到日志系统中

/**
 * Load a specific configuration.
 * @param initializationContext the logging initialization context
 * @param location the location of the configuration to load (never {@code null})
 * @param logFile the file to load or {@code null} if no log file is to be written
 */
protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location,
        LogFile logFile);

2.1.2 initializeWithConventions 方法

initializeWithConventions(LoggingInitializationContext, LogFile) 方法,使用约定配置文件进行初始化

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // <1> 尝试获得约定配置文件,例如 log4j2 约定的是 log4j2.xml
    String config = getSelfInitializationConfig();
    // <2> 如果找到了约定的配置文件
    if (config != null && logFile == null) {
        // self initialization has occurred, reinitialize in case of property changes
        // <2.1> 自定义初始化,子类实现
        reinitialize(initializationContext);
        return;
    }
    // <3> 尝试获取约定的配置文件(带有 `-spring` ),例如 log4j2 对应是 log4j2-spring.xml
    if (config == null) {
        config = getSpringInitializationConfig();
    }
    // <4> 获取到了 `-spring` 配置文件,则加载到日志系统中,抽象方法,子类实现
    if (config != null) {
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    // <5> 加载默认配置,抽象方法,子类实现
    loadDefaults(initializationContext, logFile);
}

过程如下

  1. 调用 getSelfInitializationConfig() 方法,尝试获得约定配置文件,例如 log4j2 约定的是 log4j2.xml

    protected String getSelfInitializationConfig() {
        return findConfig(getStandardConfigLocations());
    }
    
    protected abstract String[] getStandardConfigLocations();
    
    private String findConfig(String[] locations) {
        for (String location : locations) {
            ClassPathResource resource = new ClassPathResource(location, this.classLoader);
            if (resource.exists()) {
                return "classpath:" + location;
            }
        }
        return null;
    }
    
  2. 如果找到了约定的配置文件,则调用 reinitialize(..) 抽象方法,自定义初始化,子类实现

    protected void reinitialize(LoggingInitializationContext initializationContext) { }
    
  3. 调用 getSpringInitializationConfig(..) 方法,尝试获取约定的配置文件(带有 -spring ),例如 log4j2 对应是 log4j2-spring.xml

    protected String getSpringInitializationConfig() {    return findConfig(getSpringConfigLocations());}protected String[] getSpringConfigLocations() {    String[] locations = getStandardConfigLocations();    for (int i = 0; i < locations.length; i++) {        String extension = StringUtils.getFilenameExtension(locations[i]);        locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."                + extension;    }    return locations;}private String findConfig(String[] locations) {    for (String location : locations) {        ClassPathResource resource = new ClassPathResource(location, this.classLoader);        if (resource.exists()) {            return "classpath:" + location;        }    }    return null;}
    
  4. 获取到了 -spring 配置文件,则调用 loadConfiguration(..) 抽象方法,加载到日志系统中,子类实现

    protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location,        LogFile logFile);
    
  5. 还没有找到到指定的配置文件,那么调用 loadDefaults(..) 抽象方法,加载默认配置,子类实现

    protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);
    

整个过程就是尝试获取到各个日志框架约定好的配置文件名称,如果存在这个配置文件,则加载到日志系统中,否则使用默认的配置

Slf4JLoggingSystem

org.springframework.boot.logging.Slf4JLoggingSystem,继承 AbstractLoggingSystem 抽象类,基于 Slf4J 的 LoggingSystem 的抽象基类

1.2.1 beforeInitialize 方法

初始化的前置操作

@Overridepublic void beforeInitialize() {    super.beforeInitialize();    // <1> 配置 JUL 的桥接处理器,桥接到 slf4j    configureJdkLoggingBridgeHandler();}

先调用父类的 beforeInitialize() 方法,然后调用 configureJdkLoggingBridgeHandler() 方法,配置 JUL 的桥接处理器,桥接到 slf4j

private void configureJdkLoggingBridgeHandler() {    try {        // <1> 判断 JUL 是否桥接到 SLF4J 了        if (isBridgeJulIntoSlf4j()) {            // <2> 移除 JUL 桥接处理器            removeJdkLoggingBridgeHandler();            // <3> 重新安装 SLF4JBridgeHandler            SLF4JBridgeHandler.install();        }    }    catch (Throwable ex) {        // Ignore. No java.util.logging bridge is installed.    }}

过程如下:

  1. 判断 JUL 是否桥接到 slf4j

    protected final boolean isBridgeJulIntoSlf4j() {    // 存在 SLF4JBridgeHandler 类,且 JUL 只有 ConsoleHandler 处理器被创建    return isBridgeHandlerAvailable() && isJulUsingASingleConsoleHandlerAtMost();}
    
  2. 移除 JUL 桥接处理器

    private void removeJdkLoggingBridgeHandler() {    try {        // 移除 JUL 的 ConsoleHandler        removeDefaultRootHandler();        // 卸载 SLF4JBridgeHandler        SLF4JBridgeHandler.uninstall();    }    catch (Throwable ex) {        // Ignore and continue    }}private void removeDefaultRootHandler() {    try {        Logger rootLogger = LogManager.getLogManager().getLogger("");        Handler[] handlers = rootLogger.getHandlers();        if (handlers.length == 1 && handlers[0] instanceof ConsoleHandler) {            rootLogger.removeHandler(handlers[0]);        }    }    catch (Throwable ex) {        // Ignore and continue    }}
    
  3. 重新安装 SLF4JBridgeHandler

2.3 loadConfiguration 方法

重写 AbstractLoggingSystem 父类的方法,加载指定的日志配置文件到日志系统中

@Overrideprotected void loadConfiguration(LoggingInitializationContext initializationContext, String location,        LogFile logFile) {    Assert.notNull(location, "Location must not be null");    if (initializationContext != null) {        // 将 Environment 中的日志配置往 System 中配置        applySystemProperties(initializationContext.getEnvironment(), logFile);    }}

实际上就是将 Environment 中的日志配置往 System 中配置

LogbackLoggingSystem

org.springframework.boot.logging.logback.LogbackLoggingSystem,继承 Slf4JLoggingSystem 抽象类,基于 logback 的 LoggingSystem 实现类

1.2.2 beforeInitialize 方法

重写 LoggingSystem 的方法,初始化前置操作

@Overridepublic void beforeInitialize() {    // <1> 获得 LoggerContext 日志上下文    LoggerContext loggerContext = getLoggerContext();    // <2> 如果 LoggerContext 已有 LoggingSystem,表示已经初始化,则直接返回    if (isAlreadyInitialized(loggerContext)) {        return;    }    // <3> 调用父方法    super.beforeInitialize();    // <4> 添加 FILTER 到其中,因为还未初始化,不打印日志    loggerContext.getTurboFilterList().add(FILTER);}

过程如下:

  1. 调用 getLoggerContext() 方法,获得 LoggerContext 日志上下文

    private LoggerContext getLoggerContext() {    ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();    // 这里会校验 `factory` 是否为 LoggerContext 类型    return (LoggerContext) factory;}
    
  2. 如果 LoggerContext 已有 LoggingSystem,表示已经初始化,则直接返回

    private boolean isAlreadyInitialized(LoggerContext loggerContext) {    return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
    
  3. 调用父方法

  4. 添加 FILTER 到其中,因为还未初始化,不打印日志

    private static final TurboFilter FILTER = new TurboFilter() {    @Override    public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format,            Object[] params, Throwable t) {        // 一律拒绝        return FilterReply.DENY;    }};
    

getStandardConfigLocations 方法

重写 AbstractLoggingSystem 的方法,获取 logback 标准的配置文件名称

@Overrideprotected String[] getStandardConfigLocations() {    return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };}

2.2 initialize 方法

重写 LoggingSystem 的方法,初始化操作

@Overridepublic void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {    // <1> 获得 LoggerContext 日志上下文    LoggerContext loggerContext = getLoggerContext();    // <2> 如果 LoggerContext 已有 LoggingSystem,表示已经初始化,则直接返回    if (isAlreadyInitialized(loggerContext)) {        return;    }    // <3> 调用父方法    super.initialize(initializationContext, configLocation, logFile);    // <4> 移除之前添加的 FILTER,可以开始打印日志了    loggerContext.getTurboFilterList().remove(FILTER);    // <5> 标记为已初始化,往 LoggerContext 中添加一个 LoggingSystem 对象    markAsInitialized(loggerContext);    if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {        getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring "" + CONFIGURATION_FILE_PROPERTY                + "" system property. Please use "logging.config" instead.");    }}

过程如下:

  1. 调用 getLoggerContext() 方法,获得 LoggerContext 日志上下文

    private LoggerContext getLoggerContext() {    ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();    // 这里会校验 `factory` 是否为 LoggerContext 类型    return (LoggerContext) factory;}
    
  2. 如果 LoggerContext 已有 LoggingSystem,表示已经初始化,则直接返回

    private boolean isAlreadyInitialized(LoggerContext loggerContext) {    return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
    
  3. 调用父方法

  4. 移除之前添加的 FILTER,可以开始打印日志了

  5. 调用 markAsInitialized(..) 方法,标记为已初始化,往 LoggerContext 中添加一个 LoggingSystem 对象

2.4 loadConfiguration 方法

重写 AbstractLoggingSystem 的方法,加载指定的日志配置文件到日志系统中

@Overrideprotected void loadConfiguration(LoggingInitializationContext initializationContext, String location,        LogFile logFile) {    // <1> 调用父方法    super.loadConfiguration(initializationContext, location, logFile);    LoggerContext loggerContext = getLoggerContext();    // <2> 重置 LoggerContext 对象    // 这里会添加一个 LevelChangePropagator 监听器,当日志级别被修改时会立即生效,而不用重启应用    stopAndReset(loggerContext);    try {        // <3> 读取配置文件并解析,配置到 LoggerContext 中        configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));    }    catch (Exception ex) {        throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);    }    // <4> 判断是否发生错误,有的话抛出 IllegalStateException 异常    List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();    StringBuilder errors = new StringBuilder();    for (Status status : statuses) {        if (status.getLevel() == Status.ERROR) {            errors.append((errors.length() > 0) ? String.format("%n") : "");            errors.append(status.toString());        }    }    if (errors.length() > 0) {        throw new IllegalStateException(String.format("Logback configuration error detected: %n%s", errors));    }}

过程如下:

  1. 调用父方法

  2. 重置 LoggerContext 对象,这里会添加一个 LevelChangePropagator 监听器,当日志级别被修改时会立即生效,而不用重启应用

    private void stopAndReset(LoggerContext loggerContext) {    // 停止    loggerContext.stop();    // 重置    loggerContext.reset();    // 如果有桥接器    if (isBridgeHandlerInstalled()) {        // 添加一个日志级别的监听器,能够及时更新日志级别        addLevelChangePropagator(loggerContext);    }}private void addLevelChangePropagator(LoggerContext loggerContext) {    LevelChangePropagator levelChangePropagator = new LevelChangePropagator();    levelChangePropagator.setResetJUL(true);    levelChangePropagator.setContext(loggerContext);    loggerContext.addListener(levelChangePropagator);}
    
  3. 读取配置文件并解析,配置到 LoggerContext 中

    private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,        URL url) throws JoranException {    if (url.toString().endsWith("xml")) {        JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);        configurator.setContext(loggerContext);        configurator.doConfigure(url);    }    else {        new ContextInitializer(loggerContext).configureByResource(url);    }}
    
  4. 判断是否发生错误,有的话抛出 IllegalStateException 异常

reinitialize 方法

实现类 AbstractLoggingSystem 的方法,重新初始化

@Overrideprotected void reinitialize(LoggingInitializationContext initializationContext) {    // 重置    getLoggerContext().reset();    // 清空资源    getLoggerContext().getStatusManager().clear();    // 加载指定的配置文件,此时使用约定的配置文件    loadConfiguration(initializationContext, getSelfInitializationConfig(), null);}

loadDefaults 方法

实现类 AbstractLoggingSystem 的方法,没有指定的配置文件,也没有约定的配置文件,那么加载默认的配置到日志系统

@Overrideprotected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {    LoggerContext context = getLoggerContext();    // <1> 重置 LoggerContext 对象    // 这里会添加一个 LevelChangePropagator 监听器,当日志级别被修改时会立即生效,而不用重启应用    stopAndReset(context);    // <2> 如果开启 debug 模式则添加一个 OnConsoleStatusListener 监听器    boolean debug = Boolean.getBoolean("logback.debug");    if (debug) {        StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());    }    // <3> 往 LoggerContext 中添加默认的日志配置    LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)            : new LogbackConfigurator(context);    Environment environment = initializationContext.getEnvironment();    context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,            environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));    context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders(            "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));    context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment            .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));    // <4> 创建 DefaultLogbackConfiguration 对象,设置到 `configurator` 中    // 设置转换规则,例如颜色转换,空格转换    new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);    // <5> 设置日志文件,按天切割    context.setPackagingDataEnabled(true);}

过程如下:

  1. 重置 LoggerContext 对象,这里会添加一个 LevelChangePropagator 监听器,当日志级别被修改时会立即生效,而不用重启应用

    private void stopAndReset(LoggerContext loggerContext) {    // 停止    loggerContext.stop();    // 重置    loggerContext.reset();    // 如果有桥接器    if (isBridgeHandlerInstalled()) {        // 添加一个日志级别的监听器,能够及时更新日志级别        addLevelChangePropagator(loggerContext);    }}private void addLevelChangePropagator(LoggerContext loggerContext) {    LevelChangePropagator levelChangePropagator = new LevelChangePropagator();    levelChangePropagator.setResetJUL(true);    levelChangePropagator.setContext(loggerContext);    loggerContext.addListener(levelChangePropagator);}
    
  2. 如果开启 debug 模式则添加一个 OnConsoleStatusListener 监听器

  3. 往 LoggerContext 中添加默认的日志配置

  4. 创建 DefaultLogbackConfiguration 对象,设置到 configurator 中,设置转换规则,例如颜色转换,空格转换

  5. 设置日志文件,按天切割

Log4J2LoggingSystem

org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,继承 Slf4JLoggingSystem 抽象类,基于 log4j2 的 LoggingSystem 实现类

LogbackLoggingSystem 基本类似,感兴趣的小伙伴可以自己去瞧一瞧

JavaLoggingSystem

org.springframework.boot.logging.java.JavaLoggingSystem,继承 AbstractLoggingSystem 抽象类,基于 jul 的 LoggingSystem 实现类

逻辑比较简单,感兴趣的小伙伴可以自己去瞧一瞧

总结

本文分析了 Sping Boot 初始化不同 LoggingSystem 日志系统的一个过程,同样是借助于 Spring 的 ApplicationListener 事件监听器机制,在启动 Spring 应用的过程中,例如会广播 应用正在启动的事件应用环境已准备好,然后 LoggingApplicationListener 监听到不同的事件会进行不同的初始化操作。

LoggingSystem 日志系统主要分为 logbacklog4j2JUL 三种,本文主要对 logback 的初始化过程进行了分析,因为它是 Spring Boot 的默认日志框架嘛。整个的初始化过程稍微有点绕,嵌套的方法有点多,主要的小节都标注了序号。

大致流程就是先配置 JULslf4j 的桥接器,然后尝试找到指定的配置文件对日志系统进行配置,可通过 logging.config 设置;没有指定则获取约定好的配置文件,例如 logback.xmllog4j2.xml;还没有获取到则 Spring 约定好的配置文件,例如 logback-spring.xmllog4j2-spring.xml;要是还没有找到配置文件,那只能尝试加载默认的配置了。

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 精尽Spring Boot源码分析 – 日志系统