为什么你的日志总是“食之无味”?
日志是Java程序排查问题的“生命线”,但很多开发者的日志总是打不到重点,无法精准定位问题,今天我们来聊聊如何几个高质量日志打印方法
前提
建议使用统一的日志框架SLF4J + Logback/Log4j2
// 正确引入(类顶部)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
// 静态常量,避免每次创建Logger实例
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
}
1.参数完整
反例:只说 “失败了”,要说清 “为什么失败”
log.info("用户登录失败");
哪个用户失败了?在何时?失败原因是什么?
核心原则:日志必须包含「上下文 + 参数 + 异常信息」
正例:
log.warn("用户登录失败 name={}, IP={}, time={}, failReason={}",
username, clientIP, new Date(),"密码错误");
2.日志级别使用不当
反例:级别滥用
// 反例1:调试信息用INFO,生产环境日志刷屏
log.info("订单查询参数:orderId={}", 123456);
// 反例2:致命异常用WARN,易被忽略
log.warn("数据库连接失败,无法创建订单");
// 反例3:ERROR打印非异常信息(如正常流程)
log.error("订单创建成功,orderId={}", 123456);
核心原则:日志级别按 “从细到粗” 分为 TRACE < DEBUG < INFO < WARN < ERROR。
级别定义表:
级别 | 适用场景 |
|---|---|
TRACE | 极细粒度的调试信息(如框架内部流程) |
DEBUG | 开发/测试环境的调试信息(如参数、执行步骤) |
INFO | 生产环境核心流程(如服务启动、订单创建成功) |
WARN | 非致命异常(如参数不合法、资源不足) |
ERROR | 致命异常(如调用第三方接口失败、数据库连接异常) |
正例
// DEBUG:开发环境调试,生产可关闭
log.debug("订单查询参数:orderId={}", 123456);
// INFO:核心业务流程,生产必打
log.info("订单创建成功,orderId={},用户ID={}", 123456, 789);
// WARN:非致命问题,需关注但不阻断流程
log.warn("订单参数不合法:userId={},原因:{}", 789, "用户ID为空");
// ERROR:致命异常,必须包含异常栈+上下文
try {
createOrder();
} catch (SQLException e) {
log.error("创建订单失败,orderId={}", 123456, e);
}
3.异常必须打印堆栈
反例
// 反例1:仅打印异常信息,丢失栈
try {
orderDao.insert(order);
} catch (SQLException e) {
log.error("插入订单失败:{}", e.getMessage());
}
// 反例2:重复打印异常栈
try {
orderDao.insert(order);
} catch (SQLException e) {
log.error("插入订单失败", e);
throw new BusinessException("插入失败", e); // 上层又打印一次,日志冗余
}
核心原则:异常日志必须传递Throwable对象
正例
// 正例1:打印完整异常栈
try {
orderDao.insert(order);
} catch (SQLException e) {
log.error("插入订单失败,订单ID={}", order.getId(), e);
// 若需要抛上层,无需重复打印,上层统一处理
thrownew BusinessException("插入订单失败", e);
}
// 正例2:吞异常时必须打日志
try {
thirdService.sendMsg(userId, msg);
} catch (Exception e) {
// 允许消息发送失败,但必须记录日志
log.warn("消息发送失败,用户ID={},消息内容={}", userId, msg, e);
}
4.敏感信息泄露
打印日志泄露用户手机号有可能被投诉
反例
// 反例1:打印完整手机号/身份证
log.info("用户登录,手机号={},身份证={}", "13812345678", "110101199001011234");
// 反例2:打印密码/令牌
log.debug("调用第三方接口,token={},密码={}", "admin_edfojofwog", "123456");
核心原则:用户手机号、身份证、密码、银行卡号等敏感信息,脱敏后再打印
正例
// 工具方法:手机号脱敏(138****5678)
private static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
// 工具方法:身份证脱敏(110101********1234)
private static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() != 18) {
return idCard;
}
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
}
// 正例:打印脱敏后的信息
log.info("用户登录,手机号={},身份证={}", maskPhone("13812345678"), maskIdCard("110101199001011234"));
log.debug("调用第三方接口,token={}", "******");
5.占位符的正确打开方式
反例
log.info("User:" + user.getId() + " username " + user.getName());
核心原则:别用 “+”,避免性能损耗
正例
log.info("User:{} username {}", user.getId(), user.getName());
6.MDC链路追踪
MDC.put("traceId", UUID.randomUUID().toString().substring(0,8));
//logback.xml配置
<pattern>%d{yyyy-MM-dd} [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
核心原则:多个服务调用可以通过traceId全联路追踪
输出结果
输出效果: 2026-01-13 [e44d4gae-6kk4-3er4-rr4t-t0ykyreg] INFO com.example.OrderService - 下单成功
往期文章回顾
Redis 有一亿个key,如何优雅捞出 10 万条前缀 Key 的实战方案
1338

被折叠的 条评论
为什么被折叠?



