Spring Boot「02」日志配置

200 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第02天,点击查看活动详情

今天我们来学习下 Spring Boot 中的日志配置。

01-Logback

Spring Boot 中默认使用的是 Logback 日志组件。Logback 是 Log4j 作者发起的另外一个开源日志组件,目的是提供比 Log4j 更好地性能。

Logback 日志组件共分为三部分:

  1. logback-core 是 Logback 的核心模块与实现;
  2. logback-classic 是 Log4j 的改良版本,也是 SLF4j 的一个实现;
  3. logback-access 提供通过 HTTP 访问日志的功能;

接下来,我们来看一下一个 Spring Boot 工程是如何将 Logback 引入进来的?

使用 Spring Initializr 初始化的工程,一般项目的 parent 设置为spring-boot-starter-parentspring-boot-dependencies。例如,我们在之前文章中使用到的 payroll 项目(可以在我的 gitee 中获取完整代码)。在spring-boot-dependencies中为项目引入了 spring-boot-starter 和 spring-boot-starter-logging 两个编译时依赖,而正是后者引入了对 logback-classic 的依赖:

<dependencies>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <scope>compile</scope>
    </dependency>
    <dependency> <!-- 将 log4j 桥接到 slf4j -->
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <scope>compile</scope>
    </dependency>
    <dependency> <!-- 将 jul 桥接到 slf4j -->
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <scope>compile</scope>
    </dependency>
  </dependencies>

知道了 Spring Boot 工程是如何引入 Logback 日志组件的之后,我们接下来看一下 Spring Boot 应用是如何完成日志组件的初始化的?(当前使用的 Spring Boot 版本为 2.7.4

spring-boot-*.jar/META-INF/spring.factories 中配置了许多事件监听者(如下所示),监听应用启动事件、准备环境事件等。

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
**org.springframework.boot.context.logging.LoggingApplicationListener**,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

Logback 就是通过LoggingApplicationListener来进行初始化的,其类中的onApplicationEvent实现了对不同事件的响应动作。

public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationStartingEvent) { // 应用启动事件
        this.onApplicationStartingEvent((ApplicationStartingEvent)event);
    } else if (event instanceof ApplicationEnvironmentPreparedEvent) { // 环境准备完成时间
        this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
    } else if (event instanceof ApplicationPreparedEvent) {
        this.onApplicationPreparedEvent((ApplicationPreparedEvent)event);
    } else if (event instanceof ContextClosedEvent) {
        this.onContextClosedEvent((ContextClosedEvent)event);
    } else if (event instanceof ApplicationFailedEvent) {
        this.onApplicationFailedEvent();
    }
}

ApplicationStartingEvent事件发生时,onApplicationStartingEvent方法会对日志组件进行初始化:

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
	this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
	this.loggingSystem.beforeInitialize();
}

其中,LoggingSystem.get方法就是在遍历 spring-boot-*.jar/META-INF/spring.factories 中的LoggingSystemFactory

public static LoggingSystem get(ClassLoader classLoader) {
	String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
	if (StringUtils.hasLength(loggingSystemClassName)) {
		if (NONE.equals(loggingSystemClassName)) {
			return new NoOpLoggingSystem();
		}
		return get(classLoader, loggingSystemClassName);
	}
	// spring-boot-*.jar/META-INF/spring.factories 中配置的 LoggingSystemFactory
	LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
	Assert.state(loggingSystem != null, "No suitable logging system located");
	return loggingSystem;
}
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.**LogbackLoggingSystem.Factory**,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

ApplicationEnvironmentPreparedEvent事件发生时,onApplicationEnvironmentPreparedEvent方法会进行初始化动作:

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
	SpringApplication springApplication = event.getSpringApplication();
	if (this.loggingSystem == null) {
		this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
	}
	// 初始化
	initialize(event.getEnvironment(), springApplication.getClassLoader());
}
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
	getLoggingSystemProperties(environment).apply();
	this.logFile = LogFile.get(environment);
	if (this.logFile != null) {
		this.logFile.applyToSystemProperties();
	}
	this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
	initializeEarlyLoggingLevel(environment);
	initializeSystem(environment, this.loggingSystem, this.logFile);
	initializeFinalLoggingLevels(environment, this.loggingSystem);
	registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

