《Spring Boot 入门》16.1 日志配置与级别

本节为图书服务建立生产可用的日志基线:讲清 SLF4J 门面与 Logback 实现的分工,以及为什么不要直接依赖 Log4j2 API;给出六个级别的选择依据与 logging.level.<包名> 的精确控制;解读 4.1.1 的真实启动日志;讲透 logging.file.name 与 path 的差别、轮转配置、4.1 的 Log4j 文件轮转与 logging.group 分组。

本节目标:为图书管理服务配好一套生产可用的日志基线,读懂启动日志的每一段,并用 logging.* 属性控制级别、格式、文件与轮转。
适用版本:Spring Boot 4.1.x(Java 21)

16.1 日志配置与级别

上一章我们把数据层收尾:Flyway 迁移、多环境数据源都落地了。但从这一章开始,我们要给图书服务装上「可观测的眼睛」——日志。一个能跑的系统和一个能运维的系统,差别往往就在日志上:线上报错时,日志是你唯一能复盘现场的东西。

本节先把地基打好:搞清 Spring Boot 用的日志体系、把级别调对、把启动日志读懂、再配上文件和轮转。结构化日志与工程实践放到 16.2、16.3。

16.1.1 日志门面与实现:SLF4J + Logback

新手最容易困惑的一点是:pom.xml 里明明没引日志依赖,代码里却能直接用 Logger。答案是 Spring Boot 已经内置了一套日志方案。

Java 日志生态分成两层:

层角色常见实现
门面(Facade)代码里调用的 APISLF4J、Commons Logging
实现(Binding)真正干活的引擎Logback、Log4j2、JUL

Spring Boot 的默认组合是 SLF4J 做门面、Logback 做实现,两者都由 spring-boot-starter-logging 带入,而它被所有 starter 间接依赖,所以你什么都不用配就能打日志。

为什么要在门面后面写代码,而不是直接调 Logback 或 Log4j2 的 API?

  • 可替换:门面是接口,实现只是运行时的一个 jar。换实现不用改业务代码,pom.xml 排除旧实现、引入新实现即可。
  • 不锁死:直接 import org.apache.logging.log4j.LogManager 会把整个项目绑死在 Log4j2 上。哪天要换成 Logback,改动面是全项目,而不是一处配置。
  • 框架统一:Spring、Hibernate、Tomcat 内部用的门面各不相同,SLF4J 通过桥接包把它们的输出统一汇到同一个实现,你才能用一个 logging.level.* 管住所有框架。

所以正确写法只有一种:面向 org.slf4j.Logger 编程。

package com.example.library.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class BookService {

    private static final Logger log = LoggerFactory.getLogger(BookService.class);

    private final BookRepository repository;

    public BookService(BookRepository repository) {
        this.repository = repository;
    }

    public Book findById(Long id) {
        log.debug("查询图书 id={}", id);
        Book book = repository.findById(id).orElseThrow(() -> new BookNotFoundException(id));
        log.info("命中图书 id={}, title={}", id, book.getTitle());
        return book;
    }
}

LoggerFactory.getLogger(BookService.class) 把 logger 的名字绑定到类名,这是后面 logging.level.com.example.library=DEBUG 能按包精确控级的前提。字段声明成 static final 是约定,避免每个实例重复创建。

16.1.2 六个级别与选择依据

SLF4J 定义了六个级别,从低到高排列:

级别语义典型用途
TRACE最细粒度,通常只用于框架内部逐帧、逐字节的排查
DEBUG开发/排查用入参、分支、SQL 参数
INFO关键流程节点启动、请求摘要、状态变更
WARN潜在问题,尚可运行重试、降级、废弃用法
ERROR出错了,需要人处理异常、失败、数据不一致
OFF关闭该 logger屏蔽噪音

「级别」的真正含义是阈值:给一个 logger 设成 INFO,意味着 TRACE 和 DEBUG 被丢弃,INFO、WARN、ERROR 才输出。不是「只输出 INFO」。

选择依据可以归纳成一句话:看这个事件需不需要人主动采取行动。

  • 需要人半夜爬起来处理的 → ERROR;
  • 需要人白天关注、但暂时不影响用户 → WARN;
  • 记录了「系统做过什么」,平时没人看但出事时必需 → INFO;
  • 只在排查某个具体问题时才有价值 → DEBUG。

生产环境的默认根级别是 INFO。不要一上来把根级别调成 DEBUG:日志量会暴涨几十倍,磁盘、网络、采集成本一起上升,真正的 ERROR 反而被淹没。

16.1.3 用 logging.level 精确控制

想临时看某个包的细节,不用改代码,加一行属性即可。logging.level 接受「logger 名 = 级别」的映射,logger 名就是 getLogger 传进去的那个名字(惯例是类全名)。

