Java 里该怎样打日志

更新于 2025-12-30

Marco Behler 2020-12-09

你可以通过本指南发现、理解并选择适合你应用程序的 Java 日志库,比如 Log4j2、Logback 或 java.util.logging。

日志记录“看起来”是个非常简单的主题,但在实践中却相当棘手,而且在任何地方都没有被充分详细地讲解过。阅读本指南,全面理清 Java 日志生态。

引言

每个 Java 应用程序迟早都需要日志记录。

也许你只是想把系统状态或用户操作记录到文件中,以便运维人员了解发生了什么:

logger.info("Application successfully started on port 8080");

也许你需要在发生异常时记录错误消息,并向相关人员发送电子邮件或短信以进行紧急干预:

logger.error("Database connection is down", exception);

又或者你的某个批处理作业希望在无法导入 CSV 文件中的某些记录时,将警告信息记录并发送到一个集中式的图形化日志服务器:

logger.warn("Invalid bank account number for record=[{}]", 53);

无论你想做什么,你都需要确保使用了合适的日志库,并正确配置和使用它。

不幸的是,Java 生态中有大量可用的日志库,开发者应该大致了解为什么会有这么多选项,以及何时该使用哪个选项。

让我们一探究竟。

老旧日志库

要理解 Java 日志库的最新发展,有必要先认识并了解那些“恐龙”——Java 最早的日志库,如今仍可在一些生产环境中见到。

java.util.logging (JUL)

自 Java 1.4(2002 年)起,JDK 自带了一个日志“框架”,名为 java.util.logging,常简称为 JUL。以下是使用 JUL 记录日志的示例:

// java.util.logging
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(this.getClass().getName());
logger.info("This is an info message");
logger.severe("This is an error message"); // == ERROR
logger.fine("Here is a debug message");   // == DEBUG

与其他日志库一样,首先为特定类或包获取一个 Logger,然后就可以记录日志语句。你可能会觉得 severefine 这些日志级别名称很奇怪,但它们基本上对应现代日志库中的 errordebug 级别。

当然,你也可以通过所谓的 Handler 来配置 Logger,比如 FileHandler(将日志写入文件)或 ConsoleHandler(写入 System.err):

FileHandler fileHandler = new FileHandler("status.log");
logger.addHandler(fileHandler);

那么问题来了:既然有 JUL,为什么还需要其他日志框架?

虽然 JUL 能完成基本任务,但过去对其缺点有不少讨论,包括 API 不一致、性能慢、缺乏(复杂)配置选项、文档不足等——这些最终促使人们开发并使用其他日志框架。


Log4j (v1)

长期以来,Java 领域最流行的日志选择是 Log4j(版本 1),最初发布于 2001 年,维护至 2015 年。事实上,截至 2018 年,你仍能在不少企业项目中看到它的身影。

使用 Log4j 的日志示例如下:

// Log4j V1
org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MyClass.getClass().getName());
logger.info("This is an info message");
logger.error("This is an error message");
logger.debug("Here is a debug message");

Log4j 不仅拥有合理的日志级别名称(如 errordebug),还提供了大量巧妙的 Appender,例如:

  • SMTPAppender(通过邮件发送日志事件)
  • SyslogAppender(发送到远程 syslog 守护进程)
  • JDBCAppender(写入数据库)

此外,它还通过 PatternLayout 让你精确控制日志消息的格式。同一个日志事件可根据布局配置输出为不同形式:

# status.log 内容示例

[INFO] 2012-11-02 21:57:53,662 MyLoggingClass - Application successfully started on port 8080

# 或

2010.03.23-mainThread --INFO -MyLoggingClass:Application successfully started on port 8080

# 或其他无限可能

Log4j 表现不错,但近年来已被 Log4j2 取代。由于 Log4j2 与 Log4j1 不完全兼容,我们将在下一节讨论它。


Apache Commons Logging (JCL)

大约在同一时期(2002 年),另一个名为 JCL(Jakarta Commons Logging 或 Apache Commons Logging)的库出现了。

JCL 的有趣之处在于:它本身不是日志框架的实现,而是一个面向其他日志实现的接口。这意味着什么?

如你所料,日志代码本身很简单,只是不再引用 JUL 或 Log4j 类,而是引用 JCL 类(前提是类路径中有 JCL 库):

// Apache Commons Logging
org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(MyApp.class);
log.info("This is an info message");
log.error("This is an error message");
log.debug("Here is a debug message");

你的代码只使用 JCL 特定的类,但实际日志记录由另一个日志框架完成——无论是 Log4j、JUL 还是早已废弃的 Apache Avalon。

