今天我们来学习下 Spring Boot 中的日志配置。
01-Logback
Spring Boot 中默认使用的是 Logback 日志组件。Logback 是 Log4j 作者发起的另外一个开源日志组件,目的是提供比 Log4j 更好地性能。
Logback 日志组件共分为三部分:
logback-core 是 Logback 的核心模块与实现;
logback-classic 是 Log4j 的改良版本,也是 SLF4j 的一个实现;
logback-access 提供通过 HTTP 访问日志的功能;
接下来,我们来看一下一个 Spring Boot 工程是如何将 Logback 引入进来的?
使用 Spring Initializr 初始化的工程,一般项目的 parent 设置为spring-boot-starter-parent
→ spring-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 称为其默认的日志组件
接下来,我们梳理下常用的日志门面接口:
我们的 Spring Boot 项目 payroll 使用的是 SLF4j 作为日志门面,以 Logback-classic 作为日志实现。日志门面与日志实现的关系可以用 SLF4j 官网的一个图片很好的诠释:
SLF4j 将应用与底层的日志组件解耦,屏蔽了具体地日志实现细节。除此之外,日志门面还会提供桥接接口,将那些对 Log4j、JCL、JUL 的调用重定向到对 SLF4j 的调用,SLF4j 官网的一个图片很好的诠释了这个思路:
最后,我们梳理下 SLF4j 中各类 jar 包的具体作用:
桥接
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
适配(不能与对应的桥接包共存,例如 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 自带的简单日志实现
核心包
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
[1] SpringBoot揭密:spring-boot-starter-logging及源码解析
[2] 关于slf4j log4j log4j2的jar包配合使用的那些事
[3] SpringBoot入门 - 添加Logback日志
评论