目录
[Spring Boot 日志详解:从入门到精通(新手版)](#Spring Boot 日志详解:从入门到精通(新手版))
[1. 日志概述:为什么要学?](#1. 日志概述:为什么要学?)
[1.1 从System.out.println到专业日志框架](#1.1 从System.out.println到专业日志框架)
[2. 日志使用:手把手教你写代码](#2. 日志使用:手把手教你写代码)
[2.1 打印日志:第一个日志程序](#2.1 打印日志:第一个日志程序)
[2.2 日志框架介绍:门面模式详解](#2.2 日志框架介绍:门面模式详解)
[2.3 日志格式说明](#2.3 日志格式说明)
[2.4 日志级别:代码演示与配置效果](#2.4 日志级别:代码演示与配置效果)
[2.5 日志配置详解](#2.5 日志配置详解)
[3. Lombok简化日志:更优雅的写法](#3. Lombok简化日志:更优雅的写法)
[3.1 添加依赖](#3.1 添加依赖)
[3.2 使用@Slf4j注解](#3.2 使用@Slf4j注解)
[4. 日志最佳实践与扩展知识](#4. 日志最佳实践与扩展知识)
[4.1 日志安全:避免记录敏感信息](#4.1 日志安全:避免记录敏感信息)
[4.2 高性能日志记录:性能优化技巧](#4.2 高性能日志记录:性能优化技巧)
[4.3 集中式日志管理:微服务场景](#4.3 集中式日志管理:微服务场景)
[4.4 日志标准化:JSON结构化日志](#4.4 日志标准化:JSON结构化日志)
[4.5 日志与追踪结合:分布式TraceID](#4.5 日志与追踪结合:分布式TraceID)
[5. 总结:知识体系回顾](#5. 总结:知识体系回顾)
[5.1 核心知识点速查表](#5.1 核心知识点速查表)
[5.2 新手学习路径建议](#5.2 新手学习路径建议)
[5.3 代码 vs 结论:如何建立联系?](#5.3 代码 vs 结论:如何建立联系?)
Spring Boot 日志详解:从入门到精通(新手版)
1. 日志概述:为什么要学?
1.1 从System.out.println到专业日志框架
新手理解 :想象一下,你做的程序就像一个黑盒子。当它出问题时,你需要透过"窗户"看里面发生了什么。System.out.println就像在小盒子上戳个小洞,只能看到一点点;而专业日志框架就像安装了监控摄像头,能全方位、分级别、可配置地观察系统状态。
核心痛点:
-
System.out.println打印的信息只能看到控制台,程序重启就消失 -
无法区分信息的重要性(是致命错误还是普通提示?)
-
不能控制输出位置(只能控制台,不能存文件)
-
影响性能(每次都要打印,无法动态关闭)
2. 日志使用:手把手教你写代码
2.1 打印日志:第一个日志程序
知识点:如何获取日志对象
java
/*
* 【完整代码示例1:基础日志打印】
* 这段代码演示了如何在Spring Boot中获取并使用日志对象
* 对应结论:Spring Boot内置SLF4J框架,通过LoggerFactory获取日志对象
*/
// ========== import区域:导入必要的类 ==========
import org.slf4j.Logger; // 【第1行】导入SLF4J的Logger接口,这是所有日志操作的入口
import org.slf4j.LoggerFactory; // 【第2行】导入LoggerFactory工厂类,用于创建Logger实例
import org.springframework.web.bind.annotation.RequestMapping; // 【第3行】导入Spring MVC的请求映射注解,用于定义URL路径
import org.springframework.web.bind.annotation.RestController; // 【第4行】导入REST控制器注解,表示这个类处理HTTP请求
// ========== 类定义区域 ==========
@RestController // 【第6行】标记这个类是一个RESTful控制器,能处理HTTP请求并返回JSON或纯文本
public class LoggerController { // 【第7行】定义控制器类,类名自定义
// ========== 核心:获取日志对象 ==========
// 【第9行】声明一个静态(final不可变)、线程安全的日志对象
// static:所有实例共享同一个logger对象,节省内存
// final:确保logger引用不会被修改,保证线程安全
// LoggerFactory.getLogger(LoggerController.class):创建与当前类绑定的logger
// 参数作用:LoggerController.class告诉框架"这个日志来自哪个类",日志中会显示类名
private static final Logger logger = LoggerFactory.getLogger(LoggerController.class);
// ========== 请求处理方法 ==========
@RequestMapping("/logger") // 【第13行】定义URL路径,访问 http://localhost:8080/logger 会触发此方法
public String logger() { // 【第14行】定义处理请求的方法,返回字符串
// 【第16行】使用logger对象打印INFO级别的日志,日志内容自定义
// 这行代码执行后,日志会显示在控制台(默认配置)
logger.info("--------------要输出日志的内容----------------");
return "打印日志"; // 【第18行】返回给浏览器的响应内容
}
}
代码与结论的联系:
-
结论:"通过LoggerFactory获取当前类的Logger实例"
-
代码体现 :第9行
LoggerFactory.getLogger(LoggerController.class)就是具体的获取方式,参数LoggerController.class保证了日志中能显示完整的类名路径,方便定位问题
知识点:日志占位符避免字符串拼接
java
// 演示代码片段
String userId = "user123";
String action = "login";
/*
* 【重点】为什么用占位符{}而不是字符串拼接?
*
* 字符串拼接的问题:
* logger.debug("用户 " + userId + " 执行了 " + action + " 操作");
* 即使日志级别设置为WARN(不输出DEBUG),JVM仍会先执行字符串拼接,浪费性能
*
* 占位符的优势:
* 下面这行代码,当日志级别为WARN时,不会执行字符串拼接,直接跳过,性能更好
*/
logger.debug("用户 {} 执行了 {} 操作", userId, action);
2.2 日志框架介绍:门面模式详解
知识点:什么是门面模式?
现实生活中的例子 :你去餐厅吃饭,不需要直接去后厨告诉厨师怎么炒菜、告诉服务员怎么摆盘。你只需要对服务员(门面)说"我要一份宫保鸡丁",服务员会协调后厨、配菜、上菜等所有子系统。
代码层面的体现:
java
/*
* 【完整代码示例2:门面模式演示】
* 这段代码展示了如何通过门面模式统一控制多个灯
* 对应结论:门面模式简化客户端与复杂子系统的交互
*/
// ========== 客户端代码:使用门面 ==========
public class FacadePatternDemo { // 【第1行】演示类
public static void main(String[] args) { // 【第2行】主方法,程序入口
// 【第4行】创建门面对象:客户端只与门面交互,不直接操作具体灯
LightFacade lightFacade = new LightFacade();
// 【第5行】调用门面的统一方法:一句代码开启所有灯
// 结论体现:客户端无需知道有几个灯、灯怎么开,只需调用门面方法
lightFacade.lightOn();
}
}
// ========== 门面类:统一接口 ==========
/*
* 灯的门面,提供统一的开关控制
* 门面角色:隐藏了子系统的复杂性
*/
class LightFacade { // 【第12行】门面对象
// 【第14-16行】子系统:包含多个具体灯对象(类的集合)
// 这些是门面管理的子系统,客户端不需要直接访问
private Light livingRoomLight = new LivingRoomLight(); // 客厅灯
private Light hallLight = new HallLight(); // 走廊灯
private Light diningLight = new DiningLight(); // 餐厅灯
// ========== 门面方法:封装子系统操作 ==========
// 【第19行】统一开启所有灯:遍历调用每个子系统的on方法
public void lightOn() {
livingRoomLight.on(); // 调用客厅灯on
hallLight.on(); // 调用走廊灯on
diningLight.on(); // 调用餐厅灯on
}
// 【第24行】统一关闭所有灯
public void lightOff() {
livingRoomLight.off();
hallLight.off();
diningLight.off();
}
}
// ========== 子系统接口:定义规范 ==========
interface Light { // 【第30行】灯的接口,定义子系统必须实现的方法
void on(); // 开灯方法
void off(); // 关灯方法
}
// ========== 子系统实现:具体灯类 ==========
// 【第35-47行】客厅灯实现:实现Light接口的具体行为
class LivingRoomLight implements Light {
@Override
public void on() {
System.out.println("打开客厅灯"); // 具体开灯逻辑
}
@Override
public void off() {
System.out.println("关闭客厅灯");
}
}
// 【第49-61行】走廊灯实现
class HallLight implements Light {
@Override
public void on() {
System.out.println("打开走廊灯");
}
@Override
public void off() {
System.out.println("关闭走廊灯");
}
}
// 【第63-75行】餐厅灯实现
class DiningLight implements Light {
@Override
public void on() {
System.out.println("打开餐厅灯");
}
@Override
public void off() {
System.out.println("关闭餐厅灯");
}
}
代码与结论的联系:
-
结论:"减少系统相互依赖,客户端只与门面对象交互"
-
代码体现 :
FacadePatternDemo的main方法中,客户端只创建了LightFacade对象,调用了lightOn()方法,完全没有直接引用LivingRoomLight、HallLight等子系统类,实现了隔离
知识点:SLF4J就是日志框架的门面
示意图解读:
java
【你的应用代码】 --> 【SLF4J API(门面接口)】 --> 【SLF4J绑定(适配器)】 --> 【具体实现(Logback)】
为什么需要这个门面?
java
// 假设你直接依赖Log4j
import org.apache.log4j.Logger; // 硬编码依赖Log4j
Logger logger = Logger.getLogger(MyClass.class);
// 问题来了:如果项目要换成Logback,你需要修改所有类的import和代码
// 工作量巨大,风险极高
// 使用SLF4J门面:
import org.slf4j.Logger; // 接口不变
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(MyClass.class);
// 好处:切换实现只需改依赖和配置,代码完全不用动
// 比如从Log4j换成Logback,只需在pom.xml中替换依赖,代码零改动
2.3 日志格式说明
默认日志格式:
java
2023-05-15 10:30:45.123 INFO 12345 --- [nio-8080-exec-1] com.example.demo.LoggerController : --------------要输出日志的内容----------------
逐段解析:
-
2023-05-15 10:30:45.123:时间戳,精确到毫秒,用于追踪事件发生顺序 -
INFO:日志级别,告诉你这条信息的重要性 -
12345:进程ID,多实例部署时区分是哪个进程打印的 -
---:分隔符,视觉分隔,提高可读性 -
[nio-8080-exec-1]:线程名,并发场景下定位哪个线程执行的 -
com.example.demo.LoggerController:Logger名(通常用类名),精确定位代码位置 -
::分隔符 -
--------------要输出日志的内容----------------:日志消息,你自定义的内容
结论与代码的联系:
-
结论:"这种格式设计使得日志信息丰富且结构化"
-
代码体现 :当你执行
logger.info("要输出日志的内容")时,Logback框架会自动在前面拼接时间、级别、线程等信息,无需你手动添加
2.4 日志级别:代码演示与配置效果
知识点:六级别详解
java
/*
* 【完整代码示例3:日志级别演示】
* 这段代码演示如何打印不同级别的日志
* 对应结论:日志级别从低到高为 TRACE < DEBUG < INFO < WARN < ERROR
* 对应结论:Spring Boot默认只显示INFO及以上级别
*/
import org.slf4j.Logger; // 导入日志接口
import org.slf4j.LoggerFactory; // 导入工厂类
import org.springframework.web.bind.annotation.RequestMapping; // 导入请求映射
import org.springframework.web.bind.annotation.RestController; // 导入REST控制器
@RestController // 【第6行】标记为REST控制器
public class LoggerController {
// 【第9行】获取与当前类绑定的logger对象
private static final Logger logger = LoggerFactory.getLogger(LoggerController.class);
/**
* 打印不同级别的日志
* @return 响应字符串
*/
@RequestMapping("/printLog") // 【第15行】定义URL路径
public String printLog() { // 【第16行】处理方法
// 【第18行】TRACE级别:最详细,用于追踪代码执行路径(默认不显示)
logger.trace("================= trace===============");
// 【第20行】DEBUG级别:调试信息,开发环境使用(默认不显示)
logger.debug("================= debug===============");
// 【第22行】INFO级别:普通信息,记录业务流程(默认显示)
logger.info("================= info===============");
// 【第24行】WARN级别:警告信息,可能有问题(默认显示)
logger.warn("================= warn===============");
// 【第26行】ERROR级别:错误信息,需要处理(默认显示)
logger.error("================= error===============");
return "打印不同级别的日志"; // 【第29行】返回响应
}
}
代码与结论的联系:
-
结论:"Spring Boot默认配置只输出INFO级别及以上的日志"
-
代码体现 :执行
printLog()方法后,控制台只会看到info、warn、error三行,trace和debug不会显示。这是因为默认配置相当于logging.level.root=INFO,低于INFO级别的被过滤掉了
知识点:配置日志级别如何影响代码行为
配置文件:
java
# application.properties
logging.level.root=debug # 设置根日志级别为DEBUG
# 也可以针对特定包设置
logging.level.com.example.demo=trace # 这个包下所有类都输出TRACE级别
配置效果示意图:
配置前(默认INFO):
代码中的5条日志 → [过滤器] → 只显示≥INFO的3条
配置后(root=DEBUG):
代码中的5条日志 → [过滤器] → 显示≥DEBUG的4条(TRACE仍被过滤)
配置后(com.example.demo=TRACE):
代码中的5条日志 → [过滤器] → 全部5条都显示
代码与配置的联系:
-
配置结论:"logging.level可以针对不同包设置级别"
-
代码体现 :如果你的项目结构是
com.example.demo.controller.LoggerController,那么logging.level.com.example.demo.controller=trace这个配置就会让LoggerController类中的logger.trace()生效
2.5 日志配置详解
知识点:日志持久化(存文件)
方式一:指定文件名
# application.yml
logging:
file:
name: logger/springboot.log # 相对路径,会在项目根目录下创建logger文件夹
方式二:指定目录
# application.yml
logging:
file:
path: D:/temp # 只指定目录,文件名默认为spring.log
代码与配置的联系:
-
结论:"如果同时配置name和path,只有name生效"
-
实际操作:
yaml logging: file: name: myapp.log # 这个生效 path: D:/logs # 这个被忽略
日志会保存在./myapp.log,而不是D:/logs/spring.log
知识点:日志分割配置
# application.yml
logging:
logback:
rollingpolicy:
max-file-size: 1KB # 单个文件超过1KB就分割
file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i # 分割后命名规则
效果演示:
java
初始文件:springboot.log
当大小>1KB后:
- springboot.log(新文件)
- springboot.log.2023-05-15.0(旧文件)
- springboot.log.2023-05-15.1(更旧的文件)
时间格式占位符说明:
-
%d{yyyy-MM-dd}:按天分割 -
%d{yyyy-MM-dd_HH}:按小时分割 -
%i:序号,从0开始递增
3. Lombok简化日志:更优雅的写法
3.1 添加依赖
XML
<!-- pom.xml中添加Lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional> <!-- 可选依赖,不传递给其他项目 -->
</dependency>
3.2 使用@Slf4j注解
java
/*
* 【完整代码示例4:Lombok简化日志】
* 这段代码演示如何使用Lombok的@Slf4j注解自动生成logger
* 对应结论:Lombok减少样板代码,直接使用log变量
*/
// ========== import区域 ==========
import lombok.extern.slf4j.Slf4j; // 【第1行】导入Lombok的日志注解
import org.springframework.web.bind.annotation.RestController; // 【第2行】导入REST控制器
// ========== 使用@Slf4j注解 ==========
@Slf4j // 【第4行】**核心注解**:在编译期自动生成以下代码:
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogController.class);
@RestController // 【第5行】标记为REST控制器
public class LogController { // 【第6行】类定义
/**
* 演示日志输出方法
*/
public void log() { // 【第9行】普通方法
// 【第11行】直接使用自动生成的log对象,无需手动创建
// 效果与logger.info完全相同
log.info("--------------要输出日志的内容----------------");
// ========== 高级用法1:占位符避免字符串拼接 ==========
String userName = "张三";
int userId = 123;
// 【第16行】使用{}占位符,效果与2.1.2节相同
// Lombok生成的log对象支持所有SLF4J方法
log.debug("用户 {} (ID: {}) 访问了系统", userName, userId);
// ========== 高级用法2:条件日志提升性能 ==========
// 【第20行】判断是否开启DEBUG级别,避免执行耗时操作
// 这是一个性能优化技巧,对复杂日志尤其重要
if (log.isDebugEnabled()) {
// 【第22行】只有在DEBUG开启时才执行复杂计算
// 如果DEBUG关闭,generateExpensiveLogData()方法根本不会被执行
String expensiveData = generateExpensiveLogData();
log.debug("详细信息: {}", expensiveData);
}
}
/**
* 模拟生成复杂日志数据的耗时操作
* @return 复杂数据字符串
*/
private String generateExpensiveLogData() {
// 【第32行】模拟耗时操作,睡眠100毫秒
try {
Thread.sleep(100); // 模拟耗时100ms
} catch (InterruptedException e) { // 【第34行】处理中断异常
// 【第35行】恢复线程中断状态,这是最佳实践
Thread.currentThread().interrupt();
}
return "复杂日志数据"; // 【第37行】返回结果
}
}
代码与结论的联系:
-
结论:"Lombok减少样板代码,无需手动创建Logger"
-
代码体现 :比较示例1和示例4,示例1需要写第9行的
LoggerFactory.getLogger(...),而示例4只需在类上加@Slf4j注解,直接使用log变量 -
结论:"使用log.isDebugEnabled()避免不必要的计算"
-
代码体现 :第20行判断,如果DEBUG级别未开启,第22行的
generateExpensiveLogData()方法(耗时100ms)根本不会执行,节省性能
4. 日志最佳实践与扩展知识
4.1 日志安全:避免记录敏感信息
java
/*
* 【完整代码示例5:日志安全脱敏】
* 这段代码演示如何安全地记录日志
* 对应结论:避免记录密码等敏感信息,必要时脱敏处理
*/
public class SecurityLogDemo {
private static final Logger logger = LoggerFactory.getLogger(SecurityLogDemo.class);
public void login(String username, String password) {
// ========== 错误示范:直接记录密码 ==========
// 【第9行】**极度危险**:日志文件可能被多人访问,密码明文存储违反安全规范
// 如果日志被黑客获取,直接得到用户密码
logger.info("用户 {} 使用密码 {} 登录", username, password); // ❌ 不要这样做
// ========== 正确示范:只记录业务行为 ==========
// 【第13行】**安全做法**:只记录谁尝试登录,不记录密码
logger.info("用户 {} 尝试登录", username); // ✅ 推荐
// ========== 正确示范:调试时脱敏显示 ==========
// 【第17行】脱敏处理:将密码替换为掩码
// 这样即使开了DEBUG级别,也看不到真实密码
String maskedPassword = password != null ? "***" : "null"; // 脱敏逻辑
logger.debug("用户 {} 使用密码 {} 登录", username, maskedPassword); // ✅ 安全
// 实际业务逻辑...
boolean success = validatePassword(password);
if (success) {
logger.info("用户 {} 登录成功", username);
} else {
logger.warn("用户 {} 登录失败", username); // 记录失败,但不记录密码
}
}
private boolean validatePassword(String password) {
// 模拟密码验证
return true;
}
}
代码与结论的联系:
-
结论:"避免记录敏感信息,必要时脱敏"
-
代码体现 :第9行错误示范直接记录
password,第17行正确示范使用"***"替代真实密码 -
为什么这样做:日志通常会被收集到集中系统,可能被运维、开发人员查看,明文密码是重大安全隐患
4.2 高性能日志记录:性能优化技巧
技巧1:始终使用占位符
java
// ❌ 低效做法:先拼接字符串,再判断日志级别
// 即使DEBUG未开启,也会执行getName()和getAction()方法,并拼接字符串
logger.debug("当前用户: " + user.getName() + ", 操作: " + user.getAction());
// ✅ 高效做法:使用占位符
// 如果DEBUG未开启,getName()和getAction()都不会执行,直接跳过
logger.debug("当前用户: {}, 操作: {}", user::getName, user::getAction); // Java 8方法引用,更优
技巧2:复杂日志使用条件判断
java
// 当需要构造复杂日志消息时
public void processLargeData(List<Data> dataList) {
// 假设dataList有10000条数据
// ❌ 低效:即使DEBUG关闭,也会先格式化整个列表为字符串
logger.debug("处理数据: {}", dataList.toString()); // toString()耗时很长
// ✅ 高效:先判断,再执行耗时操作
if (logger.isDebugEnabled()) { // 【第7行】轻量级判断
// 只有DEBUG开启才执行toString()
String dataSummary = dataList.subList(0, 10).toString() + "...共" + dataList.size() + "条";
logger.debug("处理数据: {}", dataSummary); // 【第10行】实际记录
}
// 处理数据...
}
代码与结论的联系:
-
结论:"使用占位符避免不必要的字符串拼接"
-
代码体现 :低效做法中
+操作符会立即执行拼接;高效做法中{}占位符在日志级别判断后才处理 -
性能差异:在高并发场景下,这种优化可减少大量CPU和内存开销
4.3 集中式日志管理:微服务场景
问题场景:你有10个微服务,每个服务有3个实例,日志分散在30台机器上。用户投诉下单失败,你如何快速定位问题?
解决方案:ELK Stack架构
【各个服务】 → 【Filebeat(收集)】 → 【Logstash(处理)】 → 【Elasticsearch(存储)】 → 【Kibana(展示)】
Spring Boot集成Filebeat:
XML
# 在每个服务的application.yml中配置
logging:
file:
name: /opt/logs/${spring.application.name}.log # 统一日志路径
# Filebeat配置(filebeat.yml)
filebeat.inputs:
- type: log
enabled: true
paths:
- /opt/logs/*.log # 收集所有服务日志
output.logstash:
hosts: ["logstash:5044"]
代码与结论的联系:
-
结论:"单机日志管理已不足够,需要集中式解决方案"
-
代码体现 :通过配置
logging.file.name将日志落盘到统一路径,再由Filebeat收集。没有这些配置,日志只在控制台,无法集中管理
4.4 日志标准化:JSON结构化日志
java
/*
* 【完整代码示例6:JSON结构化日志】
* 需要添加依赖:net.logstash.logback:logstash-logback-encoder
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.logstash.logback.marker.Markers; // 导入JSON标记类
public class StructuredLogDemo {
private static final Logger logger = LoggerFactory.getLogger(StructuredLogDemo.class);
public void userLogin(Long userId, String action, String status) {
// ========== 传统日志:非结构化 ==========
// 缺点:只能通过正则表达式解析,效率低,容易出错
logger.info("用户 {} 执行 {} 操作,状态: {}", userId, action, status);
// 输出:2023-05-15...用户 123 执行 login 操作,状态: success
// ========== 结构化日志:JSON格式 ==========
// 优点:可直接作为JSON解析,每个字段独立可查询
logger.info(
Markers.append("userId", userId) // 【第17行】添加userId字段
.and(Markers.append("action", action)) // 【第18行】添加action字段
.and(Markers.append("status", status)), // 【第19行】添加status字段
"用户登录" // 【第20行】简洁的消息
);
// 输出示例:
// {
// "timestamp": "2023-05-15T10:30:45.123+08:00",
// "level": "INFO",
// "logger": "com.example.demo.StructuredLogDemo",
// "message": "用户登录",
// "userId": 123,
// "action": "login",
// "status": "success"
// }
}
}
代码与结论的联系:
-
结论:"结构化日志便于机器解析,可直接作为字段查询"
-
代码体现 :第17-19行的
Markers.append()将额外数据作为独立字段添加到JSON中 -
查询优势 :在Kibana中可以直接用
userId:123查询,而传统日志需要用正则匹配用户 123
4.5 日志与追踪结合:分布式TraceID
java
/*
* 【完整代码示例7:分布式追踪】
* 需要添加依赖:spring-cloud-starter-sleuth
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@GetMapping("/createOrder")
public String createOrder() {
// Spring Cloud Sleuth自动注入TraceID和SpanID到MDC
// MDC(Mapped Diagnostic Context)是SLF4J的线程本地变量映射
// 在日志配置中配置:logging.pattern.level=%5p [${spring.application.name},%X{traceId},%X{spanId}]
// 输出示例:2023-05-15... INFO [order-service,5af7801cc2ab4ad4,5af7801cc2ab4ad4] ... 创建订单
logger.info("创建订单开始"); // 【第18行】这行日志会自动带上TraceID
// 调用其他服务(RestTemplate或Feign)
// 这些调用会自动传递TraceID,形成完整的调用链
logger.info("创建订单完成"); // 【第22行】同一请求的TraceID相同
return "订单创建成功";
}
}
跨服务调用示例:
用户请求 → [API Gateway] → [Order Service] → [Inventory Service] → [Payment Service]
↓ ↓ ↓
TraceID:5af78... TraceID:5af78... TraceID:5af78...
SpanID:1 SpanID:2 SpanID:3
所有服务的日志都包含相同的TraceID: 5af7801cc2ab4ad4
代码与结论的联系:
-
结论:"通过TraceID关联同一请求在不同服务的日志"
-
代码体现:无侵入式,只需添加依赖和配置,第18行和第22行的日志会自动包含相同的TraceID
-
排查流程:用户反馈订单失败 → 从网关日志获取TraceID → 在ELK中查询该TraceID → 看到所有相关服务的完整调用链日志
5. 总结:知识体系回顾
5.1 核心知识点速查表
|----------|----------------------------------------|---------------------|----------------------|
| 知识点 | 代码体现 | 配置关联 | 最佳实践 |
| 获取Logger | LoggerFactory.getLogger(Class) | 无需配置 | 使用Lombok的@Slf4j更简洁 |
| 日志级别 | logger.trace/debug/info/warn/error() | logging.level.* | 生产环境用INFO,开发用DEBUG |
| 占位符 | log.info("msg {}", var) | 无需配置 | 始终使用占位符,避免字符串拼接 |
| 日志文件 | logging.file.name/path | application.yml | 线上必须持久化日志 |
| 日志分割 | logging.logback.rollingpolicy.* | application.yml | 按天和大小分割,防止磁盘满 |
| 性能优化 | if(log.isDebugEnabled()) | 无需配置 | 复杂日志务必加判断 |
| 敏感信息 | password -> "***" | 无需配置 | 密码、密钥等绝不打印明文 |
| 分布式追踪 | 自动注入 | Spring Cloud Sleuth | 微服务架构必须启用 |
5.2 新手学习路径建议
-
第一步:掌握基础(2.1节代码),能打印不同级别日志
-
第二步:理解配置(2.5节),能控制日志输出和文件存储
-
第三步:使用Lombok(3.2节),简化代码,提升效率
-
第四步:应用最佳实践(4.1-4.2节),写出安全、高性能的日志代码
-
第五步:了解分布式日志(4.3-4.5节),为微服务架构做准备
5.3 代码 vs 结论:如何建立联系?
方法论:
-
看到结论问"代码在哪?" :例如结论"占位符避免字符串拼接",立即想到
logger.debug("msg {}", var)这行代码 -
看到代码问"结论是什么?" :例如看到
logging.file.name配置,思考结论"日志持久化"和这个配置的关系 -
实践验证 :修改配置
logging.level.root=TRACE,运行示例3,观察TRACE日志是否出现,直观感受配置对代码行为的影响
附录:完整项目结构示例
spring-boot-logging-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/demo/
│ │ │ ├── LoggerController.java # 示例1、3代码
│ │ │ ├── LogController.java # 示例4代码
│ │ │ ├── SecurityLogDemo.java # 示例5代码
│ │ │ └── StructuredLogDemo.java # 示例6代码
│ │ └── resources/
│ │ ├── application.yml # 核心配置文件
│ │ └── logback-spring.xml # 高级日志配置(可选)
│ └── test/
│ └── java/
└── pom.xml # Maven依赖管理
application.yml完整配置示例:
XML
# 应用基本信息
spring:
application:
name: logging-demo
# 日志配置
logging:
# 级别配置
level:
root: INFO
com.example.demo: DEBUG # 本项目的包设为DEBUG
# 文件配置
file:
name: logs/${spring.application.name}.log # 日志文件名
# Logback滚动策略
logback:
rollingpolicy:
max-file-size: 10MB
file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz # 压缩旧日志
# 控制台格式(开发环境)
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n'
# 文件格式(生产环境)
pattern:
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n'
新手建议 :从最简单的LoggerController开始,逐步添加配置,观察日志输出变化,再学习Lombok简化,最后研究高级特性。每个知识点都动手写代码验证,理解会更深刻!