Java 日志框架的使用
日志系统的几个概念
Logger
Logger负责生成日志。用户代码中需要生成日志的地方,调用Logger的API来产生日志。但是最终日志输出到哪里不归Logger负责,而是由Appender决定。
Logger具有层级结构。最高层的logger叫做root。Logger的name为其所在class的全路径名(<包名>.<class名>)。Logger层级结构的判定和class的全路径名紧密相关。例如com.example.partA.xxx
的logger是com.example.partA
这个logger的下层logger。它们的层级关系如下所示(从上到下为高级到低级):
root
com
com.example
com.example.partA
com.example.partA.xxx
Logger按照name来区分。使用getLogger
方法多次获取name相同的logger,实际上获取到的是同一个对象。无论是log4j-api和slf4j都是如此。可用下面的代码验证:
// log4j-api
Logger log1 = LogManager.getLogger(Main.class);
Logger log2 = LogManager.getLogger(Main.class);
Logger logA = LogManager.getLogger(A.class);
System.out.println(log1 == log2);
System.out.println(log1 == logA);
// slf4j-api
org.slf4j.Logger logger1 = LoggerFactory.getLogger(Main.class);
org.slf4j.Logger logger2 = LoggerFactory.getLogger(Main.class);
org.slf4j.Logger loggerA = LoggerFactory.getLogger(A.class);
System.out.println(logger1 == logger2);
System.out.println(logger1 == loggerA);
运行的输出为:
true
false
true
false
那么问题来了,logger的层级有什么作用?在日志系统中,每个logger都对应有自己的配置(日志级别过滤和appender)。为每一个logger分别做配置是件很麻烦的事。因此常用的方式是对某一个层级的logger做统一的配置。下层的logger如果没有专门的配置,会自动继承它上层logger的配置。
Appender
Appender用来控制日志输出的目的地。比如输出到console(控制台),文件,滚动文件(按照大小或者时间自动切分文件),甚至是Kafka等。需要注意的是,不同的日志框架支持的appender种类和功能都不相同,配置方式也都不同,需要根据实际使用情况专门配置。
Level日志级别
通常日志有如下几个级别:
- ALL
- DEBUG
- INFO
- WARN
- ERROR
- OFF
在输出日志的时候可以按照级别过滤日志内容,减少不关心内容的输出。列表中ALL为输出所有级别日志,OFF为不输出任何日志。按照上面中列表的顺序,如果配置日志过滤级别为某个级别,则该级别及其后续级别的日志都会被打印。例如配置级别INFO,则INFO,WARN和ERROR级别信息都会被输出。
常见日志框架
log4j
这里的log4j特指log4j 1.x。log4j即Log for Java。1.x版本在新项目中不推荐使用。
log4j 1.x有一个包叫做log4j-1.2-api
,作用是适配log4j 1.x版本接口到新的log4j2。
reload4j
Reload4j是log4j 1.x的作者从log4j 1.2.17版本拉出来的分支,旨在修复log4j 1.x中存在的安全问题。可以无缝替换log4j 1.x(即直接替换项目中的log4j.jar为reload4j.jar)。作者单独拉一个分支而不直接发布log4j 1.x新版本的原因是log4j 1.x在Apache社区已经EOL(End of Life),不会再发布升级版本。
新项目如果考虑使用log4j,建议使用较新的log4j2。
logback
logback是log4j 1.x的大幅增强版本。
logback特性介绍:https://logback.qos.ch/reasonsToSwitch.html
另外,logback本地实现了slf4j API,这意味着使用logback作为slf4j的binding/provider的开销最小。新项目可考虑使用logback。
log4j2
log4j2是log4j 1.x的性能增强和配置简化版,是目前性能最强的日志框架。新项目中推荐使用log4j2。
log4j2增强特性一览:https://juejin.cn/post/6966060925803724836
需要注意的是log4j2的包名已变化,为org.apache.logging.log4j
,而log4j 1.x则是org.apache.log4j
。
注意:log4j2 早期版本存在著名的远程代码执行漏洞(CVE-2021-44832)。为了保证安全,项目中一定要注意使用的版本。该漏洞在如下版本中修复:Log4j 2.17.1 (Java 8), 2.12.4 (Java 7) and 2.3.2 (Java 6)。需要根据项目使用的JDK版本,选择使用已修复的log4j2版本。
参见:
- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core。
- https://logging.apache.org/log4j/2.x/security.html
和log4j 1.x不同的是,log4j2把接口和实现分离开了,分别为log4j-api
和log4j-core
。其中log4j-api
作用和下面即将讨论的slf4j一样,是一种日志门面(应用代码和日志框架的兼容层)。和slf4j不同的地方是log4j-api
只能对接自己的实现log4j-core
,不能使用其他的日志框架。
联系上面提到的logback。社区目前形成了2套体系(官方推荐的日志门面和日志实现的组合):
- slf4j-api logback
- log4j-api log4j-core
log4j-api只能对接log4j-core,而slf4j-api则兼容log4j1和2,reload4j,logback和java.util.logging等,明显适用范围更广。因此建议项目中使用slf4j-api而不是log4j-api。当然如果为了更好的性能(理论上),也可以选择log4j-api + log4j-core体系。
java util logging
它是JDK自带的日志框架。和log4j非常相似,但Java util logging的迭代速度较慢,不容易升级(只随着JDK发布)。因此,log4j等其他日志框架的功能和灵活性远强于java util logging,且版本的迭代速度和漏洞bug的响应速度也快于它。因此不建议在项目中使用java util logging。
java util logging和log4j的详细对比参见:http://www.blogjava.net/lhulcn618/articles/16996.html
日志门面
上面提到了多种日志框架,它们的API互不相同。这带来了问题:我们很难将一个项目从某日志框架迁移到另一个日志框架。另外如果我们的项目引用了其他多个模块,这些模块如果使用的日志框架各不相同的话,就需要维护多套日志框架。复杂度和维护量完全不可控。为了解决这个问题,引入了日志门面。将日志的实现和接口分离开。这样项目可以在不改写代码的前提下更换日志实现框架,应用代码和日志框架之间彻底解耦,提高了项目的可维护性。
因此,强烈建议项目不要直接使用某个具体的日志框架API,统一使用日志门面。
Apache commons logging
Apache较早的一个日志接口。内部有一个很简单的日志实现。当然commons logging在更多情况下是配合第三方的日志实现来使用。
log4j-api
如前面所说log4j2将API和实现部分分离开。这样log4j-api就相当于是日志门面了。但是log4j-api只能和log4j2的实现配合使用,无法和其他日志框架结合。如果使用log4j2可考虑在新项目中使用log4j-api。如果项目以后可能更换日志框架,或者和其他项目结合使用,建议使用下文中提到的slf4j而不是log4j-api。
slf4j
slf4j是目前最流行的日志门面。编写代码的时候仅使用slf4j提供的接口。在运行的时候,classpath放入slf4j的binding/provider就可以工作。Binding/provider为slf4j的日志实现,可以为上面提到的Log4j,logback等。slf4j具有最好的日志框架兼容性,推荐在新项目中使用。
slf4j官网手册:https://www.slf4j.org/manual.html
二进制兼容性
不要混用不同版本的slf4j-api和slf4j的log binding。可能会造成未知的问题。运行的时候slf4j会给出警告。
不同版本的slf4j-api是相互兼容的。slf4j-api的版本可以放心更换。
参考链接:https://www.slf4j.org/manual.html#compatibility
log binding/provider
Binding或者是provider为slf4j的具体日志实现。官网介绍可参考:
https://www.slf4j.org/manual.html#swapping
主要包含如下binding/provider:
- slf4j-log4j12:log4j 1.2的binding。
- log4j-slf4j-impl:log4j2的binding。
- slf4j-reload4j:reload4j的binding。
- logback-classic:logback的binding。
- slf4j-jdk14:java.util.logging的binding。
- slf4j-jcl:Apache Common Logging的binding。
- slf4j-nop:一个不打印任何日志信息的binding。
- slf4j-simple:打印INFO以上级别信息,输出所有时间到System.err的binding。适合小型程序使用。
桥接器
桥接器用于将项目中正在使用的非slf4j的接口转换为slf4j形式。
官网桥接器介绍:https://www.slf4j.org/legacy.html
官网的图很清晰的指出了桥接器的使用情形:
[图片上传失败...(image-effed1-1686817217906)]
较为常用的集中桥接场景:
- log4j -> log4j2:引入log4j-1.2-api,log4j-api和log4j-core。不需要中间转换为slf4j。
- log4j -> slf4j:引入log4j-over-slf4j替换原log4j包。引入slf4j-api。
- log4j2 -> slf4j:引入log4j-to-slf4j和slf4j-api。
- Apache commons logging -> slf4j:引入jcl-over-slf4j和slf4j-api。
- Java util logging -> slf4j:引入jul-to-slf4j。
slf4j + log4j2 使用和配置
项目依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.18.0</version>
</dependency>
slf4j使用
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
public class Main {
public static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
logger.info("haha");
}
}
日志中经常涉及到字符串中带有变量的情况。不建议在打印日志时使用字符串拼接。因为这样会生成大量的String对象,占据过多的字符串常量池空间。
建议的做法是使用参数化消息(占位符)。示例代码如下:
String hostname = "manager";
int port = 22;
logger.info("Hostname: {}. Port: {}", hostname, port);
除了上面的情形。还会遇到如下的情况:打印的日志中包含一些需要准备的信息。这些信息的准备过程比较耗时。我们需要做出优化:如果该级别的日志被过滤掉不需要输出,那么这些信息也就没有必要再准备。这种情形可以使用logger
的isXxxEnabled
判断完成。isXxxEnabled
针对每一个日志级别都对应一个判断方法。
举个例子,debug级别的日志需要打印CPU占用率。获取CPU占用率这个过程较为影响系统性能。因此只需要在开启debug级别日志的时候,才有必要获取CPU占用率。代码如下所示:
if(logger.isDebugEnabled() {
// 获取CPU占用率
String cpuUsage = ...
logger.debug("CPU usage: {}", cpuUsage);
}
参见:https://slf4j.org/faq.html#logging_performance
由于日志实现使用了log4j2,因此项目中日志的配置使用log4j2的配置方式。参见下一节。
log4j2使用
需要引入的依赖为对应版本的log4j-api
和log4j-core
。使用方法和slf4j基本是相同的,代码如下:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
public static final Logger logger = LogManager.getLogger(Main.class);
public static void main(String[] args) {
logger.info("haha");
}
}
除此之外需要在classpath放入log4j2的配置文件(log4j2.xml
)。Maven项目对应着resources目录。
一个简单版的log4j2.xml
配置文件内容示例如下。仅仅配置了console appender和root logger。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
一个复杂版的例子。配置了console,file和rolling file appender以及多个logger:
<?xml version="1.0" encoding="UTF-8"?>
<!-- status:log4j2框架自身的日志输出级别 -->
<!-- monitorInterval:log4j支持配置文件热更新。该参数为多长时间检测一次配置文件是否发生变更 -->
<configuration status="WARN" monitorInterval="30">
<!-- 定义appender -->
<appenders>
<!-- console appender是输出日志到控制台 -->
<Console name="Console" target="SYSTEM_OUT">
<!-- pattern为输出日志的格式 -->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
</Console>
<!-- file appender输出日志到文件。append属性决定每次运行时日志文件是清空还是追加 -->
<File name="log" fileName="log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>
<!-- rolling file appender是基于文件的滚动日志。支持按照配置的条件触发日志滚动,生成新的日志文件 -->
<RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd HH}-%i.log">
<!-- 日志级别过滤 -->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<!-- 如果同时使用多个触发策略,这些策略间是或的关系 -->
<!-- 基于时间触发的滚动策略,interval的单位为filePattern中的最小时间单位 -->
<TimeBasedTriggeringPolicy interval="1"/>
<!-- 基于文件大小触发的滚动策略 -->
<SizeBasedTriggeringPolicy size="100 MB"/>
<!-- 结合filePattern中的%i(整数计数器)使用,最多保留max个归档的日志文件 -->
<DefaultRolloverStrategy max="20"/>
</Policies>
</RollingFile>
</appenders>
<!-- 定义logger,将logger和appender绑定 -->
<loggers>
<!-- 使用name描述logger,指定logger的日志输出级别 -->
<logger name="org.springframework" level="INFO"></logger>
<logger name="com.xxx" level="DEBUG">
<!-- 绑定appender到logger。ref指向appender的name -->
<AppenderRef ref="Console"/>
</logger>
<!-- 定义root logger -->
<root level="all">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFileInfo"/>
</root>
</loggers>
</configuration>
参考材料:
log4j2使用:https://logging.apache.org/log4j/2.x/manual/usage.html
log4j2支持的appender:https://logging.apache.org/log4j/2.x/manual/appenders.html
日志输出格式PatternLayout:https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout
Log4j2中RollingFile的文件滚动更新机制:https://www.cnblogs.com/yeyang/p/7944899.html
log4j2配置文件:https://www.cnblogs.com/bestlmc/p/12012875.html
问题和解答
log4j2使用出现java.lang.NoClassDefFoundError: org/apache/logging/log4j/util/StacklocatorUtil
项目classpath中存在版本较老的log4j-api导致。org/apache/logging/log4j/util/StacklocatorUtil
类在项目代码中第一次出现是2017年3月27日(commit-id: 34552d7d725c3b7547e1c19f6ce803b83c60bd94)。需要删除项目中所有版本号在2.8.x之前的log4j-api,重新引入2.9.x或者更新的log4j-api。
打印stacktrace到日志
try {
Class.forName("com.doesnotexist.A");
} catch (ClassNotFoundException e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
logger.info(sw.toString());
}
注意:这里的PrintWriter
和StringWriter
不需要close。PrintWriter
close的时候实际上关闭的是内部的StringWriter
,而StringWriter
close的时候什么也不做。详细情况读者可以查看它们的源代码。