• 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.tomcatgroupId>
    	<artifactId>tomcat-juliartifactId>
    	<version>10.1.28version>
    dependency>
    

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

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

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

    <dependency>
    	<groupId>org.apache.tomcat.embedgroupId>
    	<artifactId>tomcat-embed-logging-juliartifactId>
    	<version>8.5.2version>
    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-logginggroupId>
    	<artifactId>commons-loggingartifactId>
    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

    由Ceki Gülcü首创的,后捐献给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>log4jgroupId>
    	<artifactId>log4jartifactId>
    	<version>1.2.17version>
    dependency>
    

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

    <dependency>
    	<groupId>org.apache.logging.log4jgroupId>
    	<artifactId>log4j-coreartifactId>
    dependency>
    

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

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

    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-log4jartifactId>
    	<version>1.3.8.RELEASEversion>
    dependency>
    

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

    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-log4j2artifactId>
    dependency>
    

    默认引入:

    <dependency>
    	<groupId>org.apache.logging.log4jgroupId>
    	<artifactId>log4j-slf4j2-implartifactId>
    	
    	<version>2.21.1version>
    	
    	<scope>compilescope>
    dependency>
    <dependency>
    	<groupId>org.apache.logging.log4jgroupId>
    	<artifactId>log4j-coreartifactId>
    dependency>
    <dependency>
    	<groupId>org.apache.logging.log4jgroupId>
    	<artifactId>log4j-julartifactId>
    dependency>
    

    Logback

    由Log4j创始人设计的又一个开源日记组件,作为Log4j的替代者,性能表现比Log4j优异。

    <dependency>
    	<groupId>ch.qos.logbackgroupId>
    	<artifactId>logback-coreartifactId>
    dependency>
    

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

    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-loggingartifactId>
    dependency>
    

    默认引入如下依赖:

    <dependency>
    	<groupId>ch.qos.logbackgroupId>
    	<artifactId>logback-classicartifactId>
    	<version>1.4.14version>
    dependency>
    <dependency>
    	<groupId>org.apache.logging.log4jgroupId>
    	<artifactId>log4j-to-slf4jartifactId>
    	<version>2.21.1version>
    dependency>
    <dependency>
    	<groupId>org.slf4jgroupId>
    	<artifactId>jul-to-slf4jartifactId>
    	<version>2.0.12version>
    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,Log4j的下一个版本,相比Log4j 1发生很大变化,Log4j2不兼容Log4j 1。

    除内部设计的调整外,有以下几点大升级:

    • 更简化的配置
    • 更强大的参数格式化
    • 夸张的异步性能

    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.logginggroupId>
    	<artifactId>jboss-loggingartifactId>
    	<version>3.6.0.Finalversion>
    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,可解决问题。

    参考

  • 相关阅读:
    Docker----harbor服务
    Linux 服务器使用过程中,mysql突然不能用了,报如下错误
    主定理(简化版)
    【JAVA】线程不安全问题以及相关解决方案
    CENTOS下的常用命令(一)
    无涯教程-JavaScript - BETA.INV函数
    el-table实现局部加载(只有body体加载,表头不加载)
    Java常见面试题
    C#创建Windows Service(Windows 服务)基础教程
    Qt/QML编程之路:openglwidget和倒车影像的切换(43)
  • 原文地址:https://blog.csdn.net/lonelymanontheway/article/details/89425815