这意味着你需要在类路径中添加另一个日志库(如 Log4j),并配置两者协同工作。

为什么要这样做?

为什么不直接使用 Log4j 呢?主要用例是编写库

当你编写一个库时,你不知道使用者希望在其应用中使用哪种日志框架。因此,使用一个日志接口(如 JCL)编写库是合理的,这样用户在部署自己的应用时可以插入任意日志实现。

有什么陷阱?

JCL 的问题是它依赖类加载器 hack在运行时动态发现应使用的日志实现,这可能导致很多麻烦。此外,其 API 有些僵化,包含不少冗余代码,如今已有更好的替代方案。


现代日志库

SLF4J & Logback

某天,Log4j 的原作者 Ceki Gülcü 决定离开 Log4j 项目,创建了继任者——Logback(而非 Log4j2)。你可以在这里了解他试图改进的地方。

简而言之,Logback 是一个成熟、功能丰富的日志库,其中最令人印象深刻的功能之一是生产环境中自动重载配置文件

几乎同时,他还开发了 SLF4J(Simple Logging Facade for Java),它与上述 JCL 类似,但实现更优。具体含义如下:

要开始使用 SLF4J,只需在类路径中加入 slf4j-api 依赖(以下为 Maven 示例):

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>

有了 API 后,即可编写如下日志语句:

// SLF4J
org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MyClass.class);
logger.info("This is an info message");
logger.error("This is an error message");
logger.debug("Here is a debug message"); // 无需再写 logger.isDebugEnabled() 检查,SLF4J 会自动处理

与 JCL 一样,SLF4J 本身不能记录日志,需要绑定具体的日志实现(如 Log4j、JUL、Logback 等)。

例如,若想使用 Log4j v1,则需添加 slf4j-log4j12 绑定库:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.30</version>
</dependency>

该依赖会自动引入 Log4j v1,并确保 SLF4J 通过 Log4j 输出日志。具体原理可参考 SLF4J 手册的绑定章节

而像 Logback 这样的库则原生实现了 SLF4J,因此只需添加 logback-classic

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

这种设计的妙处在于:你的代码只依赖 SLF4J,不涉及 Log4j、Logback 或 JUL。如果你在编写库,这一点尤其重要——使用者可以自由选择底层日志实现,只需调整类路径中的 JAR 包即可。


第三方库的兼容问题

但现实往往更复杂:你使用的第三方库可能硬编码了特定日志库。例如:

  • PDF 生成库强制使用 Log4j
  • 邮件发送库使用 JUL
  • 你自己的应用使用 SLF4J

你无法修改这些库的源码使其改用 SLF4J,怎么办?

幸好,SLF4J 的作者已考虑到此场景。解决方案是使用 桥接库(bridging libraries)

例如,当引入一个使用 Log4j 的第三方库时,Maven 会自动拉取:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

此时,你应该排除该依赖,改用以下 SLF4J 替代品:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.30</version>
</dependency>

关键技巧log4j-over-slf4j.jar 中包含 org.apache.log4j.Logger 等类,但它们并非真正的 Log4j 类,而是 SLF4J 的代理类——你的代码“以为”在调用 Log4j,实际上所有日志都被路由到 SLF4J。

同理,还有 jul-to-slf4jjcl-over-slf4j 等桥接库(注意:JUL 桥接机制略有不同,详见此处)。


实际应用场景

根据项目类型和第三方依赖,你的类路径中可能包含:

  • SLF4J API
  • SLF4J 实现(如 Logback 或 Log4j)
  • 一个或多个桥接库(如 log4j-over-slf4jjul-to-slf4j 等)

核心要点

使用 SLF4J 时,你面向 API 编程,可在编译时自由选择实现(Logback、Log4j 等)。同时,通过桥接库可让遗留的第三方库“说 SLF4J”。

虽然初学者可能觉得复杂,但稍加实践便会豁然开朗。


Log4j2

有人可能认为 SLF4J 及其生态已满足所有需求,但事实并非如此。2014 年,Log4j(v1)的继任者 Log4j2 发布——这是一次彻底重写,并深受现有日志库启发。

与 SLF4J/JCL 类似,Log4j2 也分为两部分:

  • API 依赖

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.0</version>
    </dependency>
    
  • 实现依赖

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.0</version>
    </dependency>
    

API 层可与其他日志框架配合使用(类似 SLF4J)。你的代码只需引用 Log4j2 类:

// Log4j (version 2)
org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(MyApp.class);
logger.info("This is an info message");
logger.error("This is an error message");
logger.debug("Here is a debug message");

你可能会疑惑:SLF4J 和 Log4j2 如此相似,为何还要用 Log4j2?

