Java日志打印的6个高质量方法:反例 + 正例,告别无效日志

为什么你的日志总是“食之无味”?

日志是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 的实战方案

@Transactional 本质是 AOP,事务失效就看这 1 点

Java 8 都老了,Optional 为啥还是没人用?到底卡在哪了?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值