Java日志框架和使用、日志记录规范

日志常用于输出到控制台或指定文件,帮助我们记录用户行为以及排查bug

最早接触的就是System.out.println将内容输出到控制台,也导致经常在后续开发中可能顺手就写了sout来记录。

这种方式虽然简单,但在真实项目中远远不够。正式开发里,我们更推荐使用标准的日志框架,比如 SLF4J + Logback 或 SLF4J + Log4j2。

一、为什么不能只用 System.out.println

System.out.println() 的优点只有一个:简单,写了就能看到输出。

但它的问题也很明显:

  • 没有日志级别,无法区分普通信息、警告、错误
  • 无法统一格式,排查问题时信息杂乱
  • 不方便输出到文件、日志平台、监控系统
  • 不能按环境灵活控制输出
  • 高并发、生产环境下几乎不适合作为正式日志手段

例如:

java 复制代码
System.out.println("用户登录成功:" + userId);

这句代码只是把一行文字打印到控制台,既没有时间、线程、类名,也无法统一收集。

而日志框架的写法通常是:

java 复制代码
log.info("用户登录成功, userId={}", userId);

它的价值在于:

  • 有明确的日志级别
  • 支持格式化输出
  • 能输出到控制台、文件或日志系统
  • 可以按配置决定哪些日志要打印、哪些不打印
  • 更适合线上运维和问题排查

所以,sout 更像临时调试工具,log.info() 才是项目开发中的标准做法。


二、Java 常见日志框架

Java 日志体系里,经常会看到好几个名字,初学者容易混淆。可以把它们分成三类来看。

1. 日志门面

日志门面本身不负责真正输出日志,它只负责"定义统一接口",让业务代码不直接依赖某个具体实现。

常见门面:

  • SLF4J
  • JCL(Jakarta Commons Logging,较老)

目前主流项目中最常见的是 SLF4J。

它的好处是:代码里统一写 log.info()、log.error(),底层到底用 Logback 还是 Log4j2,可以后续替换,不影响业务代码。


2. 日志实现

日志实现才是真正把日志打印出来、写到文件里的组件。

常见实现:

  • Logback
  • Log4j2
  • java.util.logging

其中:

  • Logback:Spring Boot 默认常用实现,配置方便,生态成熟
  • Log4j2:性能好、功能强,企业项目也很常见
  • java.util.logging:JDK 自带,但实际项目中使用相对少

3. 历史框架

  • Log4j 1.x:老版本,现在一般不推荐新项目使用
  • System.out.println():只能算打印,不算真正意义上的日志框架

三、主流推荐方案

在现在的 Java 项目中,推荐组合一般是:

  • SLF4J + Logback
  • SLF4J + Log4j2

如果是 Spring Boot 项目,默认通常就是 SLF4J + Logback,上手成本最低,适合大多数场景。

可以记一个简单结论:

开发中优先面向 SLF4J 编码,具体实现选 Logback 或 Log4j2。


四、日志级别怎么理解

日志框架通常提供多个级别,用于表示信息的重要程度。常见级别从低到高如下:

  • TRACE
  • DEBUG
  • INFO
  • WARN
  • ERROR

下面逐个理解。

1. TRACE

最细粒度的跟踪信息,一般用于非常详细的程序执行过程。

java 复制代码
log.trace("进入方法 checkPermission, userId={}", userId);

平时业务开发中很少开。


2. DEBUG

用于开发调试,记录一些关键变量、执行路径、参数信息。

java 复制代码
log.debug("查询条件: userId={}, status={}", userId, status);

开发环境常用,生产环境通常关闭或谨慎开启。


3. INFO

记录正常业务流程中的关键信息,是项目里最常用的级别之一。

java 复制代码
log.info("订单创建成功, orderId={}, userId={}", orderId, userId);

比如:

  • 服务启动成功
  • 用户登录成功
  • 订单创建成功
  • 定时任务开始/结束

4. WARN

表示程序出现异常情况或风险,但还没有导致功能彻底失败。

java 复制代码
log.warn("库存不足, skuId={}, remain={}", skuId, remain);

比如:

  • 参数不规范但系统做了兼容
  • 某个远程调用超时后触发重试
  • 检测到配置缺失但系统使用默认值继续运行

5. ERROR

表示发生了明确错误,通常需要重点关注。

java 复制代码
log.error("支付失败, orderId={}", orderId, e);

比如:

  • 数据库操作失败
  • 接口调用异常
  • 程序捕获到未预期异常

五、日志的基本使用方式

下面用常见的 SLF4J 风格说明日志怎么写。

1. 定义日志对象

传统写法:

