Java日志体系框架总结:JUL、JCL、SLF4J、Log4j、Logback、Log4j2

概述

日志记录是应用程序运行中必不可少的一部分。具有良好格式和完备信息的日志,可以在程序出现问题时帮助开发人员迅速地定位错误的根源。日志所能提供的功能是多种多样的,包括记录程序运行时产生的错误信息、状态信息、调试信息和执行时间信息等。

System.out.printlnSystem.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

<dependency>
	<groupId>org.apache.tomcat</groupId>
	<artifactId>tomcat-juli</artifactId>
	<version>10.1.28</version>
</dependency>

主要用于标准Tomcat服务器环境,代替JUL,灵活性更好,功能更丰富,如独立的日志配置文件、按Web应用程序的隔离日志记录等。

作为Tomcat服务器中重要的日志组件,仍在维护和更新,满足用户的需求。

如果是Spring Boot内嵌Tomcat应用,则会看到:

<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依赖如下:

<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

官网GitHub

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由三个重要的组成构成:

  1. Loggers:日志记录器,控制要输出哪些日志记录语句,对日志信息进行级别限制
  2. Appenders:输出端,指定日志将打印到控制台还是文件中
  3. Layout:日志格式化器,控制日志信息的显示格式

常用的appender:

  • ConsoleAppender:控制台
  • FileAppender:文件
  • DailyRollingFileAppender:每天产生一个日志文件
  • RollingFileAppender:文件大小到达指定尺寸时产生新文件
  • WriterAppender:将日志信息以流格式发送到任意指定的地方

常用的layout:

  • HTMLLayout:以HTML表格形式布局
  • PatternLayout:可以灵活地指定布局模式
  • SimpleLayout:包含日志信息的级别和信息字符串
  • TTCCLayout:包含日志产生的时间、线程、类别等信息

Log4j的早期GAV如下:

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

这就是Log4j1,被废弃,不建议使用。新的GAV如下:

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

值得一提的是,log4j-core的第一个正式版为2.0,这就是Log4j2。

spring-boot-starter-log4j的最后一个版本:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-log4j</artifactId>
	<version>1.3.8.RELEASE</version>
</dependency>

依赖于上面的log4j,此artifact已废弃:
在这里插入图片描述
使用如下GAV:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

默认引入:

<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优异。

<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-core</artifactId>
</dependency>

对于Spring Boot应用,引入spring-boot-starter-logging依赖即可:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-logging</artifactId>
</dependency>

默认引入如下依赖:

<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形式:

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:

<dependency>
	<groupId>org.jboss.logging</groupId>
	<artifactId>jboss-logging</artifactId>
	<version>3.6.0.Final</version>
</dependency>

TODO。

实战

Log4j

一般都是配合SLF4J使用:

  1. 仅供参考的log4j.properties文件
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级别
  1. Logger调用。在每一个要产生日志的类添加如下代码:
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,可解决问题。

参考

posted @ 2024-08-20 10:04  johnny233  阅读(37)  评论(0编辑  收藏  举报