SLF4j(日志框架)
前言
日志记录是软件开发中不可或缺的一环,它帮助开发者追踪程序运行状态、诊断问题、审计操作。然而,Java 生态中存在着多种日志框架(如 Log4j, Logback, java.util.logging 等),这给项目的依赖管理和维护带来了挑战。不同的框架 API 不同,配置方式各异,当项目中引入的第三方库使用了不同的日志框架时,管理和统一日志输出就变得异常复杂。为了解决这一问题,SLF4j (Simple Logging Facade for Java) 应运而生。它并非一个具体的日志实现,而是一个日志门面 (Facade),提供了一套统一的日志记录接口,使得应用程序可以与具体的日志实现框架解耦。本文将深入探讨日志的基础知识、SLF4j 的设计理念、工作原理、如何在 Spring Boot 中配置和使用 SLF4j,以及它的最佳实践。
# 1. 日志基础知识回顾
在深入 SLF4j 之前,我们先回顾一下日志相关的基本概念。
# 1.1 日志级别 (Log Level)
日志级别用于控制日志信息的输出详细程度。级别从高到低通常定义如下:
FATAL > ERROR > WARN > INFO > DEBUG > TRACE
- TRACE: 最详细的日志信息,通常用于追踪代码执行的细微步骤,仅在开发和调试时使用。
- DEBUG: 用于输出调试信息,帮助开发者理解代码执行流程和变量状态。
- INFO: 输出程序运行过程中的重要或常规信息,例如服务的启动/关闭、关键业务操作的完成等。这是生产环境中常用的级别。
- WARN: 输出警告信息,表示程序可能存在潜在问题或发生了预期之外的情况,但程序仍能继续运行。
- ERROR: 输出错误信息,表示发生了比较严重的错误,影响了程序的某个功能,但程序可能仍能继续运行。
- FATAL: 输出致命错误信息,表示发生了极其严重的错误,通常会导致应用程序中止。
级别过滤规则: 当为某个日志记录器(Logger)或整个日志系统设置了特定的日志级别后,只有等于或高于该级别的日志信息才会被输出。
例如,如果设置级别为 INFO
,则 INFO
, WARN
, ERROR
, FATAL
级别的日志会被输出,而 DEBUG
和 TRACE
级别的日志将被忽略。
Spring Boot 默认级别: Spring Boot 应用程序默认的日志级别是 INFO
。可以在配置文件(如 application.yml
或 application.properties
)中修改全局或特定包的日志级别。
# 1.2 日志格式 (Log Format)
一条典型的日志记录通常包含以下信息,其格式可以通过日志框架进行配置:
- 时间戳: 日志事件发生的时间。
- 线程名: 记录日志的线程。
- 日志级别: 该条日志的级别(INFO, DEBUG 等)。
- Logger 名称: 通常是记录日志的类的全限定名。
- 日志消息: 开发者记录的具体信息。
- (可选) 堆栈跟踪: 如果记录的是异常信息。
# 1.3 日志框架:门面与实现
Java 生态中有众多日志框架,可以大致分为两类:
日志门面 (Logging Facade): 定义一套日志记录的 API 接口,应用程序代码主要与这些接口交互。它本身不负责具体的日志输出逻辑。常见的日志门面有:
- SLF4j (Simple Logging Facade for Java): 目前最流行、事实上的标准。
- JCL (Jakarta Commons Logging): 较早的日志门面,现在使用较少。
- jboss-logging: JBoss/WildFly 项目使用的日志门面。
日志实现 (Logging Implementation): 负责实际的日志处理逻辑,包括日志的格式化、过滤、输出到控制台/文件/网络等。常见的日志实现有:
- Logback: SLF4j 的原生实现,由 Log4j 创始人设计,性能好,配置灵活,是 Spring Boot 的默认选择。
- Log4j 2: Log4j 的升级版本,性能和功能都有很大提升。
- Log4j (1.x): 曾经非常流行,但现在已不推荐使用(存在性能和安全问题)。
- JUL (java.util.logging): JDK 自带的日志实现,功能相对简单。
关系: 使用时,通常需要选择一个日志门面和一个日志实现进行搭配。SLF4j 的设计目标就是让应用程序代码只依赖 SLF4j 的 API (门面),而在运行时可以灵活地插入不同的日志实现。
日志门面 (API) | 日志实现 (Engine) |
---|---|
SLF4j (推荐) | Logback (默认/推荐) |
JCL (Commons Logging) | Log4j 2 |
jboss-logging | Log4j (1.x) (不推荐) |
JUL (java.util.logging) |
Spring Boot 的选择: Spring Boot 默认采用 SLF4j 作为日志门面,Logback 作为日志实现。
# 2. SLF4j 深度解析
# 2.1 SLF4j 介绍:日志门面的艺术
SLF4j (Simple Logging Facade for Java) 的核心价值在于它是一个简单的日志门面。它为 Java 提供了一套简洁、统一的日志记录 API。
关键点:
- 不是具体实现: SLF4j 本身不包含日志输出的逻辑(如写入文件、控制台)。它仅仅是一套接口规范。
- 抽象层/适配器: 它充当了应用程序代码和底层具体日志实现框架之间的抽象层或适配器。
- 运行时绑定: 应用程序代码在编译时仅依赖 SLF4j API。在运行时,SLF4j 会通过特定的机制(
StaticLoggerBinder
)查找并绑定到一个具体的日志实现框架(如 Logback, Log4j2),然后将日志记录的调用委托给该实现框架来完成。 - 需要搭配实现: 单独的
slf4j-api.jar
无法工作,必须在项目中加入一个 SLF4j 的绑定 (Binding) 包(通常也是具体的日志实现包,如logback-classic.jar
或slf4j-log4j12.jar
)。
(上图展示了 SLF4j 如何作为中间层连接应用和不同的日志实现)
# 2.2 使用场景:解耦日志依赖
想象一个场景:
- 架构师 A 开发了一个核心库
core-lib.jar
,使用了 Log4j 记录日志。 - 程序员 B 开发了一个业务模块
biz-module.jar
,习惯使用 Logback 记录日志。 - 现在,程序员 B 的业务模块需要依赖
core-lib.jar
。
这时,项目中就同时存在了 Log4j 和 Logback 两种日志框架及其配置,管理起来非常混乱,容易出错。
SLF4j 的解决方案:
如果 core-lib.jar
和 biz-module.jar
都面向 SLF4j API 编程,而不是直接依赖具体的 Log4j 或 Logback API,那么问题就迎刃而解了。最终部署应用程序时,只需要在项目中加入一个 SLF4j 的绑定包(例如 logback-classic.jar
),SLF4j 就会自动将所有通过 SLF4j API 发出的日志请求都路由到 Logback 进行处理。这样就实现了日志框架的统一管理。
# 2.3 SLF4j 的基本使用与绑定原理
# 基本使用
添加依赖:
- 必须: 添加 SLF4j API 依赖 (
slf4j-api.jar
)。 - 必须: 添加一个 SLF4j 绑定/实现依赖(例如
logback-classic.jar
用于绑定 Logback,slf4j-log4j12.jar
用于绑定 Log4j 1.x,slf4j-jdk14.jar
用于绑定 JUL)。
<!-- SLF4j API (核心接口) --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> <!-- 使用较新稳定版 --> </dependency> <!-- 选择一个 SLF4j 绑定/实现 --> <!-- 示例:绑定 Logback (推荐) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> <!-- 与 slf4j-api 版本通常需要兼容 --> </dependency> <!-- 示例:绑定 Log4j 1.x (如果需要兼容旧系统) --> <!-- <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28- 注意: 一个项目中只能存在一个 SLF4j 绑定包,否则 SLF4j 会在启动时打印警告信息,并且行为可能变得不可预测。
- 必须: 添加 SLF4j API 依赖 (
获取 Logger 实例: 在代码中,通过
org.slf4j.LoggerFactory
获取org.slf4j.Logger
接口的实例。import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyService { // 推荐方式:使用当前类的 Class 对象获取 Logger // Logger 的名称会自动设为该类的全限定名 (e.g., "com.example.MyService") private static final Logger logger = LoggerFactory.getLogger(MyService.class); public void doSomething(String input) { // 记录不同级别的日志 logger.trace("Entering doSomething with input: {}", input); // 使用占位符 {} if (input == null || input.isEmpty()) { logger.warn("Input is null or empty!"); // 记录警告 return; } logger.debug("Processing input: {}", input); // 记录调试信息 try { // ... 执行业务逻辑 ... logger.info("Successfully processed input: {}", input); // 记录常规信息 } catch (Exception e) { // 记录错误信息,并附带异常堆栈 logger.error("Error processing input: {}", input, e); } } public static void main(String[] args) { MyService service = new MyService(); service.doSomething("test data"); service.doSomething(null); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 绑定原理:StaticLoggerBinder
的魔法
SLF4j 如何在运行时找到并使用具体的日志实现框架呢?答案就在 StaticLoggerBinder
类。
获取
ILoggerFactory
: 当调用LoggerFactory.getLogger()
时,它首先会调用LoggerFactory.getILoggerFactory()
来获取一个ILoggerFactory
实例。ILoggerFactory
是 SLF4j 定义的用于创建Logger
实例的工厂接口。// LoggerFactory.java (部分源码) public static Logger getLogger(String name) { // 1. 获取 ILoggerFactory 实例 ILoggerFactory iLoggerFactory = getILoggerFactory(); // 2. 使用 ILoggerFactory 创建 Logger return iLoggerFactory.getLogger(name); } public static ILoggerFactory getILoggerFactory() { // ... 初始化检查 ... // 3. 通过 StaticLoggerBinder 获取 ILoggerFactory return StaticLoggerBinder.getSingleton().getLoggerFactory(); // ... 异常处理和回退逻辑 ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14StaticLoggerBinder
的角色:StaticLoggerBinder
这个类并不存在于slf4j-api.jar
中。它是由每个 SLF4j 绑定包 (如logback-classic.jar
,slf4j-log4j12.jar
等) 提供的。SLF4j API 在编译时不知道这个类的存在,但在运行时会通过类加载机制查找org.slf4j.impl.StaticLoggerBinder
这个类。绑定实现: 每个绑定包提供的
StaticLoggerBinder
类都有一个单例实例 (getSingleton()
),并且该实例持有一个具体的ILoggerFactory
实现。- Logback 绑定 (
logback-classic.jar
): 其StaticLoggerBinder
返回的是ch.qos.logback.classic.LoggerContext
(它实现了ILoggerFactory
)。 - Log4j 1.x 绑定 (
slf4j-log4j12.jar
): 其StaticLoggerBinder
返回的是org.slf4j.impl.Log4jLoggerFactory
。
// StaticLoggerBinder.java in slf4j-log4j12.jar (简化示例) package org.slf4j.impl; // ... imports ... public class StaticLoggerBinder implements LoggerFactoryBinder { private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); private final ILoggerFactory loggerFactory; // 持有 Log4j 的 LoggerFactory private StaticLoggerBinder() { // 在构造函数中创建具体的 Log4jLoggerFactory loggerFactory = new Log4jLoggerFactory(); // ... 版本检查 ... } public static final StaticLoggerBinder getSingleton() { return SINGLETON; } public ILoggerFactory getLoggerFactory() { return loggerFactory; // 返回 Log4j 的 LoggerFactory } // ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22- Logback 绑定 (
获取具体 Logger: 获取到具体的
ILoggerFactory
(如Log4jLoggerFactory
) 后,调用其getLogger(name)
方法。这个方法内部会创建或获取一个适配器Logger
(如Log4jLoggerAdapter
),该适配器包装了底层日志框架的实际 Logger (如org.apache.log4j.Logger
)。// Log4jLoggerFactory.java in slf4j-log4j12.jar (简化示例) public class Log4jLoggerFactory implements ILoggerFactory { // ... 缓存 loggerMap ... public Logger getLogger(String name) { Logger slf4jLogger = loggerMap.get(name); // 先查缓存 if (slf4jLogger != null) { return slf4jLogger; } else { // 创建底层的 Log4j Logger org.apache.log4j.Logger log4jLogger; if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) log4jLogger = LogManager.getRootLogger(); else log4jLogger = LogManager.getLogger(name); // 创建 SLF4j 的适配器 Logger,包装底层的 Log4j Logger Logger newInstance = new Log4jLoggerAdapter(log4jLogger); // ... 放入缓存 ... return newInstance; } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21日志记录委托: 当应用程序调用 SLF4j 的
Logger
接口方法(如logger.info(...)
)时,实际上是调用了适配器Logger
(如Log4jLoggerAdapter
) 的方法,适配器再将调用委托给底层具体的 Logger (如org.apache.log4j.Logger
) 来完成日志记录。
(上图展示了 SLF4j API 如何通过绑定包最终调用具体日志实现)
# 处理其他日志框架的桥接
SLF4j 还提供了一些桥接 (Bridge) 包,用于将那些不使用 SLF4j API 的框架(如直接使用 Log4j, JUL, JCL 的代码)产生的日志,也重定向到 SLF4j,最终交由 SLF4j 绑定的那个实现框架来处理。例如:
log4j-over-slf4j.jar
: 让使用 Log4j API 的代码,日志实际走向 SLF4j。jul-to-slf4j.jar
: 让使用java.util.logging
API 的代码,日志实际走向 SLF4j。jcl-over-slf4j.jar
: 让使用 JCL API 的代码,日志实际走向 SLF4j。
使用桥接包时,必须从项目中移除被桥接的原始日志框架的 JAR 包(如 log4j.jar
, commons-logging.jar
),否则会产生冲突或死循环。
总结 SLF4j 使用要点:
- 项目中包含
slf4j-api.jar
。 - 项目中包含且仅包含一个 SLF4j 绑定包(如
logback-classic.jar
)。 - (可选)如果需要统一管理项目中其他未使用 SLF4j API 的日志,可以引入相应的桥接包,并移除原始日志框架包。
- 代码中面向
org.slf4j.Logger
和org.slf4j.LoggerFactory
编程。
# 2.4 使用 SLF4j 的优势
- 解耦与灵活性: 应用程序代码与具体的日志实现框架解耦。未来可以轻松切换底层的日志框架(例如从 Logback 切换到 Log4j2),而无需修改任何业务代码,只需更换 SLF4j 绑定包和配置文件即可。这对于库开发者尤其重要。
- 统一日志管理: 可以将项目中所有(包括第三方库)的日志输出统一到一个日志框架进行管理和配置。
- 性能: SLF4j 提供的占位符
{}
语法 (详见第 4 节) 比传统的字符串拼接性能更好,尤其是在日志级别被禁用时,可以避免不必要的字符串对象创建和方法调用开销。
# 3. Spring Boot 日志配置与管理
Spring Boot 对日志记录提供了强大的支持和简化的配置。
# 3.1 默认日志配置
Spring Boot 的 spring-boot-starter
(几乎所有 starter 的基础) 默认包含了 spring-boot-starter-logging
依赖。
<!-- spring-boot-starter-logging (通常无需手动添加) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
2
3
4
5
spring-boot-starter-logging
的主要作用:
- 引入 SLF4j API: 包含了
slf4j-api.jar
。 - 引入 Logback 实现: 包含了
logback-classic.jar
(SLF4j 的原生绑定) 和logback-core.jar
。 - 引入日志桥接包: 包含了
jul-to-slf4j.jar
(将 JUL 日志桥接到 SLF4j) 和log4j-to-slf4j.jar
(将 Log4j API 调用桥接到 SLF4j,注意需要配合排除 Log4j 原始包)。
依赖关系图:
从图中可以看出:
- Spring Boot 底层使用 SLF4j + Logback。
- 通过桥接包,将其他常见日志框架(JUL, Log4j)的输出也引导到 SLF4j,最终由 Logback 处理。
- 存在替换包(如
log4j-to-slf4j
代替log4j
),实现日志框架的统一。
结论: Spring Boot 默认就提供了一套基于 SLF4j + Logback 的、能够自动适配并统一管理多种日志源的日志系统。当你引入其他依赖(如某个第三方库使用了 Log4j)时,Spring Boot 的机制会自动尝试将其日志输出也纳入 SLF4j + Logback 的体系中(可能需要手动排除原始日志框架的依赖以避免冲突)。
SpringBoot 日志核心总结
- 自动适配与默认选择: Spring Boot 自动检测并适配项目中的日志框架,默认选用 SLF4j 作为门面,Logback 作为实现。
- 统一管理: 通过内置的桥接包,将来自 JUL、Log4j 等其他框架的日志输出重定向到 SLF4j,实现日志管理的统一。
- 依赖管理: 引入其他框架时,如果该框架自带了非 SLF4j 的日志实现依赖,建议在 Maven/Gradle 配置中将其排除 (exclude),以避免日志冲突,确保所有日志都由 Spring Boot 配置的 SLF4j + Logback (或其他你选定的组合) 处理。
# 3.2 使用 application.yml
(或 .properties
) 进行基本配置
对于常见的日志配置需求,可以直接在 Spring Boot 的主配置文件 application.yml
或 application.properties
中进行设置。
logging:
# 设置全局或特定包的日志级别
level:
# root 设置根 Logger 的级别,影响所有未明确指定级别的 Logger
root: warn
# 为指定包设置级别,会覆盖 root 级别
com.example.myapp: debug # 设置 com.example.myapp 包及其子包的级别为 debug
org.springframework.web: info # 设置 Spring Web 相关日志为 info
org.hibernate.SQL: trace # 打印 Hibernate 执行的 SQL (需要底层支持)
# 配置日志输出到文件
file:
# 指定日志文件的完整路径和名称。如果指定了 path,则此项设置文件名。
name: logs/my-app.log
# 指定日志文件存储的目录。日志文件名默认为 spring.log。
# name 和 path 通常不同时使用,优先使用 name (如果包含路径)。
# path: /var/log/myapp
# 配置日志文件滚动策略 (默认启用,基于大小)
# logback 特有配置 (如果使用 Logback)
logback:
rollingpolicy:
# 触发滚动的最大文件大小,默认 10MB
max-file-size: 20MB
# 保留日志文件的最大天数,默认不限制
max-history: 7
# 日志文件名模式 (归档文件名)
file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
# 所有归档文件的最大总大小
total-size-cap: 1GB
# 启动时是否清理历史归档文件
clean-history-on-start: false
# 配置日志输出格式 (Pattern)
pattern:
# 控制台输出格式
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%15.15t] %-5level %-40.40logger{39} : %msg%n"
# 文件输出格式 (如果配置了 logging.file.name 或 logging.file.path)
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
# 可选:设置特定 profile 下的格式
# level: "%5p" # 只显示级别
# 常用日志格式占位符解释:
# %d{...} 或 %date{...}: 日期时间,花括号内可指定 SimpleDateFormat 格式,如 yyyy-MM-dd HH:mm:ss.SSS
# %thread 或 %t: 输出日志的线程名
# %-5level 或 %le 或 %p: 日志级别 (INFO, DEBUG等),-5 表示左对齐且宽度为 5
# %logger{length} 或 %c{length}: Logger 名称 (通常是类名),{length} 指定最大长度,超长时会缩略 (通常按包名缩略)
# %msg 或 %m: 日志消息本身
# %n: 换行符
# %F: 输出日志的文件名
# %L: 输出日志的行号
# %M: 输出日志的方法名
# %X{key}: 输出 MDC (Mapped Diagnostic Context) 中指定 key 的值,用于追踪请求等
# %ex 或 %exception 或 %throwable: 输出异常堆栈信息
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 3.3 使用日志框架的原生配置文件
虽然 application.yml
提供了方便的基础配置,但如果需要更复杂、更细粒度的控制(例如配置多个 Appender、按时间滚动、异步日志、过滤器等),或者想利用特定日志框架的高级特性,就需要使用该框架的原生配置文件。
Spring Boot 会自动查找并加载类路径 (classpath) 下特定名称的日志配置文件。
日志系统 | Spring Boot 会加载的配置文件 (按优先级顺序) |
---|---|
Logback (默认) | logback-spring.xml , logback-spring.groovy , logback.xml , logback.groovy |
Log4j 2 | log4j2-spring.xml , log4j2.xml |
JDK (Java Util Logging) | logging.properties |
命名约定与 Spring Boot 特性:
- 标准名称 (
logback.xml
,log4j2.xml
): 如果使用这些标准名称,对应的日志框架会直接加载它们。Spring Boot 不会对其进行额外的处理。你不能在这些文件中使用 Spring Boot 特有的扩展功能(如<springProfile>
)。 - Spring Boot 扩展名称 (
logback-spring.xml
,log4j2-spring.xml
): 推荐使用这些带-spring
后缀的名称。Spring Boot 会识别并解析这些文件,允许你在其中使用 Spring Boot 提供的扩展标签,最常用的就是<springProfile>
。
<springProfile>
标签:
这个标签允许你根据当前激活的 Spring Profile (通过 spring.profiles.active
或环境变量 SPRING_PROFILES_ACTIVE
设置) 来定义条件化的日志配置。这对于在不同环境(开发 dev
, 测试 test
, 生产 prod
)下应用不同的日志策略非常有用。
使用 <springProfile>
的示例 (logback-spring.xml
):
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入 Spring Boot 提供的默认配置基础 (可选,可以简化配置) -->
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- 定义控制台 Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 使用 Spring Boot 默认控制台日志格式变量 -->
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 定义文件 Appender (滚动文件) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 使用 Spring Boot 默认日志文件路径/名称变量 -->
<file>${LOG_FILE}</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- ================== 根据 Spring Profile 配置 Logger ================== -->
<!-- 开发环境 (dev) 配置 -->
<springProfile name="dev">
<logger name="com.example.myapp" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/> <!-- 开发环境主要输出到控制台 -->
</logger>
<root level="INFO"> <!-- Root logger 也输出到控制台 -->
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 生产环境 (prod) 配置 -->
<springProfile name="prod">
<logger name="com.example.myapp" level="INFO" additivity="false">
<appender-ref ref="FILE"/> <!-- 生产环境主要输出到文件 -->
</logger>
<root level="WARN"> <!-- Root logger 只记录 WARN 及以上级别到文件 -->
<appender-ref ref="FILE"/>
</root>
</springProfile>
<!-- 默认配置 (如果没有匹配的 profile 或者没有激活 profile) -->
<!-- 可以省略,或者提供一个通用的配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
何时使用原生配置文件:
- 需要在
application.yml
之外进行更复杂的配置(多 Appender、过滤器、异步日志等)。 - 需要根据 Spring Profile 应用截然不同的日志策略(不仅仅是级别,还包括 Appender、格式等)。
- 需要利用日志框架本身的高级特性。
# 3.4 Logback 常用配置示例 (logback-spring.xml
)
以下是一个常用的 logback-spring.xml
配置示例,包含了按级别分别输出到不同文件、滚动策略、MDC (用于追踪 ID) 等:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds"> <!-- scan: 热加载配置, scanPeriod: 检查间隔 -->
<!-- 引入 Spring Boot 提供的属性,方便引用 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="my-app"/>
<property name="LOG_PATH" value="./logs/${APP_NAME}"/> <!-- 日志根目录 -->
<!-- 控制台 Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- 格式化输出:%d日期,%X{traceId}MDC变量,%thread线程名,%-5level级别,%logger{36}类名缩写,%msg日志,%n换行 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- INFO 级别日志文件 Appender -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/info.log</file> <!-- 当前日志文件 -->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 归档文件名 -->
<fileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize> <!-- 每个文件最大 100MB -->
<maxHistory>30</maxHistory> <!-- 保留最近 30 天的归档 -->
<totalSizeCap>10GB</totalSizeCap> <!-- 所有归档文件总大小上限 -->
</rollingPolicy>
<!-- 只记录 INFO 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch> <!-- 匹配 INFO 级别,接受 -->
<onMismatch>DENY</onMismatch> <!-- 不匹配(其他级别),拒绝 -->
</filter>
</appender>
<!-- WARN 级别日志文件 Appender -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/warn.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/warn-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<!-- 只记录 WARN 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- ERROR 级别日志文件 Appender -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<!-- 只记录 ERROR 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 配置 Logger -->
<!-- 开发环境: 控制台输出 DEBUG, 文件记录 INFO/WARN/ERROR -->
<springProfile name="dev | test"> <!-- 支持 | (或), & (且), ! (非) -->
<logger name="com.example.myapp" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</logger>
<!-- MyBatis SQL 日志 -->
<logger name="com.example.myapp.dao" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- 生产环境: 文件记录 INFO/WARN/ERROR -->
<springProfile name="prod | online">
<logger name="com.example.myapp" level="INFO" additivity="false">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</logger>
<!-- 生产环境一般不打印 SQL 到日志 -->
<logger name="com.example.myapp.dao" level="INFO"/>
<root level="WARN"> <!-- 根 logger 级别设为 WARN,避免记录过多第三方库的 INFO 日志 -->
<appender-ref ref="INFO_FILE"/> <!-- 仍然记录 INFO 文件,但 Root 不会触发 INFO -->
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<!-- <appender-ref ref="CONSOLE"/> --> <!-- 生产环境通常不输出到控制台 -->
</root>
</springProfile>
</configuration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 3.5 切换日志框架 (例如切换到 Log4j 2)
如果你希望将 Spring Boot 项目的日志实现从默认的 Logback 切换到 Log4j 2,需要执行以下步骤:
排除默认日志启动器: 在
pom.xml
中,找到spring-boot-starter-web
或其他引入了spring-boot-starter-logging
的依赖,并添加<exclusions>
来排除它。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency>
1
2
3
4
5
6
7
8
9
10引入 Log4j 2 启动器: 添加
spring-boot-starter-log4j2
依赖。它会自动引入 SLF4j API 以及 Log4j 2 的核心和 API 包。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
1
2
3
4添加 Log4j 2 配置文件: 在
src/main/resources
目录下添加 Log4j 2 的配置文件。推荐使用log4j2-spring.xml
以便利用<SpringProfile>
等 Spring Boot 集成特性。如果使用log4j2.xml
,则无法使用这些特性。log4j2-spring.xml
示例结构:<?xml version="1.0" encoding="UTF-8"?> <!-- Log4j 2 配置根元素 --> <Configuration status="WARN" monitorInterval="30"> <!-- status: Log4j2 内部日志级别; monitorInterval: 自动重载配置间隔 --> <!-- 定义属性变量 --> <Properties> <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property> <Property name="LOG_PATH">./logs</Property> </Properties> <!-- 定义 Appenders --> <Appenders> <!-- 控制台 Appender --> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="${LOG_PATTERN}"/> </Console> <!-- 滚动文件 Appender --> <RollingFile name="File" fileName="${LOG_PATH}/app.log" filePattern="${LOG_PATH}/app-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <TimeBasedTriggeringPolicy /> <!-- 按时间滚动 (基于 filePattern 中的日期) --> <SizeBasedTriggeringPolicy size="100 MB"/> <!-- 按大小滚动 --> </Policies> <DefaultRolloverStrategy max="10"/> <!-- 保留最多 10 个归档文件 --> </RollingFile> </Appenders> <!-- 定义 Loggers --> <Loggers> <!-- 根 Logger --> <Root level="info"> <AppenderRef ref="Console"/> <AppenderRef ref="File"/> </Root> <!-- 特定包的 Logger --> <Logger name="com.example.myapp" level="debug" additivity="false"> <AppenderRef ref="Console"/> <AppenderRef ref="File"/> </Logger> <!-- 使用 SpringProfile --> <SpringProfile name="prod"> <Root level="warn"> <!-- 生产环境提高 Root 级别 --> <AppenderRef ref="File"/> </Root> <Logger name="com.example.myapp" level="info" additivity="false"> <AppenderRef ref="File"/> </Logger> </SpringProfile> </Loggers> </Configuration>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
完成以上步骤后,重新启动 Spring Boot 应用,它就会使用 Log4j 2 作为底层的日志实现了。原有的基于 SLF4j API 的日志代码无需任何修改。
# 4. SLF4j 占位符 {}
:高效简洁的日志记录
SLF4j API 的一个显著特点是它推荐使用占位符 {}
来进行日志消息的参数化,而不是使用传统的字符串拼接 (+
)。
# 4.1 占位符用法
在日志消息字符串中使用 {}
作为变量的占位符,然后在该字符串后面依次传入对应的变量作为参数。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PlaceholderExample {
private static final Logger logger = LoggerFactory.getLogger(PlaceholderExample.class);
public static void main(String[] args) {
String user = "Alice";
int orderId = 12345;
double amount = 99.99;
// 使用占位符记录日志
// SLF4j 会按顺序将 user, orderId, amount 的值替换到 {} 的位置
logger.info("User '{}' placed order ID {} with amount {}", user, orderId, amount);
// 输出示例: INFO User 'Alice' placed order ID 12345 with amount 99.99
// 如果最后一个参数是 Throwable,它会被特殊处理,用于打印堆栈跟踪,而不是替换最后一个 {}
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
logger.error("Error calculating result for user {} and order {}", user, orderId, e);
// 输出示例: ERROR Error calculating result for user Alice and order 12345
// java.lang.ArithmeticException: / by zero
// at ... (堆栈信息) ...
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 4.2 占位符的优势:性能与简洁
相比于传统的字符串拼接方式:
// 传统方式 (性能较低)
String user = "Alice";
int orderId = 12345;
// logger.debug("User '" + user + "' placed order ID " + orderId); // 不推荐
2
3
4
使用 SLF4j 占位符主要有两个优势:
性能提升 (懒计算):
- 避免不必要的字符串拼接: 在传统方式中,即使
DEBUG
级别最终被禁用,"User '" + user + "' placed order ID " + orderId
这段字符串拼接操作仍然会执行,产生了临时的String
对象,造成 CPU 和内存的浪费。 - 延迟参数构造: SLF4j 的
logger.debug("...", arg1, arg2)
方法内部会先检查DEBUG
级别是否启用。只有在级别启用时,才会真正地将参数arg1
,arg2
转换为字符串并与消息模板组合成最终的日志字符串。如果级别未启用,则后面的参数(包括可能的方法调用getSomeData()
)根本不会被执行或评估。
// 只有当 DEBUG 级别启用时,getComplexData() 才会被调用,toString() 才会被执行 // logger.debug("Processing complex data: {}", getComplexData());
1
2- 避免不必要的字符串拼接: 在传统方式中,即使
代码更简洁、可读性更高: 使用占位符使得日志消息模板本身更清晰,避免了大量的
+
号,提高了代码的可读性和可维护性。
因此,强烈推荐在所有使用 SLF4j 的日志记录场景中,始终使用 {}
占位符的方式来传递参数。