logging:
  level:
    root: INFO
    com.example.library: DEBUG
    com.example.library.service: DEBUG
    org.springframework.web: INFO
    org.hibernate.SQL: DEBUG

要点:

  • root 是兜底:没有更具体的匹配时用它。
  • 最长前缀优先:com.example.library.service 比 com.example.library 更具体,前者覆盖后者。
  • 可以精确到单个类:com.example.library.service.BookService: TRACE。
  • org.hibernate.SQL: DEBUG 会打印 SQL 语句,org.hibernate.orm.jdbc.bind: TRACE 会连参数一起打印——这是排查 ORM 问题的经典组合,只在本地开。

同样的配置用 properties 写:

logging.level.root=INFO
logging.level.com.example.library=DEBUG
logging.level.org.hibernate.SQL=DEBUG

日志分组能让你把一组包当整体开关,避免每处都写一遍。logging.group.<名字> 定义一组 logger,然后像用单个 logger 一样给这个组设级别:

logging:
  group:
    web: org.springframework.web,org.springframework.security
    library: com.example.library
  level:
    web: DEBUG
    library: DEBUG

Spring Boot 内置了几个常用分组,比如 web(Spring MVC 相关)、sql(JPA/Hibernate 相关),直接 logging.level.sql=DEBUG 即可。

16.1.4 读懂 4.1.1 的真实启动日志

配好级别后,回到最常见的那屏输出。下面是本机 Spring Boot 4.1.1 的真实启动日志(Java 21,PID 43496):

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.1.1)