java 复制代码
public class UserService {
    private static final Logger log = 
            LoggerFactory.getLogger(UserService.class);
}

如果项目使用 Lombok,也可以简化为:

java 复制代码
@Slf4j
public class UserService {
    
}

然后直接使用:

java 复制代码
log.info("日志打印");

2. 参数化输出,不要字符串拼接

推荐写法:

java 复制代码
log.info("用户登录成功, userId={}", userId);

不推荐:

java 复制代码
log.info("用户登录成功, userId=" + userId);

原因:

  • 代码更整洁
  • 日志框架会更高效地处理参数
  • 可读性更好

多个参数时:

java 复制代码
log.info("订单创建成功, orderId={}, userId={}, amount={}", orderId, userId, amount);

3. 异常日志要带堆栈

正确写法:

java 复制代码
@Slf4j
public class test {
    public static void main(String[] args) {
        log.info("日志打印");
        try {
            int x = 1 / 0;
        } catch (Exception e) {
            log.error("系统计算异常", e);
        }
    }
}

或者带业务上下文:

java 复制代码
 log.error("订单支付异常, orderId={}, userId={}", orderId, userId, e);

不推荐只打印异常信息:

java 复制代码
log.error("订单支付异常, orderId={}, userId={}", orderId, userId);

因为这样会丢失完整堆栈,不利于定位问题。


六、Spring Boot 中的日志使用

Spring Boot 默认对日志支持比较友好,一般引入 Web 依赖后就已经带了日志能力。

业务代码里通常直接这样写:

java 复制代码
@Slf4j
@Service
public class OrderService {
    public void createOrder(Long userId) {
        log.info("开始创建订单, userId={}", userId);
        log.info("订单创建成功, userId={}", userId);
    }
}

配置日志级别也比较方便,例如在 application.yml 中:

XML 复制代码
logging:
  level:
    root: info        # 全局默认日志级别
    com.example.demo: debug  # 你项目包的日志级别

表示:

  • 全局日志级别是 info
  • com.example.demo 包下允许输出 debug 级别日志

七、日志记录规范

下面是比较实用的一套日志规范。


1. 日志要有价值,避免"流水账"

不要什么都记,也不要一句话完全没有信息量。

不推荐:

java 复制代码
log.info("进入方法");

log.info("执行结束");

这种日志看起来很多,但对排查问题帮助有限。

推荐:

java 复制代码
log.info("开始处理退款申请, refundId={}, orderId={}", refundId, orderId);
log.info("退款处理完成, refundId={}, status={}", refundId, status);

要尽量带上业务上下文,让日志能回答:

  • 是谁触发的
  • 处理了什么对象
  • 结果是什么
  • 出错时影响了哪个业务数据

2. 关键业务日志必须带唯一标识

例如:

  • userId
  • orderId
  • requestId
  • traceId
  • taskId

示例:

java 复制代码
log.info("订单支付成功, orderId={}, userId={}", orderId, userId);

这样才能通过一个 ID 把整条调用链串起来。


3. 不要打印敏感信息

日志中严禁直接记录:

  • 密码
  • 身份证号完整信息
  • 银行卡完整号码
  • 手机号完整号码
  • token、密钥、验证码
  • 用户隐私数据

例如下面这种就不合适:

java 复制代码
log.info("用户登录, username={}, password={}", username, password);

正确做法是脱敏或不打印:

java 复制代码
log.info("用户登录, username={}", username);

如果必须记录手机号,也要脱敏处理。


4. INFO 记录业务节点,DEBUG 记录调试细节

很多项目的日志问题不是"太少",而是"太乱"。最常见错误是把大量细节都打成 info,导致生产日志噪声很大。

可以这样理解:

  • INFO:记录关键业务事件
  • DEBUG:记录开发调试信息
  • ERROR:记录失败和异常

例如:

