Appearance
第49课:日志框架
🎯 学习目标
- 理解日志不是
System.out.println的替代品,而是线上系统的可观测性基础。 - 掌握 SLF4J、Logback、Log4j2、JUL 之间的关系,知道项目里应该依赖哪一层。
- 能在 Spring Boot 项目中配置日志级别、日志格式、文件滚动和异步日志。
- 能写出可检索、可定位、低开销的业务日志。
- 能识别日志泄露、重复打印、吞异常、字符串拼接等常见问题。
📖 一、为什么需要日志框架
Java 程序在本地运行时,System.out.println() 看起来足够;但一旦进入企业项目,它很快暴露问题:
| 问题 | System.out.println | 日志框架 |
|---|---|---|
| 级别控制 | 无法按 DEBUG/INFO/ERROR 控制 | 支持按包、类、环境动态控制 |
| 输出位置 | 通常只能到控制台 | 控制台、文件、远程采集系统 |
| 格式统一 | 手写,容易不一致 | 统一时间、线程、级别、类名、traceId |
| 性能 | 字符串常被提前拼接 | 支持参数化、异步、缓冲 |
| 运维检索 | 信息零散 | 可被 ELK、Loki、Cloudflare Logs 等采集 |
日志的核心价值不是“打印信息”,而是回答线上问题:
text
谁在什么时间调用了哪个接口?
传入了什么关键业务参数?
系统在哪一步失败?
失败是业务错误、参数错误、网络错误,还是依赖服务错误?
一次请求跨多个服务时,如何串起来看完整链路?因此,日志设计属于系统设计的一部分。
📖 二、Java 日志体系
Java 日志生态容易混乱,因为它分为“门面”和“实现”。
1. 日志门面
门面只定义 API,不负责真正写日志。业务代码应该依赖门面。
| 门面 | 说明 |
|---|---|
| SLF4J | 企业项目最常用,推荐业务代码直接使用 |
| Commons Logging | Spring 早期常见,现在通常由 Spring 内部兼容 |
| JUL | JDK 自带 java.util.logging,能力较弱 |
2. 日志实现
实现负责真正输出日志。
| 实现 | 说明 |
|---|---|
| Logback | Spring Boot 默认实现,配置简单,生态稳定 |
| Log4j2 | 性能强,异步日志能力突出 |
| JUL | JDK 自带实现,企业项目很少直接选它 |
3. 推荐组合
大多数 Spring Boot 项目直接使用:
text
业务代码:SLF4J API
底层实现:Logback
Spring Boot starter:spring-boot-starter-logging业务代码不要直接依赖 Logback API,否则以后切换 Log4j2 会很麻烦。
📖 三、基本用法
1. 创建 Logger
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void createOrder(Long userId, Long productId) {
log.info("create order start, userId={}, productId={}", userId, productId);
try {
// 业务逻辑
log.info("create order success, userId={}, productId={}", userId, productId);
} catch (Exception e) {
log.error("create order failed, userId={}, productId={}", userId, productId, e);
throw e;
}
}
}注意最后一行:
java
log.error("create order failed, userId={}, productId={}", userId, productId, e);异常对象 e 必须作为最后一个参数传入,这样日志框架会输出完整堆栈。
2. 参数化日志
推荐:
java
log.info("user login success, userId={}, ip={}", userId, ip);不推荐:
java
log.info("user login success, userId=" + userId + ", ip=" + ip);原因是字符串拼接会提前执行,即使当前日志级别不会输出这条日志,拼接成本也已经产生。
📖 四、日志级别
常见级别从低到高:
| 级别 | 使用场景 |
|---|---|
| TRACE | 极细粒度调试,通常只在框架或临时排查中使用 |
| DEBUG | 开发和测试环境排查细节 |
| INFO | 关键业务流程,生产环境默认可见 |
| WARN | 可恢复异常、降级、重试、参数边界问题 |
| ERROR | 当前操作失败,需要开发或运维关注 |
推荐标准
text
DEBUG:开发排查细节,例如 SQL 参数、分支判断。
INFO :业务关键节点,例如下单成功、支付回调、任务开始结束。
WARN :系统还能继续运行,但出现了异常信号。
ERROR:请求失败、任务失败、数据不一致、依赖不可用。不要把所有日志都打成 INFO,否则生产环境会被无用日志淹没。
📖 五、Spring Boot 日志配置
1. application.yml 基础配置
yaml
logging:
level:
root: INFO
com.example.demo: DEBUG
org.springframework.web: INFO
file:
name: logs/app.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n"这适合简单项目。更复杂的滚动策略、异步日志、脱敏逻辑,建议使用 logback-spring.xml。
2. logback-spring.xml 示例
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>
<property name="LOG_PATH" value="logs"/>
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{traceId:-}] %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>这里的 %X{traceId:-} 来自 MDC,用于把一次请求的链路 ID 打入每条日志。
📖 六、MDC 与 traceId
MDC 是 Mapped Diagnostic Context,可以为当前线程绑定上下文信息。
1. 请求过滤器示例
java
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import java.io.IOException;
import java.util.UUID;
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String traceId = httpRequest.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
try {
MDC.put("traceId", traceId);
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}2. 为什么必须 clear
Tomcat 线程池会复用线程。如果请求结束后不清理 MDC,下一个请求可能继承上一个请求的 traceId,导致链路串线。
⚠️ 七、常见陷阱
1. 吞异常
错误示例:
java
try {
paymentClient.pay(orderId);
} catch (Exception e) {
log.error("pay failed");
}问题是堆栈丢失,无法定位。
正确示例:
java
try {
paymentClient.pay(orderId);
} catch (Exception e) {
log.error("pay failed, orderId={}", orderId, e);
throw e;
}2. 重复打印异常
Controller、Service、DAO 每层都 log.error,会导致同一个异常刷屏三次。通常只在边界层或真正处理异常的位置打印。
3. 打印敏感信息
不要直接打印:
text
身份证号
手机号完整值
银行卡号
密码
Token
Cookie
Authorization Header生产日志一旦被采集到集中系统,泄露范围会扩大。
4. 日志级别滥用
业务失败不一定是 ERROR。例如用户密码错误、库存不足、参数校验失败,通常属于正常业务分支,适合 INFO 或 WARN。
🆚 八、Java vs C 对比
| 维度 | C 常见做法 | Java 企业项目 |
|---|---|---|
| 输出方式 | printf、文件写入、syslog | SLF4J + Logback/Log4j2 |
| 上下文 | 手动拼接线程、请求信息 | MDC 自动注入 traceId |
| 日志级别 | 需要自行封装 | 框架内置级别 |
| 文件滚动 | 手写或依赖系统工具 | 框架配置滚动策略 |
| 异步输出 | 需要自行实现队列 | 框架支持异步 appender |
Java 日志框架把“输出、格式、级别、滚动、采集”都标准化了。业务代码只需要表达事件本身。
💡 九、最佳实践
- 业务代码只依赖 SLF4J,不直接依赖 Logback 或 Log4j2 API。
- 日志必须带关键业务 ID,例如
userId、orderId、requestId。 - 生产默认
INFO,排查问题时临时打开某个包的DEBUG。 - 异常日志必须带异常对象,不能只打印
e.getMessage()。 - 高频循环里少打日志,必要时采样或聚合。
- 不在日志里输出敏感数据,必须输出时先脱敏。
- 接口入口、外部依赖调用、异步任务开始结束、关键状态变更都应该有日志。
- 同一个异常只在一个边界位置打印,避免重复刷屏。
🎓 小结
日志框架是企业 Java 项目的基础设施。一个合格的日志系统至少要做到:统一格式、正确级别、包含业务上下文、可按 traceId 检索、不会泄露敏感信息、不会拖垮性能。
掌握日志后,后续学习 JUnit、JSON、Validation、Swagger 和 Spring Boot 项目时,才能把“程序能跑”提升到“线上可排查、可维护”。
🧭 十、项目落地清单
新建 Spring Boot 项目时,日志部分至少完成这些配置:
text
1. 统一使用 SLF4J API。
2. 确认底层实现是 Logback 或 Log4j2,避免多个实现冲突。
3. 配置控制台日志格式,开发环境方便阅读。
4. 配置文件日志滚动,生产环境避免单文件无限增长。
5. 日志格式包含时间、级别、线程、logger、traceId。
6. 接入全局 traceId 过滤器或链路追踪系统。
7. 为不同包设置合理级别,例如业务 INFO、SQL DEBUG 仅测试环境开启。
8. 统一异常打印位置,避免重复堆栈。
9. 建立敏感字段脱敏规则。
10. 确认日志采集系统能按 traceId、orderId、userId 检索。示例日志应该长这样:
text
2026-06-30 21:10:12.123 INFO [http-nio-8080-exec-1] [traceId=8f1a...] c.e.OrderService - create order success, orderId=1001, userId=20不应该长这样:
text
success
error
用户请求了接口
java.lang.NullPointerException日志必须能帮助一个没有上下文的人定位问题。
🔍 十一、自测问题
学习完本节后,应该能回答:
text
SLF4J 和 Logback 分别是什么?
为什么业务代码不应该直接依赖 Logback API?
DEBUG、INFO、WARN、ERROR 的边界是什么?
为什么异常对象要作为 log.error 的最后一个参数?
为什么日志里需要 traceId?
为什么 MDC 使用后必须 clear?
为什么不能在生产环境随意打开 SQL DEBUG?
为什么不能打印 Token 和密码?如果这些问题答不上来,说明还只是会“打印日志”,没有真正掌握企业项目日志设计。
📌 十二、学习建议
学习日志框架时,建议自己做一次小实验:
text
1. 写一个接口,入口打印 traceId。
2. 在 Service 中打印 orderId 和 userId。
3. 人为抛出异常,确认日志有完整堆栈。
4. 把日志级别从 INFO 改成 DEBUG,观察输出变化。
5. 配置文件滚动,确认不会无限写入单个日志文件。这比只看配置更容易建立直觉。