Log4j2 团队对此作出了解释,主要优势在于:

  • 性能:异步日志器(AsyncLogger)、减少垃圾回收
  • API 改进:支持直接记录对象(非仅字符串)、Lambda 表达式支持等

不过,这些优势主要在高负载复杂应用中体现,普通应用开发者可能感知不到差异。


JBoss Logging

讨论日志库若不提 JBoss Logging 就不完整。它是另一个日志桥接层,与 SLF4J/JCL 类似,需配合具体实现(甚至可与 SLF4J 一起使用)。

其主要特色是国际化支持。除此之外,很少有理由将整个项目基于 JBoss Logging。不过你会在 Hibernate 等 RedHat 旗下项目中见到它。


如何记录日志

选定日志框架后,就该实际使用了。那么问题来了:该如何记录日志?

技术细节:Logger 变量命名

观察不同 Java 项目(甚至同一项目),你会发现获取 Logger 实例的方式五花八门:

// class 1
private Logger LOG = LoggerFactory.getLogger(...);

// class 2
private static final Logger LOGGER = ....;

// class 3
private static Logger log = Logger.getLogger(...);

// class 4
private Logger LOG_INSTANCE = ...;

最佳实践:如果类名和方法名都包含 "logger",变量名就叫 logger

不必过分纠结 static/非 staticfinal/非 final,但务必在整个项目中保持一致

另外,不要仅仅为了突出而将 logger 全大写(尤其当它是代码库中唯一全大写的变量时)。


日志级别与文件

一个极其重要的话题是:到底该用哪个日志级别? 可选级别包括 TRACE、DEBUG、INFO、WARN、ERROR、FATAL,许多开发者对此感到困惑。

以下是一种经实践验证的有效策略(非金科玉律,可根据实际情况调整,但需有充分理由,并确保开发与运维团队达成共识)。

错误类日志级别

FATAL

表示 Java 进程无法继续,即将终止。
极少使用,SLF4J 等 API 甚至不直接支持。

ERROR

请求被中止,且需立即人工干预

WARN

请求未被满意处理,需尽快(但非立即)人工介入。

实用判断标准
针对 ERROR/WARN,问自己:“需要采取什么行动?”
如果不是“天啊!必须立刻处理!”的事件,就降级为 WARN。

示例 1(ERROR)
上线新功能后,用户查看交易记录时触发 Hibernate LazyInitializationException

2018-09-11 08:48:36.480 ERROR 10512 --- [ost-startStop-1] com.marcobehler.UserService : Retrieving transaction list for user[id={}] failed

org.hibernate.LazyInitializationException: failed to lazily initialize a collection...

示例 2(WARN)
批处理作业导入 CSV 时,部分记录格式错误(需人工修复,但非紧急):

2018-09-11 00:00:36.480 WARN 10512 --- [ost-startStop-1] com.marcobehler.BatchJob : Could not import record[id=25] from csv file[name=transactions.csv] because of malformed[firstName,lastName]

核心原则:保持 ERROR/WARN 级别日志的“纯净”,便于监控和快速响应。

确保只在真正需要时才在凌晨 3 点叫醒运维人员。


INFO 级别

开发者最“舒适”的日志级别,常用于记录:

  • 客户端活动(Web 应用)
  • 进度信息(批处理作业)
  • 内部流程细节(但这类应归入 DEBUG)

历史原因:过去动态调整日志级别困难(需重启应用),且开发与运维协作不畅,导致开发者倾向于过度使用 INFO。

INFO 的两种典型用途

  1. 应用状态

    2018-09-11 08:46:26.547 INFO 8844 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http)
    
  2. 弱错误场景
    请求未成功处理,但已向请求方返回错误详情,无需主动支持
    例如用户登录失败:

    2018-09-11 08:46:26.547 INFO 8844 --- [main] com.marcobehler.UserService : User with id[=45] tried to login with wrong username/password
    

    运维可通过日志快速定位用户问题(即使前端已提示错误)。


DEBUG 与 TRACE

关于是否需要 TRACE 级别曾引发激烈争论,SLF4J 在后期版本才加入该级别(因社区强烈要求)。

  • DEBUG:内部流程的高级细节,仅在排查特定问题时开启,事后关闭。

    2018-08-01 05:05:00,031 DEBUG - Checking uploaded XML files for valid structure [...]
    2018-08-01 05:06:00,031 DEBUG - Masking inputs for XML file[id=5] [...]
    
  • TRACE:比 DEBUG 更详细,或专用于特定环境(如 DEV/TEST)。
    开发者可在这些环境中自由记录,而生产环境始终禁用 TRACE。
    典型用例:Spring 框架的事务边界日志:

    2018-08-01 05:05:00,031 TRACE - Getting transaction for [com.marcobehler.BitcoinApp.mine]
    ...
    2018-08-01 05:05:00,142 TRACE - Completing transaction for [com.marcobehler.BitcoinApp.mine]
    

