日志技术 Logback + Slf4j —— 从零开始的 Web 后端学习

原创 · 上篇咱们做部门管理实战时,Controller 里还在用 System.out.println("根据 id 删除部门 , id=" + id) 这种"土办法"打日志。这篇就把它升级成专业的日志方案:Logback + Slf4j 。一个注解 @Slf4j 搞定,代码全都能直接抄 ✅


一. 前言:为什么不能用 System.out.println?

ok,咱们先想一个问题:你在 Controller 里这样"打印日志",上线后会怎么样?

java 复制代码
@DeleteMapping("/depts")
public Result delete(Integer id){
    System.out.println("根据 id 删除部门 , id=" + id);   // 控制台打印
    deptService.deleteById(id);
    return Result.success();
}

开发时挺香,但到了生产环境,这种方式有三个致命毛病 😩:

  • ❌ 硬编码:不想输出这条日志了,只能回来删代码、重新部署。想临时关掉某些日志?做不到。
  • ❌ 只能输出到控制台 :服务器上的控制台一闪而过,没法存到文件里长期保留、排查历史问题。
  • ❌ 不便扩展维护:想按天/按大小切分日志文件?想区分"普通信息"和"错误信息"分开展示?想控制不同包输出不同详细程度?全都做不到。

📌 一句话总结System.out.println 是"一次性输出",而日志框架是"可配置、可分级、可持久化的运行记录系统"。专业项目里,日志就是程序的"黑匣子",出问题时全靠它还原现场。


二. 日志框架家族:门面 vs 实现

Java 日志框架有一大堆,名字容易混。咱们先分两类看清楚:

框架 角色 说明
JUL 实现 Java 官方自带(java.util.logging),配置简单但不够灵活、性能差,很少用
Log4j 实现 曾经最流行的日志实现,配置灵活(已停止维护,改用 Log4j2)
Logback 实现 Log4j 作者另起炉灶的升级版,功能更多、性能更好,Spring Boot 默认就是它
Slf4j 门面(接口) Simple Logging Facade for Java,只定义统一接口,不干活,底层可换任意实现

📌 重点:门面模式(Facade)

Slf4j 自己不记录日志,它只是定了一套"标准 API"。你的代码里只调用 Slf4j 的方法,底层到底用 Logback 还是 Log4j2,随时可换,业务代码一行都不用改

复制代码
你的代码
   │  只调用 log.info(...)(Slf4j 接口)
   ▼
┌─────────┐
│  Slf4j  │  ← 门面:统一接口,不干活
└─────────┘
   │  运行时绑定到某个实现
   ▼
Logback / Log4j2 / JUL ...   ← 真正干活的实现,可插拔替换

这正是"面向接口编程"的好处:写代码只依赖抽象(Slf4j),不依赖具体实现。就像你玩电脑只关心 USB 接口,里面插 U 盘还是鼠标随你换。

📌 Spring Boot 的默认组合 = Slf4j + Logback 。好消息是:你什么都不用引 ------spring-boot-starter-web 已经通过传递依赖把 spring-boot-starter-logging(内含 Logback + Slf4j)带进来了,开箱即用。


三. 日志级别:给日志分个轻重缓急

不是所有日志都同等重要。Slf4j 定义了 5 个级别,优先级从低到高

级别 含义 典型用途
trace 追踪 最细粒度,一步一步跟,一般不用
debug 调试 开发期排查问题,看变量值、走哪条分支
info 信息 正常运行的关键节点(最常用),如"接收到请求""操作成功"
warn 警告 潜在问题,暂时不影响运行,如"参数缺失用了默认值"
error 错误 出错了需要关注,如"数据库连不上""空指针异常"

📌 关键规则(记死) :配置的级别是门槛 ------设置 level="info" 表示"只输出 info 及以上级别",即 info / warn / error 会输出,debug / trace 被过滤掉。

  • 开发环境一般设 debug,看得越细越好;
  • 生产环境一般设 info,避免日志刷屏拖慢性能。
  • 特殊值:ALL = 全部输出,OFF = 全部关闭。

对应到代码就是 5 个方法:

java 复制代码
log.trace("trace 级别");
log.debug("debug 级别,调试变量: {}", someVar);
log.info("info 级别,正常运行信息");
log.warn("warn 级别,警告");
log.error("error 级别,出错啦");

四. 实战:@Slf4j 一步替换 System.out

4.1 原始写法(不用 Lombok)

不用 Lombok 的话,每个类都要先手动获取一个日志对象:

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class DeptController {

    // 每个类都要写这一行样板代码,拿到日志对象
    private static final Logger log = LoggerFactory.getLogger(DeptController.class);

    @GetMapping
    public Result list(){
        log.info("查询部门列表");
        return Result.success(deptService.findAll());
    }
}

