原创 · 上篇咱们做部门管理实战时,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>
📌 几个易忘点(记死):
appender就是"输出目的地":ConsoleAppender输出到控制台,RollingFileAppender输出到文件(可滚动)。一个日志可以同时发给多个 appender。pattern占位符 :%d时间、%thread线程名、%-5level级别(左对齐占 5 位)、%logger{50}打印日志的类名(最长 50)、%msg日志内容、%n换行。SizeAndTimeBasedRollingPolicy:表示"按大小 + 时间"双重滚动------文件到 10MB 或跨天就切一个新文件,MaxHistory控制最多保留多少天的旧文件,防止磁盘被日志塞满。<root level="info">:全局只输出 info 及以上级别。想给某个包单独调级别,可以加<logger name="com.itheima" level="debug"/>。
⚠️ 文件名 D:/tlias-...log 里写的是路径,换成你自己系统存在的目录;Linux 服务器上就写 /var/log/xxx.log 之类。
七. 常见坑(踩过才记得住)
- ❌ @Slf4j 不生效、log 报红 :项目是 Lombok 注解,IDEA 需要装 Lombok 插件 并开启注解处理(新版 IDEA 内置)。没装的话
@Slf4j生不出log变量。 - ❌ 导错 Logger 的包 :
Logger/LoggerFactory必须来自org.slf4j,别导成 JUL 的java.util.logging.Logger或 Log4j 的,那就不是门面了。 - ❌ logback.xml 放错位置 / 文件名错 :必须放在
resources根目录、文件名严格叫logback.xml(或logback-spring.xml)。名字写错、放子目录都不会被加载。 - ❌ 级别设错导致看不到日志 :root 设成
info,你写的log.debug自然不输出------不是没执行,是被级别过滤了。调试时临时调成debug。 - ❌ 日志文件没生成 :
FileNamePattern里的磁盘路径不存在或无写入权限。Windows 确认盘符目录存在,Linux 确认目录有写权限。 - ❌ 字符串拼接代替占位符 :
"id=" + id在级别不达标时仍会拼接,浪费性能。统一用log.info("id={}", id)。 - ❌ 每个类手动 new Logger 还写错类名 :
LoggerFactory.getLogger(XxxController.class)里类名复制错,日志里显示的类名就对不上。用@Slf4j就没这问题。 - ❌ 把敏感信息打进日志 :密码、身份证号、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+SizeAndTimeBasedRollingPolicy,FileNamePattern里用%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 #后端 #学习笔记