java 复制代码
log.info("开始支付流程, orderId={}", orderId);
log.debug("支付请求参数: {}", request);
log.info("支付完成, orderId={}, response={}, userId={}, orderId, response, userId);

5. 不要重复记录同一个异常

一个异常如果层层捕获、层层 log.error(),最后日志里就会出现多份重复堆栈,反而干扰排查。

常见原则:

  • 能处理就处理,不一定要打 error
  • 不能处理再往上抛
  • 最终由边界层统一记录一次完整异常

例如在 service 里:

java 复制代码
catch (Exception e) { 
    throw new BusinessException("支付失败", e); 
}

在 controller 或全局异常处理中统一打印:

java 复制代码
log.error("请求处理失败, requestId={}", requestId, e);

这样更清晰。


6. 日志内容要统一、可检索

建议统一格式,避免同一种业务场景每个人写法都不一样。

例如统一风格:

java 复制代码
log.info("订单创建成功, orderId={}, userId={}, amount={}", orderId, userId, amount);

统一风格的好处是:

  • 搜索方便
  • 排查高效
  • 团队协作成本低

7. 重要操作必须记日志

下面这些场景通常建议记录日志:

  • 用户登录、登出
  • 创建订单、支付、退款
  • 定时任务执行
  • 第三方接口调用结果
  • 权限校验失败
  • 数据导入导出
  • 配置加载、服务启动与停止
  • 关键异常和兜底逻辑

8. 高频循环里谨慎打日志

如果在循环中大量打印日志,可能导致:

  • 日志量暴增
  • 磁盘占用过快
  • IO 压力变大
  • 排查时淹没关键信息

例如:

java 复制代码
for (User user : userList) {
   log.info("处理用户: {}", user.getId());
}

如果数据量很大,这种写法就要非常谨慎。

更好的方式可能是汇总记录:

java 复制代码
log.info("开始批量处理用户, size={}", userList.size());

处理结束后再输出结果摘要。

八、一个简单的日志示例

下面给一个比较符合规范的 service 示例:

java 复制代码
@Slf4j
@Service
public class OrderService {
    public void pay(Long orderId, Long userId) {
        log.info("开始支付订单, orderId={}, userId={}", orderId, userId);
        try {
            // 1. 参数校验 
            log.debug("校验订单状态, orderId={}", orderId);
            // 2. 执行支付逻辑 
            log.debug("调用支付网关, orderId={}", orderId);
            // 3. 支付成功 
            log.info("订单支付成功, orderId={}, userId={}", orderId, userId);
        } catch (Exception e) {
            log.error("订单支付失败, orderId={}, userId={}", orderId, userId, e);
            throw e;
        }
    }
}

这个例子体现了几个点:

  • info 记录关键业务节点
  • debug 记录内部处理细节
  • error 记录失败并保留异常堆栈
  • 日志中带了 orderId 和 userId 这样的关键上下文

九、实际开发中的常见误区

1. 把 System.out.println() 当正式日志用

这是最常见的初级问题。临时调试可以,但不能作为项目正式方案。

2. 所有日志都打 info

这会让生产日志变得非常吵,真正重要的信息反而不明显。

3. 只打印"异常了",不打印上下文

例如:

java 复制代码
log.error("支付失败");

这太模糊了。至少要带上订单号、用户 ID 等关键信息。

4. 只打印 e.getMessage(),不打印堆栈

这样往往无法定位代码具体在哪一行出错。

5. 打印敏感数据

这是日志安全的大忌。

6. 每层都打印同一个异常

会导致重复日志,影响排查效率。


十、总结

Java 日志框架的核心价值,不只是"输出内容",而是帮助我们在开发、测试、上线、运维整个生命周期里快速理解系统发生了什么。

从实践角度来说,可以记住这几个结论:

  1. 正式项目不要依赖 System.out.println(),而要使用日志框架。
  2. 推荐面向 SLF4J 编码,底层常用 Logback 或 Log4j2。
  3. 合理区分 debug、info、warn、error。
  4. 日志要带关键业务上下文,比如 userId、orderId、traceId。
  5. 异常日志要保留完整堆栈,不要只打印一句错误信息。
  6. 严禁记录密码、密钥、身份证号等敏感数据。
  7. 日志要服务于排查问题,而不是机械地"打印几行字"。
相关推荐
AC赳赳老秦1 小时前
接口测试自动化:用 OpenClaw 对接 Postman,实现批量回归测试、测试报告自动生成与推送
java·人工智能·python·算法·elasticsearch·deepseek·openclaw
pq2172 小时前
最简单的理解synchronized锁升级
java
杨凯凡2 小时前
【032】排查入门:jstack、heap dump、Arthas 初识
java·开发语言·后端
pq2172 小时前
Spring FactoryBean源码解析
java·spring boot·spring
其实防守也摸鱼2 小时前
无线网络安全--实验 规避WLAN验证之发现隐藏的SSID
java·开发语言·网络·安全·web安全·智能路由器·无线网络安全
l1t2 小时前
astral-sh发布的musl和gnu版本standalone python 性能比较
开发语言·python
书源丶2 小时前
四十三、网络编程(下)——TCP 编程与 HTTP 入门
java·网络·tcp/ip·http
木井巳2 小时前
【递归算法】单词搜索
java·算法·leetcode·决策树·深度优先
阿豪只会阿巴2 小时前
【没事学点啥】TurboBlog轻量级个人博客项目——Turbo Blog 项目学习与上线指南
开发语言·python·学习·状态模式