能跑,但每个类都得复制那行 private static final Logger log = ...,很啰嗦。

4.2 推荐写法:Lombok 的 @Slf4j

咱们项目本来就引了 Lombok,直接在类上加一个 @Slf4j 注解即可:

java 复制代码
package com.itheima.controller;

import com.itheima.pojo.Dept;
import com.itheima.pojo.Result;
import com.itheima.service.DeptService;
import lombok.extern.slf4j.Slf4j;   // @Slf4j 的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Slf4j                                    // 关键注解:自动生成 log 对象
@RequestMapping("/depts")                // 类级公共路径
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    @GetMapping
    public Result list(){
        log.info("查询部门列表");              // 直接用 log,无需手动定义
        return Result.success(deptService.findAll());
    }

    @DeleteMapping
    public Result delete(Integer id){
        log.info("根据 id 删除部门 , id: {}", id);   // {} 占位符
        deptService.deleteById(id);
        return Result.success();
    }

    @PostMapping
    public Result save(@RequestBody Dept dept){
        log.info("新增部门 , dept: {}", dept);
        deptService.save(dept);
        return Result.success();
    }

    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        log.info("根据 ID 查询 , id: {}", id);
        return Result.success(deptService.getById(id));
    }

    @PutMapping
    public Result update(@RequestBody Dept dept){
        log.info("修改部门 , dept: {}", dept);
        deptService.update(dept);
        return Result.success();
    }
}

📌 @Slf4j 到底干了啥? 它是 Lombok 提供的注解,编译时自动为当前类生成一句:

java 复制代码
private static final Logger log = LoggerFactory.getLogger(DeptController.class);

所以你直接 log.info(...) 就能用,省掉每个类的样板代码。✅


五. 细节:{} 占位符为什么比字符串拼接好?

你可能注意到我用的是 log.info("id: {}", id),而不是 "id: " + id。这不是炫技,是有讲究的:

java 复制代码
log.info("根据 id 删除部门 , id: {}", id);   // ✅ 推荐:占位符
log.info("根据 id 删除部门 , id: " + id);    // ❌ 不推荐:字符串拼接

📌 区别在性能

  • 拼接写法 :不管这条日志最终输不输出,"..." + id 都会先执行字符串拼接 ,白白消耗性能。如果当前级别是 info 而你写的是 log.debug,拼接照样发生但日志却不输出,纯浪费。
  • 占位符写法 :Slf4j 会先判断级别 ------只有这条日志真的要输出时,才把参数填进 {} 占位。级别不够就直接跳过,不做无用功。

结论 :带参数的日志一律用 {} 占位符,多个参数按顺序写:log.info("a={}, b={}", a, b)


六. logback.xml:把日志存到文件

默认情况下日志只输出到控制台。要持久化到文件 、按天/按大小切分,就需要在 resources 下加一个 logback.xml 配置文件。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- ① 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 输出格式:时间 [线程] 级别 类名 - 消息 换行 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- ② 输出到文件(按大小 + 时间滚动切分) -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 文件名:按天分目录,%i 是当天内超过大小后的序号 -->
            <FileNamePattern>D:/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
            <MaxHistory>30</MaxHistory>     <!-- 只保留最近 30 天的日志 -->
            <maxFileSize>10MB</maxFileSize> <!-- 单个文件超 10MB 就滚动出新文件 -->
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- ③ 根日志器:全局级别 info,同时输出到控制台和文件 -->
    <root level="info">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE" />
    </root>

</configuration>

📌 几个易忘点(记死)

  1. appender 就是"输出目的地":ConsoleAppender 输出到控制台,RollingFileAppender 输出到文件(可滚动)。一个日志可以同时发给多个 appender。
  2. pattern 占位符%d 时间、%thread 线程名、%-5level 级别(左对齐占 5 位)、%logger{50} 打印日志的类名(最长 50)、%msg 日志内容、%n 换行。
  3. SizeAndTimeBasedRollingPolicy :表示"按大小 + 时间"双重滚动------文件到 10MB 或跨天就切一个新文件,MaxHistory 控制最多保留多少天的旧文件,防止磁盘被日志塞满。
  4. <root level="info"> :全局只输出 info 及以上级别。想给某个包单独调级别,可以加 <logger name="com.itheima" level="debug"/>

⚠️ 文件名 D:/tlias-...log 里写的是路径,换成你自己系统存在的目录;Linux 服务器上就写 /var/log/xxx.log 之类。