日志文件管理

常见做法是按用途分离日志文件,即一个应用输出多个日志文件:

  • error.log:仅含 ERROR/WARN,供监控告警系统和运维使用
    (文件名模式:<appname>.<instance-name>.YYYYMMDD.ZZZ.error.log

  • status.log(或 info.log):记录应用进度、用户活动等
    (文件名模式:<appname>.<instance-name>.YYYYMMDD.ZZZ.status.log

  • trace.log:调试时启用的详细日志

日志合并工具
提供命令行工具(如 log-merger 或 Bash 脚本),可按时间戳动态合并多个日志文件。

示例
合并前:

# error.log
2015-08-29 15:49:46,641 ERROR [org.jboss.msc.service.fail] ...

# status.log
2015-08-29 15:49:46,033 INFO [org.xnio] ...

合并后:

[1] 2015-08-29 15:49:46,033 INFO [org.xnio] ...
[0] 2015-08-29 15:49:46,641 ERROR [org.jboss.msc.service.fail] ...

重要提醒
开发者常误以为日志的时间/位置关联性代表因果关系。
反例:应用启动卡在 Hibernate 初始化后无日志,未必是 Hibernate 问题——可能是后续组件尚未执行到日志输出阶段。

日志中的时间关联 ≠ 逻辑因果,切勿草率下结论!


MDC(映射诊断上下文)

在分布式系统(微服务)中,MDC(Mapped Diagnostic Context)是强关联日志的关键。

场景:用户请求经过多个微服务,如何关联所有相关日志?
方案:为每个请求生成唯一 ID,并自动附加到所有日志中。

实现步骤

  1. 在 HTTP 过滤器中设置 MDC:

    MDC.put("requestId", "lknwelqk-12093alks-123nlkasn-5t234-lnakmwen");
    
  2. 正常记录日志:

    logger.info("Hi, my name is: Slim Shady!");
    
  3. 配置日志框架输出 MDC(配置示例),得到:

    [lknwelqk-12093alks-123nlkasn-5t234-lnakmwen] - Hi, my name is: Slim Shady!
    

通过搜索 requestId,即可跨服务追踪完整请求链路。


敏感信息处理

绝对禁止记录敏感信息:密码、信用卡号、用户隐私等。

通用解决方案(避免逐条修复日志):

  • Log4j2:编写自定义 LogEventPatternConverter 自动脱敏
    (参考:Log4j2 脱敏指南

主动帮助(Proactive Help)

日志内容不仅要有错误信息,最好提供解决方案建议

经典案例:Spring Boot
旧版端口冲突日志:

2018-09-11 09:35:57.062 ERROR ... Failed to start connector [Connector[HTTP/1.1-8080]]
Caused by: Protocol handler start failed

新版改进:

***************************
APPLICATION FAILED TO START
***************************

Description:
Embedded servlet container failed to start. Port 8080 was already in use.

Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

核心思想:在日志中提供可操作的修复建议,大幅降低故障排查成本。
(其他范例:Apache Wicket 框架的错误建议机制)


遗留应用迁移

将现有系统迁移到统一日志策略需循序渐进:

  1. 初期可将所有日志写入同一文件
  2. 随代码迭代逐步按新规范调整日志级别和内容

(完整迁移指南超出本文范围)


集中式日志

当应用实例增多(尤其是微服务架构),需考虑集中式日志方案:

  • 简易方案:运维脚本收集各实例日志到共享目录
  • 专业方案:Graylog、Splunk、ELK 等日志平台

以 Graylog 为例

  • 各主流日志框架均有 GELF Appender
  • 直接配置 Log4j2 将日志发送至 Graylog
  • 通过 Web UI 统一查询(需学习其搜索语法)

关键成功因素

确保开发和运维团队不仅会写入日志,更要熟练使用日志平台的查询功能(尤其在多实例混合场景下)。


总结:如何选择“正确”的日志方式

回顾全文,我们探讨了多种日志库和实践方法。最后总结如下(记住:没有唯一正确答案):

  • 新项目:首选 SLF4J + LogbackLog4j2,二者皆可
  • 遗留项目:逐步迁移到 SLF4J 门面,统一团队日志规范
  • 日志实践:正确使用级别、优化消息内容、建立监控机制——这需要时间,保持耐心
  • 大型系统:评估集中式日志方案(Graylog/Splunk/ELK)

最重要的是:享受日志记录的过程!

今日分享到此结束。