今天我们来学习下 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 Listenersorg.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 Systemsorg.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日志
评论