JavaEE进阶——SpringBoot日志从入门到精通

目录

[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 日志框架介绍:门面模式详解)

知识点:什么是门面模式?

知识点:SLF4J就是日志框架的门面

[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 高性能日志记录:性能优化技巧)

技巧1:始终使用占位符

技巧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("关闭餐厅灯");
    }
}

代码与结论的联系

  • 结论:"减少系统相互依赖,客户端只与门面对象交互"

  • 代码体现FacadePatternDemomain方法中,客户端只创建了LightFacade对象,调用了lightOn()方法,完全没有直接引用LivingRoomLightHallLight等子系统类,实现了隔离

知识点: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.LoggerControllerLogger名(通常用类名),精确定位代码位置

  • :分隔符

  • --------------要输出日志的内容----------------日志消息,你自定义的内容

结论与代码的联系

  • 结论:"这种格式设计使得日志信息丰富且结构化"

  • 代码体现 :当你执行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 新手学习路径建议

  1. 第一步:掌握基础(2.1节代码),能打印不同级别日志

  2. 第二步:理解配置(2.5节),能控制日志输出和文件存储

  3. 第三步:使用Lombok(3.2节),简化代码,提升效率

  4. 第四步:应用最佳实践(4.1-4.2节),写出安全、高性能的日志代码

  5. 第五步:了解分布式日志(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简化,最后研究高级特性。每个知识点都动手写代码验证,理解会更深刻!

相关推荐
利刃大大8 小时前
【JavaSE】十九、JVM运行流程 && 类加载Class Loading
java·开发语言·jvm
testresultstomorrow8 小时前
GB26875消防物联网协议Java实现详解
java·物联网
Clarence Liu8 小时前
Go Context 深度解析:从源码到 RESTful 框架的最佳实践
开发语言·后端·golang
heartbeat..8 小时前
Java Map 详解:原理、实现与使用场景
java·map·集合
踏浪无痕8 小时前
Nacos到底是AP还是CP?一文说清楚
分布式·后端·面试
踏浪无痕8 小时前
深入JRaft:Nacos配置中心的性能优化实践
分布式·后端·面试
果然途游8 小时前
完整Java后端学习路径
java·开发语言·学习笔记
又是重名了8 小时前
导出新方案-poi和easyexcel融合
java·poi·easyexcel
我梦见我梦见我8 小时前
CentOS下安装RocketMQ
后端