该系列文章是笔者在学习 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-logging、log4j、log4j2、sl4j 和 logback 等等,眼花缭乱。经常会碰到各种依赖冲入的问题,非常烦恼,例如这几个问题:

  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 版本后增加了 JUL(java.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 日志系统主要分为 logback、log4j2 和 JUL 三种,本文主要对 logback 的初始化过程进行了分析,因为它是 Spring Boot 的默认日志框架嘛。整个的初始化过程稍微有点绕,嵌套的方法有点多,主要的小节都标注了序号。

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

精尽Spring Boot源码分析 - 日志系统的更多相关文章

  1. 精尽Spring Boot源码分析 - 文章导读

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

  2. 精尽Spring Boot源码分析 - SpringApplication 启动类的启动过程

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

  3. 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

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

  4. 精尽Spring Boot源码分析 - 支持外部 Tomcat 容器的实现

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

  5. 精尽Spring Boot源码分析 - Condition 接口的扩展

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

  6. 精尽Spring Boot源码分析 - 剖析 @SpringBootApplication 注解

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

  7. 精尽Spring Boot源码分析 - 配置加载

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

  8. 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现

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

  9. 精尽Spring Boot源码分析 - 序言

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

随机推荐

  1. nginx 的基础知识(一)

    Nginx  HTTP 和 反向代理web服务器 epoll 占用少的系统资源.支持更多的并发连接 负载均衡 安装简单.配置灵活 热部署.启动快.不间断服务情况下对软件配置进行升级 反向代理 反向代理 ...

  2. MongoDB(5)- Document 文档相关

    Documents MongoDB 的文档可以理解为关系型数据库(Mysql)的一行记录 MongoDB 将数据记录为 BSON 格式的文档 BSON 是 JSON 文档的二进制表示,但它支持的数据类 ...

  3. thinkphp api接口 统一结果返回处理类

    20210602 修正 wqy的笔记:http://www.upwqy.com/details/216.html 返回结果处理,归根结底 主要是有两点 数据结构和返回的数据类型 1.数据类型 :一般情 ...

  4. 上传靶机实战之upload-labs解题

    前言 我们知道对靶机的渗透可以提高自己对知识的掌握能力,这篇文章就对上传靶机upload-labs做一个全面的思路分析,一共21个关卡.让我们开始吧,之前也写过关于上传的专题,分别为浅谈文件上传漏洞( ...

  5. Spring框架两大核心机制(IoC、AOP)

    IoC(控制反转)/ DI(依赖注入) AOP(面向切面编程) Spring 是一个企业级开发框架,是软件设计层面的框架,优势在于可以将应用程序进行分层,开发者可以自主选择组件. MVC:Struts ...

  6. 十一、.net core(.NET 6)搭建ElasticSearch(ES)系列之ElasticSearch、head-master、Kibana环境搭建

    搭建ElasticSearch+Kibana环境 前提条件:已经配置好JDK环境以及Nodejs环境.如果还未配置,请查看我的上一篇博客内容,有详细配置教程. 先下载ElasticSearch(以下文 ...

  7. 细粒度语义分割:ICCV2019论文解析

    细粒度语义分割:ICCV2019论文解析 Fine-Grained Segmentation Networks: Self-Supervised Segmentation for Improved L ...

  8. ADAS虚拟车道边界生成

    ADAS虚拟车道边界生成 Virtual Lane Boundary Generation for Human-Compatible Autonomous Driving: A Tight Coupl ...

  9. BEP 7:CUDA外部内存管理插件(上)

    BEP 7:CUDA外部内存管理插件(上) 背景和目标 在CUDA阵列接口使得能够共享不同的Python之间的数据库的访问CUDA设备.但是,每个库都与其它库区别对待.例如: Numba在内部管理内存 ...

  10. 神经网络AI加速器技术

    神经网络AI加速器技术 能够直接加速卷积神经网络,还能够直接运行常见的网络框架,如TensorFlow.Caffe.PyTorch,DarkNet等,支持用户定制化的网络和计算类型. 功能特点: ● ...