Skip to content

第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 LoggingSpring 早期常见,现在通常由 Spring 内部兼容
JULJDK 自带 java.util.logging,能力较弱

2. 日志实现 ​

实现负责真正输出日志。

实现说明
LogbackSpring Boot 默认实现,配置简单,生态稳定
Log4j2性能强,异步日志能力突出
JULJDK 自带实现,企业项目很少直接选它

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、文件写入、syslogSLF4J + 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. 配置文件滚动,确认不会无限写入单个日志文件。

这比只看配置更容易建立直觉。