七. 常见坑(踩过才记得住)

  1. @Slf4j 不生效、log 报红 :项目是 Lombok 注解,IDEA 需要装 Lombok 插件 并开启注解处理(新版 IDEA 内置)。没装的话 @Slf4j 生不出 log 变量。
  2. 导错 Logger 的包Logger / LoggerFactory 必须来自 org.slf4j,别导成 JUL 的 java.util.logging.Logger 或 Log4j 的,那就不是门面了。
  3. logback.xml 放错位置 / 文件名错 :必须放在 resources 根目录、文件名严格叫 logback.xml(或 logback-spring.xml)。名字写错、放子目录都不会被加载。
  4. 级别设错导致看不到日志 :root 设成 info,你写的 log.debug 自然不输出------不是没执行,是被级别过滤了。调试时临时调成 debug
  5. 日志文件没生成FileNamePattern 里的磁盘路径不存在或无写入权限。Windows 确认盘符目录存在,Linux 确认目录有写权限。
  6. 字符串拼接代替占位符"id=" + id 在级别不达标时仍会拼接,浪费性能。统一用 log.info("id={}", id)
  7. 每个类手动 new Logger 还写错类名LoggerFactory.getLogger(XxxController.class) 里类名复制错,日志里显示的类名就对不上。用 @Slf4j 就没这问题。
  8. 把敏感信息打进日志 :密码、身份证号、token 别 log.info 输出,生产日志可能被很多人看到,有安全风险。

八. 面试题速记

  • ① Slf4j 和 Logback 是什么关系? Slf4j 是日志门面 (只定义统一接口,不实现),Logback 是日志实现(真正干活)。代码面向 Slf4j 接口编程,运行时绑定 Logback,符合"面向接口编程",底层实现可插拔替换而业务代码不变。
  • ② 为什么不直接用 Logback 的 API? 那样代码就和 Logback 强耦合了,将来想换 Log4j2 就得改所有日志代码。用 Slf4j 门面,换实现只需换依赖和配置。
  • ③ Spring Boot 默认用什么日志?要引依赖吗? 默认 Slf4j + Logback。不用额外引------spring-boot-starter-web 通过 spring-boot-starter-logging 已传递依赖进来,开箱即用。
  • ④ 日志级别有哪些?配置 info 会输出哪些? trace < debug < info < warn < error 五级。配 info 输出 info/warn/error,过滤掉 debug/trace。开发用 debug,生产用 info。
  • log.info("{}", x)"..." + x 好在哪? 占位符会先判断级别,日志不输出时就不做参数填充/拼接,避免无用字符串拼接的性能损耗;拼接写法无论是否输出都会先拼接。
  • ⑥ @Slf4j 注解的原理? Lombok 编译期自动生成 private static final Logger log = LoggerFactory.getLogger(当前类.class);,省去手动定义日志对象的样板代码。
  • ⑦ 怎么把日志存到文件并按天切分?logback.xml,用 RollingFileAppender + SizeAndTimeBasedRollingPolicyFileNamePattern 里用 %d{yyyy-MM-dd} 按天分、maxFileSize 按大小切,MaxHistory 控制保留天数。

九. 结语

ok,到这儿 Logback + Slf4j 日志技术 就讲透了 🎉 咱们用一句话收个尾:

Java 打日志 = 代码里用 @Slf4j 拿到 log → 按级别 log.info/debug/... 输出(带参用 {} 占位)→ 配 logback.xml 决定输出到控制台还是文件、按天按大小切分。门面 Slf4j 定标准,实现 Logback 干活,Spring Boot 默认全配好,你只管用。

回头把部门管理实战里的 System.out.println 全换成 log.info,你的项目就立马"专业"了一个档次 👌 这篇和《后端 Web 实战 · 部门管理》《Spring Boot 整合 MyBatis CRUD》是同一条线的,连起来看效果最好。

如果对你有帮助的话,不妨点一个免费的赞和收藏 👍 你们的支持就是我最大的动力!有问题欢迎评论区交流,咱们下篇继续"从零开始"系列~

标签#java #spring boot #logback #slf4j #后端 #学习笔记

相关推荐
韶博雅1 小时前
开启补充日志
java·开发语言·sql
炎武丶航1 小时前
汽车功能测试学习(2):LDW车道偏离预警
功能测试·学习·汽车
Logintern091 小时前
【AGV】openTCS学习
学习·物流·agv
FYKJ_20101 小时前
django学习成绩预警系统10905
java·javascript·spring boot·python·spark·django·php
wzq11_6661 小时前
云计算运维学习day22——Ansible-Roles
运维·学习·云计算
卓怡学长1 小时前
w192基于springboot教务管理系统
java·数据库·spring boot·spring·maven·intellij-idea
逸Y 仙X1 小时前
Spark SQL
java·大数据·sql·spark
码农颜1 小时前
6.4.1 监听连接
java·数据库·mysql
mldong1 小时前
会签三兄弟:并行/串行/按比例的实现与取舍
java·架构