02-日志门面、日志实现之间的关系

在学习日志组件的时候,经常会看到说 JCL、JUL、Log4j、Log4j2、SLF4j等等的名词,它们之间的关系是如何的?接下来,我们将一块学习下这些名词具体指代什么,它们之间的关系又是如何的。

首先,我们先梳理一下常用的日志组件(或者说日志实现):

  • JUL,java.util.logging,是 JDK 内置的日志组件
  • Log4j,是 Apache 软件基金会下的开源项目,也是 Java 中的老牌日子组件
  • Log4j2,是 Log4j 的升级版本,2.*
  • Logback,是 Log4j 作者发起的另一个开源日志项目,旨在提供更高效的日志组件;Spring Boot 2.0 以后,Logback 称为其默认的日志组件

接下来,我们梳理下常用的日志门面接口:

  • SLF4j,是最常见的日志门面接口
  • JCL,Jakarta commons logging 或者称为 Apache commons logging,是 Apache 旗下的一个日志门面接口

我们的 Spring Boot 项目 payroll 使用的是 SLF4j 作为日志门面,以 Logback-classic 作为日志实现。日志门面与日志实现的关系可以用 SLF4j 官网的一个图片很好的诠释:

concrete-bindings.png

SLF4j 将应用与底层的日志组件解耦,屏蔽了具体地日志实现细节。除此之外,日志门面还会提供桥接接口,将那些对 Log4j、JCL、JUL 的调用重定向到对 SLF4j 的调用,SLF4j 官网的一个图片很好的诠释了这个思路:

legacy.png

最后,我们梳理下 SLF4j 中各类 jar 包的具体作用:

  1. 桥接
    • jcl-over-slf4j.jar,将 JCL 桥接到 SLF4j
    • jul-to-slf4j.jar,将 JUL 桥接到 SLF4j
    • log4j-over-slf4j.jar,将 log4j 桥接到 SLF4j
    • osgi-over-slf4j.jar,将 osgi 桥接到 SLF4j
    • slf4j-android.jar,将 android 桥接到 SLF4j
  2. 适配(不能与对应的桥接包共存,例如 slf4j-jcl.jar 与 jcl-over-slf4j.jar 不能共存
    • slf4j-jcl.jar,提供SLF4j 与日志实现 JCL 之间的适配层
    • slf4j-jdk14.jar,提供SLF4j 与日志实现 JUL 之间的适配层
    • slf4j-log4j12.jar,提供SLF4j 与日志实现 Log4j 之间的适配层
    • slf4j-nop.jar,提供SLF4j 输出到 /dev/null
    • slf4j-simple.jar,SLF4j 自带的简单日志实现
  3. 核心包
    • slf4j-api.jar,日志门面核心 jar 包,主要用于应用层调用
    • slf4j-ext.jar,扩展包

03-Spring Boot 项目中日志配置

了解了上述内容,我们来简单看下 Spring Boot 项目中是如何修改日志配置的。使用的日志实现是 Logback。

Spring Boot 项目中,日志配置文件可以是 logback-spring.xml(会自动被 Spring Boot 框架识别),也可在 application.yml 中通过logging.config: classpath:xxx.xml指定。通常推荐使用上述这两种方式,而不推荐使用 logback.xml 这种可以直接被 Logback 组件识别并解析的配置文件。原因主要是交由 Spring Boot 来解析日志配置,可以使用 Spring Boot 的一些特性,而这些特性是 Logback 组件不支持的。

LogbackLoggingSystem 中定义了标准配置文件的名称:

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

Spring Boot 框架可扫描到的配置文件名称:

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;
	}

可以看出,即getStandardConfigLocations返回值加上“-spring”形成的文件名。

refs