日志
Spring Boot 默认使用 SLF4J(日志门面)+ Logback(实现),开箱即用,无需额外配置即可输出日志。
日志门面与实现
代码层 SLF4J API(接口)
│
实现层 Logback(默认)/ Log4j2 / JUL
始终通过 SLF4J 编写代码,不直接引用 Logback 类,方便切换实现:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
// Lombok @Slf4j 等价写法,推荐
// @Slf4j public class OrderService { ... }
public void create(Order order) {
log.debug("创建订单, orderId={}", order.getId());
log.info("订单创建成功, orderId={}, userId={}", order.getId(), order.getUserId());
log.warn("库存不足, productId={}, remaining={}", order.getProductId(), 0);
log.error("订单创建失败", e);
}
}基础配置(application.yml)
logging:
level:
root: INFO # 全局默认级别
com.example: DEBUG # 指定包开启 DEBUG
com.example.service.OrderService: TRACE # 指定类开启 TRACE
org.springframework.web: DEBUG # Spring MVC 请求日志
org.hibernate.SQL: DEBUG # 打印 SQL
org.hibernate.orm.jdbc.bind: TRACE # 打印 SQL 绑定参数
# 日志文件输出(二选一)
file:
name: logs/app.log # 指定文件路径(包含文件名)
# path: logs/ # 只指定目录,文件名固定为 spring.log
# 控制台输出格式
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"
# 滚动归档配置(Spring Boot 2.7+)
logback:
rollingpolicy:
max-file-size: 100MB # 单个文件最大
max-history: 30 # 保留最近 30 天
total-size-cap: 3GB # 所有归档文件总大小上限
file-name-pattern: logs/app-%d{yyyy-MM-dd}.%i.log.gz日志级别
TRACE < DEBUG < INFO < WARN < ERROR
| 级别 | 用途 |
|---|---|
TRACE | 最细粒度,方法入参/出参,仅开发调试 |
DEBUG | 关键中间状态,SQL、HTTP 请求详情 |
INFO | 业务关键节点(订单创建、用户登录)、启动信息 |
WARN | 可恢复的异常、降级、慢查询告警 |
ERROR | 需要人工介入的错误,必须携带异常堆栈 |
生产环境建议:root 级别设为 WARN,业务包设为 INFO,避免日志爆炸。
logback-spring.xml(精细控制)
application.yml 满足大多数场景,若需要条件配置或多 Appender,改用 logback-spring.xml(支持 Spring Profile):
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入 Spring Boot 默认基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="appName"
source="spring.application.name" defaultValue="app"/>
<!-- ── 控制台 Appender ── -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ── 滚动文件 Appender ── -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${appName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${appName}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- ── JSON 结构化日志 Appender(用于日志聚合) ── -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${appName}-json.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${appName}-json-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
<!-- ── 异步包装(减少日志 I/O 对主线程的影响) ── -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold> <!-- 0=不丢弃,默认 80% 满时丢 TRACE/DEBUG/INFO -->
<queueSize>512</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- ── Profile 条件配置 ── -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.example" level="TRACE"/>
</springProfile>
<springProfile name="prod">
<root level="WARN">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="JSON_FILE"/>
</root>
<logger name="com.example" level="INFO"/>
</springProfile>
</configuration>JSON 结构化日志需引入 logstash-logback-encoder:
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'MDC(Mapped Diagnostic Context)
MDC 是线程本地的键值存储,记录的字段会自动追加到该线程的每条日志中:
// Filter 中统一注入请求上下文
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", currentUser.getId().toString());
try {
chain.doFilter(request, response);
} finally {
MDC.clear(); // 请求结束后必须清空,防止线程池复用时污染
}Pattern 中通过 %X{key} 引用 MDC 值:
%d{HH:mm:ss} [%X{requestId}] [%X{userId}] %-5level - %msg%n
链路追踪中 traceId / spanId 也通过 MDC 注入,详见 链路追踪。
切换到 Log4j2
Log4j2 的异步性能优于 Logback(LMAX Disruptor),高吞吐场景可考虑切换:
configurations {
all {
// 排除 Logback
exclude group: 'ch.qos.logback', module: 'logback-classic'
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
}配置文件改用 log4j2-spring.xml,其余 SLF4J 代码无需改动。
常用日志实践
不要用字符串拼接(每次都创建对象,即使该级别被过滤也执行):
// 错误:无论级别是否开启,都会拼接字符串
log.debug("用户信息: " + user.toString());
// 正确:占位符,仅在 DEBUG 开启时才格式化
log.debug("用户信息: {}", user);
// 极端性能场景:先判断级别
if (log.isDebugEnabled()) {
log.debug("复杂对象: {}", buildExpensiveDebugInfo());
}异常日志必须传入 Throwable:
// 错误:只打了消息,没有堆栈
log.error("处理失败: " + e.getMessage());
// 正确:消息 + Throwable(Logback 自动打完整堆栈)
log.error("处理订单失败, orderId={}", orderId, e);相关链接
- 链路追踪 — Micrometer Tracing 自动注入 traceId / spanId 到 MDC
- 日志聚合 — JSON 结构化日志 + ELK / Loki 集中收集
- 环境与Profile —
<springProfile>区分开发/生产日志配置 - 过滤器与拦截器 — 在 Filter 中注入 MDC 请求上下文
- 性能调优 — 异步 Appender 与 Log4j2 高吞吐配置