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,然后就可以记录日志语句。你可能会觉得 severe 和 fine 这些日志级别名称很奇怪,但它们基本上对应现代日志库中的 error 和 debug 级别。
当然,你也可以通过所谓的 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 不仅拥有合理的日志级别名称(如 error 和 debug),还提供了大量巧妙的 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-slf4j、jcl-over-slf4j 等桥接库(注意:JUL 桥接机制略有不同,详见此处)。
实际应用场景
根据项目类型和第三方依赖,你的类路径中可能包含:
- SLF4J API
- SLF4J 实现(如 Logback 或 Log4j)
- 一个或多个桥接库(如
log4j-over-slf4j、jul-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/非 static、final/非 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 的两种典型用途:
应用状态:
2018-09-11 08:46:26.547 INFO 8844 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http)弱错误场景:
请求未成功处理,但已向请求方返回错误详情,无需主动支持。
例如用户登录失败: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,并自动附加到所有日志中。
实现步骤:
在 HTTP 过滤器中设置 MDC:
MDC.put("requestId", "lknwelqk-12093alks-123nlkasn-5t234-lnakmwen");正常记录日志:
logger.info("Hi, my name is: Slim Shady!");配置日志框架输出 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 框架的错误建议机制)
遗留应用迁移
将现有系统迁移到统一日志策略需循序渐进:
- 初期可将所有日志写入同一文件
- 随代码迭代逐步按新规范调整日志级别和内容
(完整迁移指南超出本文范围)
集中式日志
当应用实例增多(尤其是微服务架构),需考虑集中式日志方案:
- 简易方案:运维脚本收集各实例日志到共享目录
- 专业方案:Graylog、Splunk、ELK 等日志平台
以 Graylog 为例:
- 各主流日志框架均有 GELF Appender
- 直接配置 Log4j2 将日志发送至 Graylog
- 通过 Web UI 统一查询(需学习其搜索语法)
关键成功因素:
确保开发和运维团队不仅会写入日志,更要熟练使用日志平台的查询功能(尤其在多实例混合场景下)。
总结:如何选择“正确”的日志方式
回顾全文,我们探讨了多种日志库和实践方法。最后总结如下(记住:没有唯一正确答案):
- 新项目:首选 SLF4J + Logback 或 Log4j2,二者皆可
- 遗留项目:逐步迁移到 SLF4J 门面,统一团队日志规范
- 日志实践:正确使用级别、优化消息内容、建立监控机制——这需要时间,保持耐心
- 大型系统:评估集中式日志方案(Graylog/Splunk/ELK)
最重要的是:享受日志记录的过程!
今日分享到此结束。