2026-10-09T15:42:06.435+08:00  INFO 43496 --- [           main] com.example.probe.ProbeApplication       : Starting ProbeApplication v0.0.1-SNAPSHOT using Java 21.0.12.1 with PID 43496
2026-10-09T15:42:06.436+08:00  INFO 43496 --- [           main] com.example.probe.ProbeApplication       : No active profile set, falling back to 1 default profile: "default"
2026-10-09T15:42:07.027+08:00  INFO 43496 --- [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-10-09T15:42:07.039+08:00  INFO 43496 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-10-09T15:42:07.059+08:00  INFO 43496 --- [           main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 589 ms
2026-10-09T15:42:07.285+08:00  INFO 43496 --- [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-10-09T15:42:07.421+08:00  INFO 43496 --- [           main] com.example.probe.ProbeApplication       : Started ProbeApplication in 1.101 seconds (process running for 1.749)
2026-10-09T15:42:07.840+08:00  INFO 43496 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms

一行日志按空格分隔的固定列读:

列例子含义
时间戳2026-10-09T15:42:06.435+08:00ISO-8601,毫秒精度,带时区偏移
级别INFO右对齐占 5 字符
PID43496进程号,多实例部署时用来区分
分隔符---固定三横线,标记「元数据结束」
线程名[ main]中括号内,方括号外补空格对齐
logger 名com.example.probe.ProbeApplication缩写规则见下
消息Starting ProbeApplication ...冒号后的正文

两个容易忽略的细节:

  • 线程名告诉你这段代码跑在哪个线程。启动阶段是 main;处理请求时变成 [nio-8080-exec-1];优雅停机时是 [ionShutdownHook](ApplicationShutdownHook 被截断)、[tomcat-shutdown]。排查线程池、异步、死锁时,这一列是入口。
  • logger 名被缩写:o.s.boot.tomcat.TomcatWebServer 是 org.springframework.boot.tomcat.TomcatWebServer,b.w.c.s.WebApplicationContextInitializer 是 org.springframework.boot.web.context.support.WebApplicationContextInitializer。规则是包名的每一段保留首字母,最后一段类名完整保留。这样长包名不会撑爆一行。

这里还藏着一个 4.x 的重要信号:Tomcat 相关的 logger 是 o.s.boot.tomcat.TomcatWebServer,而不是 3.x 的 o.s.b.w.embedded.tomcat.TomcatWebServer。这是 4.0 模块化重构的直接证据——每个技术栈被拆成独立模块 spring-boot-<technology>,根包变成 org.springframework.boot.<technology>。当你按 3.x 的包名去写 logging.level.org.springframework.boot.web.embedded.tomcat=DEBUG 却不生效时,就是这个原因。

16.1.5 定制输出格式

默认格式够用,但团队往往要统一格式或加字段。用 logging.pattern.console 改控制台、logging.pattern.file 改文件(两者默认不同:文件格式不带颜色)。

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

常用占位符:

占位符输出说明
%d{格式}时间括号内是日期格式
%thread线程名当前线程
%-5level级别- 左对齐,5 固定宽度
%logger{36}logger 名超长时缩写到 36 字符
%msg / %m消息正文
%n换行不要用 \n,%n 才跨平台
%clr(...)颜色Logback 扩展,见下一节

不要在 pattern 里手写 \n 当换行,用 %n;否则在 Windows 上会得到 \r\n 与 \n 混用。

16.1.6 输出到文件与轮转

生产环境不能只靠控制台。logging.file.* 有两个属性,含义不同,常被搞混:

属性值形态结果
logging.file.name文件名(可带路径)写到该文件,如 logs/book-service.log
logging.file.path目录在该目录下写名为 spring.log 的文件

两个都设时 logging.file.name 赢。经验法则:想要确定的文件名用 name,只关心放哪个目录用 path。

logging:
  file:
    name: logs/book-service.log
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
      total-size-cap: 1GB

logging.logback.rollingpolicy.* 控制轮转(这些属性仅在 Logback 生效):

属性含义默认
max-file-size单个文件上限,超过即切分10MB
max-history保留多少天的归档7
total-size-cap所有归档的总大小上限0(不限)
clean-history-on-start启动时是否清理旧归档false
file-name-pattern归档文件名模板${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz

轮转后的文件形如 book-service.log.2026-10-08.0.gz:按天滚动、同日多份用序号 .0 .1 区分、压缩存放。total-size-cap 是防磁盘打满的关键,生产环境务必设一个上限。

16.1.7 4.1 的 Log4j 文件轮转

前面讲的是 Logback。如果项目换用了 Log4j2(排除 spring-boot-starter-logging、引入 spring-boot-starter-log4j2),4.0 之前 Spring Boot 不会替你配置文件轮转,得手写 log4j2-spring.xml。

4.1 补齐了这块:现在 logging.log4j2.rollingpolicy.* 提供了与 Logback 对齐的轮转属性,文件输出开箱即用。

logging:
  file:
    name: logs/book-service.log
  log4j2:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
      total-size-cap: 1GB

也就是说,4.1 起无论底层是 Logback 还是 Log4j2,logging.file.name + logging.*.rollingpolicy.* 的配置方式一致,迁移实现时这部分配置几乎不用改。

16.1.8 控制台颜色开关

彩色日志让人一眼分清级别,但输出被重定向到文件或采集器时,ANSI 转义码会变成乱码。Spring Boot 4.0 新增了 logging.console.enabled 用来整体开关控制台输出:

logging:
  console:
    enabled: true

这个属性控制的是是否向控制台输出(默认 true)。当进程把日志完全交给文件、不需要 stdout 时,设成 false 可省掉一份重复输出。

至于颜色本身,由 spring.output.ansi.enabled 控制,取值 ALWAYS / DETECT / NEVER。默认 DETECT:检测到终端才上色,重定向到文件时自动关闭。容器里日志乱码,通常就是这里配成了 ALWAYS。

16.1.9 本节常见坑速查

现象原因处理
设了 logging.level.* 没生效按 3.x 包名写的(如 o.s.b.w.embedded.tomcat)改用 4.x 包名 org.springframework.boot.tomcat
日志文件里全是乱码强制开了 ANSI 颜色spring.output.ansi.enabled=DETECT
磁盘被日志打满没设 total-size-cap设上限并配 max-history
只设了 logging.file.path 却找不到期望的文件名目录模式下文件名固定为 spring.log要指定名就用 logging.file.name
生产日志太多根级别被调成 DEBUG只对具体包开 DEBUG

小结

  • Spring Boot 默认用 SLF4J 门面 + Logback 实现,业务代码只依赖 org.slf4j.Logger,永远不要直接调具体实现的 API。
  • 级别是阈值:设 INFO 会丢弃 TRACE/DEBUG。选择依据是「需不需要人行动」——ERROR 要处理、WARN 需关注、INFO 记关键流程、DEBUG 排查时开。
  • logging.level.<logger>=级别 按最长前缀匹配;logging.group.* 可把一组包当整体开关。
  • 启动日志一行分七列:时间戳、级别、PID、---、线程名、logger 名(包名缩写、类名全留)、消息。4.x 的 o.s.boot.tomcat.* 是模块化的证据。
  • 格式用 logging.pattern.console / file;换行用 %n。
  • 文件输出用 logging.file.name(文件)或 logging.file.path(目录,固定名 spring.log),两者同时设时 name 赢;轮转用 logging.logback.rollingpolicy.*。
  • 4.1 新增 logging.log4j2.rollingpolicy.*,让 Log4j2 也有了开箱即用的文件轮转。
  • 4.0 新增 logging.console.enabled 控制是否输出到控制台;颜色由 spring.output.ansi.enabled 控制。

日志已经能落到文件并轮转了,但它还是给人看的纯文本。下一节我们把它变成机器能检索的结构化 JSON,并解决线程池里上下文丢失的问题。

阅读导航:上一节:15.3 多环境数据初始化 · 下一节:16.2 结构化日志 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计