> **Spring 系列 · 第 4 篇**
> 面向 Java 初学者和有一定 Spring 使用经验的开发者。
> 技术环境:Java 17+ · Spring Boot 3.4.x(兼容 3.x / 4.x)· SLF4J · Logback
> 示例项目:spring-logging-demo(./spring-logging-demo/)**(订单 API + 完整日志配置)
系列文章目录

> 系列阅读建议:第 1、2 篇帮你搭好 Spring 骨架;本文聚焦**日志**------线上出问题 80% 靠它定位。建议 clone 示例项目,边看边跑。
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 系列文章目录
- [- 导语(#导语)
- 一、日志到底是什么(#一日志到底是什么)
- 1.1 一句话结论(#11-一句话结论)
- 1.2 日志解决了什么问题(#12-日志解决了什么问题)
- 1.3 日志 vs System.out.println()(#13-日志-vs-systemoutprintln)
- 1.4 为什么生产环境不能只依赖控制台(#14-为什么生产环境不能只依赖控制台)
- 二、Spring 日志体系的核心组件(#二spring-日志体系的核心组件)
- 2.1 一句话结论(#21-一句话结论)
- 2.2 生活类比:统一插座与具体电器(#22-生活类比统一插座与具体电器)
- 2.3 组件关系类图(#23-组件关系类图)
- 2.4 Spring 本身如何使用日志(#24-spring-本身如何使用日志)
- 2.5 常见错误(#25-常见错误)
- 三、Spring Boot 默认日志机制(#三spring-boot-默认日志机制)
- 3.1 默认实现与级别(#31-默认实现与级别)
- 3.2 默认日志格式拆解(#32-默认日志格式拆解)
- 3.3 用 application.yml 配置(#33-用-applicationyml-配置)
- 3.4 多环境不同级别(#34-多环境不同级别)
- 四、如何在 Java 代码中正确打印日志(#四如何在-java-代码中正确打印日志)
- 4.1 推荐的 Logger 定义方式(#41-推荐的-logger-定义方式)
- 4.2 日志级别「阶梯」(#42-日志级别阶梯)
- 4.3 参数化日志 vs 字符串拼接(#43-参数化日志-vs-字符串拼接)
- 4.4 异常日志:保留堆栈(#44-异常日志保留堆栈)
- 4.5 一次 HTTP 请求的日志对象图(#45-一次-http-请求的日志对象图)
- 五、Spring Boot 日志配置实战(#五spring-boot-日志配置实战)
- 5.1 示例项目结构(#51-示例项目结构)
- 5.2 logback-spring.xml 关键配置(#52-logback-springxml-关键配置)
- 5.3 配置优先级(由高到低)(#53-配置优先级由高到低)
- 六、常见问题排查(#六常见问题排查)
- 6.1 日志完全没有输出(#61-日志完全没有输出)
- 6.2 级别配置不生效(#62-级别配置不生效)
- 6.3 同一条日志重复打印 2~3 次(#63-同一条日志重复打印-23-次)
- 6.4 其他高频坑(#64-其他高频坑)
- 七、日志使用最佳实践(#七日志使用最佳实践)
- 7.1 什么该记、什么不该记(#71-什么该记什么不该记)
- 7.2 生产环境设计要点(#72-生产环境设计要点)
- 八、MDC 与链路追踪简介(#八mdc-与链路追踪简介)
- 8.1 MDC 是什么(#81-mdc-是什么)
- 8.2 它解决什么问题(#82-它解决什么问题)
- 8.3 示例:TraceIdFilter(#83-示例traceidfilter)
- 8.4 进阶概念(点到为止)(#84-进阶概念点到为止)
- 总结(#总结)
- 常见面试题 / 读者自测题(#常见面试题--读者自测题)](#导语) - 一、日志到底是什么 - 1.1 一句话结论 - 1.2 日志解决了什么问题 - 1.3 日志 vs System.out.println() - 1.4 为什么生产环境不能只依赖控制台 - 二、Spring 日志体系的核心组件 - 2.1 一句话结论 - 2.2 生活类比:统一插座与具体电器 - 2.3 组件关系类图 - 2.4 Spring 本身如何使用日志 - 2.5 常见错误 - 三、Spring Boot 默认日志机制 - 3.1 默认实现与级别 - 3.2 默认日志格式拆解 - 3.3 用 application.yml 配置 - 3.4 多环境不同级别 - 四、如何在 Java 代码中正确打印日志 - 4.1 推荐的 Logger 定义方式 - 4.2 日志级别「阶梯」 - 4.3 参数化日志 vs 字符串拼接 - 4.4 异常日志:保留堆栈 - 4.5 一次 HTTP 请求的日志对象图 - 五、Spring Boot 日志配置实战 - 5.1 示例项目结构 - 5.2 logback-spring.xml 关键配置 - 5.3 配置优先级(由高到低) - 六、常见问题排查 - 6.1 日志完全没有输出 - 6.2 级别配置不生效 - 6.3 同一条日志重复打印 2~3 次 - 6.4 其他高频坑 - 七、日志使用最佳实践 - 7.1 什么该记、什么不该记 - 7.2 生产环境设计要点 - 八、MDC 与链路追踪简介 - 8.1 MDC 是什么 - 8.2 它解决什么问题 - 8.3 示例:TraceIdFilter - 8.4 进阶概念(点到为止) - 总结 - 常见面试题 / 读者自测题)
导语
凌晨两点,生产环境接口超时。你打开服务器,面对满屏 `System.out.println("here1")`、`System.out.println("here2")`------不知道哪条对应哪个请求,更不知道异常发生在哪一层。
日志不是「打印调试信息」那么简单。它是应用的黑匣子 :记录谁在什么时候做了什么、成功还是失败、出了什么错。会写业务代码 ≠ 会设计日志;后者直接决定你排查问题的效率。
本文用费曼学习法讲清 Spring 日志体系:先给结论,再讲原理,最后给可运行代码。读完后你将能够:
-
说清楚日志与 `println` 的本质区别;
-
理解 SLF4J、Logback、Spring Boot Logging 的关系;
-
在 Java 代码中正确选择日志级别、记录异常;
-
独立完成控制台 + 文件 + 滚动 + 多环境的日志配置;
-
排查「日志不输出、重复打印、级别不生效」等常见问题;
-
了解 MDC 链路追踪的基本用法。
提示:以下是本篇文章正文内容,下面案例可供参考
一、日志到底是什么
1.1 一句话结论
日志是应用在运行过程中,按时间顺序、分级别、结构化地记录事件与状态地机制。
1.2 日志解决了什么问题

1.3 日志 vs `System.out.println()`
java
package org.example.loggingdemo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
public class test {
@Test
void test1(String password, long userId){
// 不推荐:生产环境地大坑
System.out.println("用户登录成功,密码="+password);
// 推荐
log.info("用户登录成功,userId=",userId);
}
}

1.4 为什么生产环境不能只依赖控制台
1.容器重启即丢失:Docker / K8s 中Pod 重建后,控制台输出Gone。
2.无法长期留存:磁盘有限,需要滚动归档策略。
3.多实例难关联:10台机器地控制台,我们怎么拼成一次请求地完整链路?
4.无法分级过滤:调试信息淹没关键ERROR,真正地问题被淹没。
用大白话总结 :`println`像在白板上随手写字,擦了就没了;日志系统像带时间戳、分类归档地飞行记录仪,专门给【事后复盘】用的。
二、Spring日志体系的核心组件
2.1 一句话结论
Spring Boot 采用**【日志门面 + 日志实现】** 分层设计:代码只依赖门面(SLF4J),底层实现(默认Logback)可随时替换。
2.2 生活类比:同一插座与具体电器

-SLF4J :Simple Logging Facade for Java,门面 ,只提供 API,不干活。
- Logback :Logback 是 SLF4J 的原生实现,Spring Boot 默认 使用它。
-Log4j2 :Apache 的高性能实现,需排除默认 Logback 后手动引入。
- Spring Boot Logging :Spring Boot 对 Logback 的自动配置封装,读 `application.yml` 和 `logback-spring.xml`。
2.3 组件关系类图

2.4 Spring 本身如何使用日志
SpringFramework 内部也使用SLF4J 打日志。启动时我们会看到大量`org.springframework.*`的INFO日志------ 那就是框架在报告【我正在初始化Bean、扫描组件......】。
依赖链(SpringBoot Web项目):

2.5 常见错误

> 用大白话总结:我们写代码时只认 SLF4J 这个「国标插座」;Spring Boot 帮你默认接好了 Logback 这台「电器」。想换 Log4j2?换插座后面的机器就行,代码一行不改。
三、SpringBoot 默认日志机制
3.1 默认实现与级别
-
实现:Logback
-
默认级别: root=INFO (INFO/WARN/ERROR会输出,DEBUG/ TRACE不会)
-
默认输出:控制台(彩色)
3.2 默认日志格式拆解
启动Spring Boot后,典型一行日志:

3.3 用application.yml配置
示例项目【application.yml】
java
spring:
application:
name: spring-logging-demo
profiles:
active: dev
server:
port: 8080
logging:
level:
root: info
com.example.loggingdemo: debug
file:
name: logs/spring-logging-demo.log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 14
total-size-cap: 500MB
file-name-pattern: logs/archive/spring-logging-demo.%d{yyyy-MM-dd}.%i.log
等价的`application.properties`:
java
logging.level.root=info
logging.level.com.example.loggingdemo=debug
logging.file.name=logs/spring-logging-demo.log
logging.logback.rollingpolicy.max-file-size=10MB
logging.logback.rollingpolicy.max-history=14
包级别会覆盖 root:`com.example.loggingdemo=debug` 只对这个包生效,其他包仍按 `root: info`。
3.4 多环境不同级别
【`application-dev.yml`】开发环境更啰嗦;
java
logging:
level:
root: debug
com.example.loggingdemo: trace
org.springframework.web: debug
【`application-prod.yml`】生产环境只留关键信息。
java
logging:
level:
root: warn
com.example.loggingdemo: info
org.springframework: warn
启动指定Profile:
bash
mvn spring-boot:run -Dspring-boot.run.profiles=prod
**用大白话总结:**不配任何东西也能跑,因为Spring Boot 已经帮我们接好了Logback;想细调,改
`application.yml`就行,不用碰XML。
四、如何在Java代码中正确打印日志
4.1 推荐的Logger 定义方式
java
package org.example.loggingdemo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
// 每个类一个static final Logger,用当前类做名称
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
}
4.2 日志级别的【阶梯】


4.3 参数化日志vs 字符串拼接
java
@Test
void test2(long orderId){
//不好:即使DEBUG 关闭,字符串拼接仍会执行
log.debug("orderId="+orderId +",cost="+calculateCost(orderId));
// 好:级别不够时,calculateCost不会被调用
log.debug("orderId={},cost={}"+orderId +",cost="+calculateCost(orderId));
}
4.4 异常日志:保留堆栈
java
@Test
void test2(long orderId){
try {
riskOperation();
}catch (Exception e){
// 丢失堆栈
log.error("操作失败:"+e.getMessage());
// 把异常当普通字符串参数
log.error("操作失败:{}",e);
// 正确:Throwable 放最后一个参数
log.error("操作失败,orderId={}",orderId,e);
}
示例项目中的全局异常处理:
java
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String , Object>> handleUnexpected(Exception ex){
log.error("系统异常",ex);//完整堆栈
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code",500,"message","internal server error"));
}
4.5 一次HTTP 请求的日志对象图
以 `POST /api/orders` 为例:

用大白话总结:Logger 按类建一个就行;业务关键节点用 INFO,调试用 DEBUG;打异常一定要把 `e` 传进去,别只打 `getMessage()`。
五、Spring Boot 日志配置实战
5.1 示例项目结构

Java代码
控制器类
java
package org.example.loggingdemo.controller;
import org.example.loggingdemo.dto.response.OrderResponse;
import org.example.loggingdemo.dto.response.request.CreateOrderRequest;
import org.example.loggingdemo.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
// 每个类一个static final Logger,用当前类做名称
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
@Autowired
private OrderService orderService;
@PostMapping("/create")
public OrderResponse create(@RequestBody CreateOrderRequest request){
log.info("收到常见订单请求,productId={},quantity={}",request.productId(),request.quantity());
return orderService.create(request);
}
@GetMapping("/{id}")
public OrderResponse getById(@PathVariable Long id){
log.debug("查询订单,orderId={}",id);
return orderService.getById(id);
}
}
DTO类
java
package org.example.loggingdemo.dto.response.request;
public record CreateOrderRequest(Long productId,Integer quantity) {
}
java
package org.example.loggingdemo.dto.response;
public record OrderResponse(Long id, Long productId,Integer quantity,String status) {
}
异常类
java
package org.example.loggingdemo.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<Map<String,Object>> handleNotFound(OrderNotFoundException ex){
log.warn("业务异常:{}",ex
.getCause());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("code",404,"message","internal server error"));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String,Object>> handleUnexpected(Exception e){
// 正确做法:把 Throwable 作为最后一个参数,保留完整堆栈
log.error("系统异常",e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("code",500,"message","internal server error"));
}
}
java
package org.example.loggingdemo.exception;
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long orderId) {
super("Order not found"+orderId);
}
}
filter类
java
package org.example.loggingdemo.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID_KEY = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()){
traceId = UUID.randomUUID().toString().replace("-","").substring(0,16);
}
MDC.put(TRACE_ID_KEY,traceId);
response.setHeader("X-Trace-Id",traceId);
try{
filterChain.doFilter(request,response);
}finally {
MDC.remove(TRACE_ID_KEY);
}
}
}
Service类
java
package org.example.loggingdemo.service;
import org.example.loggingdemo.dto.response.OrderResponse;
import org.example.loggingdemo.dto.response.request.CreateOrderRequest;
public interface OrderService {
OrderResponse getById(Long id);
OrderResponse create(CreateOrderRequest request);
}
java
package org.example.loggingdemo.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.example.loggingdemo.dto.response.OrderResponse;
import org.example.loggingdemo.dto.response.request.CreateOrderRequest;
import org.example.loggingdemo.exception.OrderNotFoundException;
import org.example.loggingdemo.service.OrderService;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
private final AtomicLong idGenerator = new AtomicLong(1000);
private final Map<Long ,OrderResponse> store = new ConcurrentHashMap<>();
@Override
public OrderResponse getById(Long id) {
OrderResponse order = store.get(id);
if (order == null){
log.warn("订单不存在,orderId={}",id);
throw new OrderNotFoundException(id);
}
return order;
}
@Override
public OrderResponse create(CreateOrderRequest request) {
if (request.quantity()<=0){
log.warn("订单数量非法,productId={},quantity={}",request.productId(),request.quantity());
throw new IllegalArgumentException("quantity must be positive");
}
Long orderId =idGenerator.incrementAndGet();
OrderResponse order = new OrderResponse(orderId,request.productId(),request.quantity(),"CREATED");
store.put(orderId,order);
log.info("订单创建成功,orderId={}",orderId);
return order;
}
}
启动类
java
package org.example.loggingdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoggingdemoApplication {
public static void main(String[] args) {
SpringApplication.run(LoggingdemoApplication.class, args);
}
}
测试:
bash
http://localhost:8081/orders/create


5.2 logback-spring.xml 关键配置
> 文件名必须是 `logback-spring.xml`(带 `-spring`),才能使用 `<springProfile>` 等 Spring 扩展。
XML
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="spring-logging-demo"/>
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-NA}] %-5level %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>logs/${APP_NAME}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/archive/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>14</maxHistory>
<totalSizeCap>500MB</totalSizeCap>
</rollingPolicy>
</appender>
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<logger name="com.example.loggingdemo" level="DEBUG"/>
</springProfile>
<springProfile name="prod">
<root level="WARN">
<appender-ref ref="FILE"/>
</root>
<logger name="com.example.loggingdemo" level="INFO"/>
</springProfile>
<springProfile name="!prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>

5.4 配置优先级(由高到低)

> 用大白话总结:简单需求用 `application.yml`;要分环境、自定义格式、异步写入,上 `logback-spring.xml`。
六、常见问题排查
6.1日志完全没有输出

6.2 级别配置不生效
1.Profile 没激活 :`application-dev.yml` 里的配置只在 `dev` Profile 下生效。
2.logback-spring.xml 硬编码了 level :XML 中 `<root level="WARN">` 优先级高于 yml(部分场景)。
- Logger 名称不对 :应使用完整类名或包名,不是 Spring Bean 名称。
6.3 同一条日志重复打印2~3次
原因:Classpath 中存在多个 SLF4J 绑定(如同时有 `logback-classic` 和 `slf4j-simple`)。
解决:排除多余依赖:
XML
<dependency>
<groupId>some.library</groupId>
<artifactId>some-artifact</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
6.4 其他高频坑

用大白话总结
:日志不出来,先查【级别】和【包名】;重复打印,查依赖冲突;生产环境,永远假设日志会被别人看到------别写秘密。
七、日志使用最佳实践
7.1 什么该记、什么不该记
应该记
-
请求入口:关键参数(脱敏后)、TraceId
-
业务里程碑:订单创建、支付回调、状态变更
-
异常:完整堆栈 + 业务上下文(orderId、userId)
-外部调用:下游HTTP/MQ 失败原因与耗时
不应该记
-
密码、Token、身份证号、银行卡号
-
超大JSON Body(截断或哈希)
-
无意义的【进入方法/离开方法】(除非 TRACE调式)
7.2 生产环境要点

1.按环境分级 :见示例 `application-dev.yml` / `application-prod.yml`。
- 文件滚动必备 :`max-history` + `total-size-cap`。
3**. 敏感字段脱敏** :手机号 `138****5678`。
4.统一格式 :便于日志平台解析;MDC 带 traceId。
- 异步日志(高并发):Logback `AsyncAppender`,减少 I/O 阻塞业务线程------进阶场景再深入。
> 用大白话总结:日志是写给自己和运维同事看的「案情记录」------够查案、不含机密、别刷屏。
八、MDC与链路追踪简介
8.1 MDC是什么
MDC (Mapped Diagnostic Context)是 SLF4J 提供的线程级字典:你在 Filter 里 `MDC.put("traceId", id)`,同一线程后续所有 `log.info()` 都能自动带上这个值。
8.2 它解决什么问题
微服务/多线程下,100条日志交织在一起,无法区分哪条属于同一请求。MDC给每个请求贴标签,日志里统一输出【traceId】。
8.3 示例:TraceIdFilter
java
package org.example.loggingdemo.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID_KEY = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()){
traceId = UUID.randomUUID().toString().replace("-","").substring(0,16);
}
MDC.put(TRACE_ID_KEY,traceId);
response.setHeader("X-Trace-Id",traceId);
try{
filterChain.doFilter(request,response);
}finally {
MDC.remove(TRACE_ID_KEY);
}
}
}
Logback 格式中加入 `%X{traceId}`:
XML
<pattern>%d{HH:mm:ss.SSS} [%X{traceId:-NA}] %-5level %logger{36} - %msg%n</pattern>
8.4 进阶概念(点到为止)

> 用大白话总结:MDC 像给每个请求发一张「工牌号」,之后这个请求经过的所有代码打日志都会自动带上工牌,排障时一搜就齐。
总结
Spring Boot 日志体系并不神秘:
1.代码层 只依赖 SLF4J 门面;
2.默认实现 是 Logback,由 `spring-boot-starter-logging` 自动引入;
- 简单配置 用 `application.yml`,复杂配置 用 `logback-spring.xml`;
4.正确使用 参数化日志、合理选级别、异常带堆栈;
5.生产环境 必须考虑文件滚动、级别分环境、敏感信息脱敏;
- MDC是请求级链路追踪的入门钥匙。
把本文的spring-logging-demo跑一遍,观察控制台、日志文件和 traceId,比背十遍概念都管用。
常见面试题 / 读者自测题
-
SLF4J 和 Logback 分别是什么角色?为什么要分门面和实现?
-
Spring Boot 默认日志级别是什么?如何只把 `com.example` 包设为 DEBUG?
-
`log.debug("id=" + id)` 和 `log.debug("id={}", id)` 有何性能差异?
-
记录异常时,`log.error("msg", e)` 和 `log.error("msg: " + e.getMessage())` 差别在哪?
-
日志重复打印 3 次,最可能的原因是什么?
-
MDC 中的值存在哪里?为什么必须在 `finally` 里 `MDC.remove()`?
-
`logback.xml` 和 `logback-spring.xml` 有什么区别?
-
SLF4J 是 API 门面;Logback 是具体实现。分离后换实现不改业务代码。
-
默认 root=INFO;`logging.level.com.example=debug`。
-
前者无论级别是否开启都会拼接字符串;后者级别不够时不格式化。
-
前者输出完整堆栈;后者只有一行 message,丢失调用链。
-
Classpath 存在多个 SLF4J 绑定。
-
ThreadLocal Map;线程池复用时不清除会串数据。
-
`-spring` 版支持 Spring Profile 等扩展,应优先使用。