概述
日志记录是应用程序运行中必不可少的一部分。具有良好格式和完备信息的日志,可以在程序出现问题时帮助开发人员迅速地定位错误的根源。日志所能提供的功能是多种多样的,包括记录程序运行时产生的错误信息、状态信息、调试信息和执行时间信息等。
System.out.println
、System.err.println
及异常对象的printStrackTrace
方法等,功能有限且混乱,故而需要日志框架。直到JDK1.4才引入java.util.logging
包,JUL。
日志框架主要分两类:
- 真正的日志记录实现,如:log4j、logback;
- 日志记录相关的封装框架,如:Apache Commons Logging和SLF4J,在日志记录实现的基础上提供一个封装的API层次,对日志记录API的使用者提供一个统一的接口,使得可以自由切换不同的日志记录实现。
注:本文使用的Spring Boot版本为3.2.4。
日志级别Level
JDK的日志API,即java.util.logging.Logging
,定义的级别,即java.util.logging.Level
,包括OFF、SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST和ALL等。
OFF为日志最高等级,ALL为最低等级。每条日志必须对应一个级别,级别主要用来对日志的严重程度进行分类,同时可用于控制日志是否输出。
Log4j使用的级别则包括:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE和ALL等。用得较多的是:
- FATAL:导致程序提前结束的严重错误
- ERROR:运行时异常及预期之外的错误
- WARN:预期之外的运行时状况,不一定是错误
- INFO:运行时产生的事件
- DEBUG:与程序运行时的流程相关的详细信息
- TRACE:更加具体的详细信息
更进一步,以ERROR、WARN、INFO和DEBUG最为常用。
框架
Java Util Logging
即JUL,自JDK1.4版本引入,故而被称为JDK14Logger。
提供抽象基类Handler,和一系列实现类,如:ConsoleHandler、StreamHandler等。
提供抽象基类Formatter和2个实现类:
总结:JUL是JDK标准库一部分,无需额外的配置或依赖;使用logging.properties
进行配置,相对复杂;扩展性不够;性能不好。
基本上没有多少应用在使用。
JULI
Java Util Logging Implementation,有些项目里可能会看到tomcat-juli
xml
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-juli</artifactId>
<version>10.1.28</version>
</dependency>
主要用于标准Tomcat服务器环境,代替JUL,灵活性更好,功能更丰富,如独立的日志配置文件、按Web应用程序的隔离日志记录等。
作为Tomcat服务器中重要的日志组件,仍在维护和更新,满足用户的需求。
如果是Spring Boot内嵌Tomcat应用,则会看到:
xml
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-logging-juli</artifactId>
<version>8.5.2</version>
</dependency>
嵌入式Tomcat使用场景中,开发者倾向于使用更现代的日志框架(如后文即将介绍的Logback和Log4j来代替JUL),因此官方停止维护。
从pom文件看,两者没有什么关系,独立维护的两个项目,拥有不同的GAV;但是通过JD-GUI等工具初步分析,JAR包里的类(类名和数量)一模一样,不过源码包括内部类还是有很多差别的。
Jakarta Commons Logging
Jakarta Commons Logging,即JCL,是一个抽象层(适配器)日志框架,旨在提供对多个日志框架的统一访问接口。JCL在运行时动态查找和绑定日志实现,这使得其在不同的环境下可以自动选择合适的日志实现。
Apache Commons Logging
即Apache Commons Logging,前身是Jakarta Commons Logging。
Maven依赖如下:
xml
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</dependency>
动态查找原理,Log是一个接口声明。LogFactory的内部会去装载具体的日志系统,并获得实现该Log接口的实现类。流程如下:
- 首先寻找
org.apache.commons.logging.LogFactory
属性配置 - 否则,利用JDK1.3开始提供的服务发现机制SPI,会扫描classpath下的
META-INF/services/org.apache.commons.logging.LogFactory
文件,若找到则装载里面的配置,使用里面的配置 - 否则,从classpath里寻找
commons-logging.properties
,找到则根据里面的配置加载 - 否则,使用默认配置:如果能找到Log4j则使用Log4j实现,如果没有则使用JDK14Logger实现,再没有则使用commons-logging内部提供的SimpleLog实现。
因此,只要引入Log4j并在classpath配置log4j.xml
,则commons-logging
就会使用Log4j,而Java代码里无需添加任何Log4j代码。
存在的问题:动态绑定机制可能导致一些难以调试的配置问题,如在某些环境下可能绑定到意外的日志实现。
SLF4J
Simple Logging Facade for Java,SLF4J,Java简单日志门面,类似于JCL。为不同的日志框架提供简单的门面或抽象的实现,允许最终用户在部署时能够接入自己想要使用的日志框架。
使用SLF4J时,需要使用某一种日志实现,必须选择正确的SLF4J的JAR包的集合,即各种桥接包,这就是SLF4J的静态绑定(bindings):
如上图,SLF4J(和其他日志框架)提供的binding如下:
- logback-classic:因为Logback晚于SLF4J诞生,故一开始SLF4J没有提供Logback的实现类,由Logback提供,实现
org.slf4j.spi.SLF4JServiceProvider
。 - slf4j-logj12:SLF4J提供,下同。
- slf4j-jdk14:使用JUL打印
- slf4j-simple:使用SLF4J自带
- slf4j-nop:不打印日志
- slf4j-jcl:?
SLF4J静态绑定原理:SLF4J会在编译时查找org.slf4j.spi.LoggerFactoryBinder
(2.0.0版本后,被org.slf4j.spi.SLF4JServiceProvider
)的实现类,如slf4j-log4j12
的实现类org.slf4j.impl.StaticLoggerBinder
,该类里面实现对具体日志方案的绑定接入。任何一种基于SLF4J的实现都要有一个这个类。如果有任意两个实现SLF4J的包同时出现,可能会出现问题。
Bridging,桥接是指将某个特定的日志库的日志请求重定向到SLF4J,使得所有的日志调用最终都通过SLF4J处理。这对于希望将整个应用程序统一到一个日志框架下非常有用。
SLF4J对比Commons Logging
Commons Logging通过动态查找的机制,在程序运行时自动找出真正使用的日志库。使用ClassLoader寻找和载入底层的日志库,导致像OSGI这样的框架无法正常工作,因为OSGI的不同的插件使用自己的ClassLoader。OSGI的这种机制保证插件互相独立。
SLF4J在编译时静态绑定真正的Log库,可以在OSGI中使用。SLF4J支持参数化的log字符串,避免之前为了减少字符串拼接的性能损耗而不得不写的if(logger.isDebugEnable())
,现在你可以直接写:logger.debug("current user is: {}", user)
。拼装消息被推迟到它能够确定是不是要显示这条消息的时候,但是获取参数的代价并没有幸免。
其他
MDC
Marker
Migrator:为了方便从别的日志框架迁移到SLF4J,提供Migrator工具。具体原理,可参考GitHub项目slf4j-migrator
目录。
Log4j
Apache的一个开放源代码项目,通过使用Log4j,可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、Unix Syslog守护进程等;也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,能够更加细致地控制日志的生成过程。这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
Log4j由三个重要的组成构成:
- Loggers:日志记录器,控制要输出哪些日志记录语句,对日志信息进行级别限制
- Appenders:输出端,指定日志将打印到控制台还是文件中
- Layout:日志格式化器,控制日志信息的显示格式
常用的appender:
- ConsoleAppender:控制台
- FileAppender:文件
- DailyRollingFileAppender:每天产生一个日志文件
- RollingFileAppender:文件大小到达指定尺寸时产生新文件
- WriterAppender:将日志信息以流格式发送到任意指定的地方
常用的layout:
- HTMLLayout:以HTML表格形式布局
- PatternLayout:可以灵活地指定布局模式
- SimpleLayout:包含日志信息的级别和信息字符串
- TTCCLayout:包含日志产生的时间、线程、类别等信息
Log4j的早期GAV如下:
xml
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
这就是Log4j1,被废弃,不建议使用。新的GAV如下:
xml
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
值得一提的是,log4j-core
的第一个正式版为2.0,这就是Log4j2。
spring-boot-starter-log4j
的最后一个版本:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>
依赖于上面的log4j,此artifact已废弃:
使用如下GAV:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
默认引入:
xml
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<!-- Spring Boot 3.2.4 版本 -->
<version>2.21.1</version>
<!-- scope = compile,下文不再赘述 -->
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
Logback
由Log4j创始人设计的又一个开源日记组件,作为Log4j的替代者,性能表现比Log4j优异。
xml
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
对于Spring Boot应用,引入spring-boot-starter-logging
依赖即可:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
默认引入如下依赖:
xml
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.21.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>2.0.12</version>
</dependency>
Spring Boot将使用Logback作为日志框架,无需新增logback.xml
,开箱即用,这也是Spring Boot的方便之处。当然为了方便收集日志和统一维护,一般都会定义logback.xml
。
性能对比
Logback在设计上优于Log4j,但和下面将出场的Log4j2,孰优孰劣,请参考官网benchmark。
Log4j2
Logback在2017年3月31日发布1.2.3版本后,在很长一段时间内几乎处于停滞状态,这也使得在Maven上看到这个版本的Usages高达1w多。
4年多后,2021年7月19日终于发布1.2.4版本。
事实上,Logback自身也确实存在一些问题:
- 配置繁琐
- 功能简陋
- 异步性能不高
因此,有不少开发者将目光投向Log4j2。除内部设计的调整外,有以下几点大升级:
- 更简化的配置
- 更强大的参数格式化
- 夸张的异步性能
Log4j2中,分为API(log4j-api)和实现(log4j-core)两个模块,log4j-core
包含log4j-api
。API和SLF4J类似,属于日志抽象/门面;而实现才是Log4j2的核心:
- org.apache.logging.log4j >> log4j-api
- org.apache.logging.log4j >> log4j-core
从2016年5月25日发布的2.6版本开始,Log4j2默认就以零GC模式运行。即不会由于Log4j2而导致GC。Log4j2中各种Message对象,字符串数组,字节数组等全部复用,不重复创建,大大减少无用对象的创建,从而做到零GC。
Log4j2提供MemoryMappedFileAppender,使用MemoryMappedFile来实现,可以得到极高的I/O性能。
Log4j2支持XML/JSON/YML/Properties四种格式的配置文件,最主流的还是XML。
log4j-api
和SLF4J相比,提供更丰富的参数格式化功能。Log4j2除了支持{}
形式的参数占位符,还支持String.format
形式:
java
private static final Logger logger1 = LogManager.getLogger(Test.class);
logger1.info("current time {}", new Date());
// getFormatterLogger方法才能使用String.format打印
private static final Logger logger = LogManager.getFormatterLogger(Test.class);
logger.info("current time %s", new Date());
惰性打印:Log4j2的Logger,提供一系列lambda支持,通过这些方法可实现惰性打日志。
与其他日志抽象/门面适配
Benchmark
参考Log4j2官网。
原理
classpath下新增配置文件如log4j2.xml
,配置好Appenders和Loggers。
一个应用中可能存在多个有效的LoggerContext。每一个LoggerContext都有一个有效的Configuaration,它包含所有的Appenders、context-wide Filters、LoggerConfigs以及对StrSubstitutor的引用。在重新配置期间,两个Configuaration会同时存在;一旦日志器被重新赋予新的Configuaration,旧的Configuaration就会停止工作并丢弃。
LoggerConfig将会在Loggers在logging configuration中被声明时创建。在LoggerConfig拥有一列类的过滤器,这些过滤器将会过来所有的记录日志的事件,只有符合要求的日志才会被传递到Appenders。LoggerConfig需要将事件传递给Appenders,所以它拥有一系列Appenders的引用。
根据Logger请求选择去接受或者拒绝该只是他们的一个能力。Log4j2允许日志打印服务打印到多个目的地上,即Appdender。Appender可以是:Console、Async、File、JDBC、Cassandra、Failover、Flume、JMS、JPA、Http、Kafka、MemoryMappedFile、NoSQL、OutputStream、RandomAccessFile等。
体系
logback-classic依赖于logback-core
commons-logging.commons-logging-api
已废弃,使用commons-logging.commons-logging
log4j-over-slf4j
log4j-core依赖于log4j-api
log4j-slf4j2-impl
其他可能在开发中看到的日志框架,如jboss-logging
,类似于JCL:
xml
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.6.0.Final</version>
</dependency>
TODO。
实战
Log4j
一般都是配合SLF4J使用:
- 仅供参考的
log4j.properties
文件
yml
log4j.rootCategory=INFO,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %X{RequestId} - %m%n
log4j.appender.RollingFile.File=logs/app.log
log4j.appender.RollingFile.layout=org.apache.log4j.PatternLayout
log4j.logger.org.springframework=warn # Spring日志记录到warn级别
- Logger调用。在每一个要产生日志的类添加如下代码:
java
private static final Logger LOGGER = Logger.getLogger(*.class);
Logback
另起一篇,参考Logback实战使用笔记。
Lombok
上面介绍过,在使用log4j时,每个类都需要定义一个Logger,还是挺麻烦的。借助于Lombok的注解@Slf4j,省去冗余定义。
问题
程序包org.slf4j不存在
使用Lombok的@Slf4j注解,报错如上。
排查思路:借助于Maven Helper或mvn dependency:tree
命令分析是否添加slf4j-api这个JAR包。如果是多Maven module项目,则需要判断一下Maven dependencyManagement使用是否正确。
ClassNotFoundException: org.apache.logging.log4j.util.Lazy
报错如上。
排查:org.apache.logging.log4j.util.Lazy
位于org.apache.logging.log4j:log4j-api
这个JAR包里,而log4j-core
而默认引入log4j-api
。ClassNotFoundException这个异常一般都是类冲突,即多个JAR包引入相同的类。通过Maven Helper分析,发现当前Maven module项目里还引入一个org.apache.logging.log4j:log4j-slf4j2-impl
。经过试错,排除掉后面这个依赖,即log4j-slf4j2-impl
,可解决问题。