一、引言二、日志概念三、日志框架的作用四、日志框架的发展历程4.1 早期阶段(1996年以前)4.2 Log4j的诞生(1996年)4.3 JUL的推出(2002年)4.4 JCL的推出(2002年)4.5 SLF4J和Logback的创建(2006年)4.6 Log4j 2的推出(2012年)五、主流日志框架5.1 日志门面5.2 日志实现六、日志框架的使用6.1 日志框架的集成6.2 日志框架的桥接七、JCL 门面框架示例7.1 JCL+JUL7.2 JCL+log4j 1.x7.3 JCL+SLF4J八、SLF4J 门面框架示例8.1 SLF4J+logback8.2 SLF4J+log4j28.3 SLF4J+reload4j8.4 SLF4J+JUL8.5 SLF4J+simple九、日志规约十、总结
一、引言
在Java开发中,日志记录是不可或缺的一部分。它不仅可以帮助开发者调试程序,还能监控应用程序的运行状态,及时发现和解决问题。本文将详细介绍Java日志框架的基本概念、主流框架以及使用方法,帮助读者更好地理解和应用日志框架。
二、日志概念
日志,简单来说就是记录。它用来记录程序运行时发生的事情,比如程序启动、执行某个操作、遇到问题等等。这些记录信息对于开发者来说非常重要,因为它们提供了程序运行时的详细情况和状态。
在电商网站上,日志可以记录用户的登录、浏览、购买行为,监控交易过程,及时发现异常交易;通过分析日志,还可以实现精准推送等功能。服务器日志则可以记录服务器的启动、运行、关闭状态,以及发生的各种错误,帮助管理员及时发现并解决问题。
三、日志框架的作用
在没有日志框架时,我们可能会使用 System.out.println 或 e.printStackTrace() 来打印程序的运行状态和错误信息。这种方式简单直接,但存在一些缺点,如灵活性差、性能问题、不易管理等。因此,我们需要引入功能强大的日志框架进行日志管理。
日志框架由日志门面和日志实现构成。日志门面提供了一套标准的日志记录接口,而具体的日志记录工作则由不同的日志框架来完成。这样做的好处是,可以在不修改代码的情况下,通过配置来切换不同的日志框架。
注意:虽然日志实现也可以单独使用,但实际开发中一般采用日志门面和日志实现的方式。
【图】日志门面和日志实现
四、日志框架的发展历程
Java日志框架的发展历程是一个不断演进和改进的过程,以下是按时间顺序对Java日志框架的主要发展历程的介绍:
4.1 早期阶段(1996年以前)
在Java发展的早期阶段,开发人员主要依赖System.out和System.err来记录日志。这些方法虽然简单,但存在许多局限性,如缺乏灵活性、无法配置不同的日志级别、不能轻松地将日志保存到文件等。
4.2 Log4j的诞生(1996年)
1996年,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。这个API最终发展成为一个十分受欢迎的Java日志软件包,即Log4j。
Log4j提供了灵活的日志级别配置,允许开发人员根据需要记录不同级别的日志。它还提供了多种日志输出方式,包括控制台、文件和数据库。
Log4j后来成为Apache基金会项目中的一员,并近乎成了Java社区的日志标准。
Log4j[1]在2015年宣布不在维护了。
reload4j[2] 是 Apache log4j版本1.2.17的一个分支,旨在解决log4j的安全问题。Reload4j是log4j版本1.2.17的直接替代品,所以想继续使用log4j 1.x的框架,推荐使用slf4j-reload4j进行替代。
4.3 JUL的推出(2002年)
2002年,随着Java 1.4的发布,Sun推出了自己的日志库JUL(Java Util Logging)。JUL基本模仿了Log4j的实现,但性能和可用性在JDK 1.5以后才有所提升。
由于Log4j比JUL更成熟且好用,所以Log4j在选择上占据了一定的优势。
4.4 JCL的推出(2002年)
2002年8月,Apache推出了Jakarta Commons Logging(JCL,一个简单的日志门面框架)。JCL旨在成为Java日志领域的标准抽象层,为各种日志框架提供统一的接口。
JCL支持运行时动态加载日志组件的实现,也就是说,在应用代码里只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。
然而,JCL在实际使用中存在一些问题,如效率低下、容易引发混乱以及在特定情况下引发内存泄露等。
JCL最初是由Apache软件基金会的Jakarta项目组开发的,当时它是Apache Jakarta项目的一部分。在2011年,Jakarta 项目组重新组织并成为Apache Commons项目。因此,JCL目前被称为Apache Commons Logging。
4.5 SLF4J和Logback的创建(2006年)
2006年,Log4j的主要贡献者Ceki Gülcü因不适应Apache的工作方式而离开,并创建了SLF4J[3](Simple Logging Facade for Java,一个简单的日志门面框架)和Logback[4]两个项目。
SLF4J
是一个日志门面接口,类似于JCL,但提供了更简单和优雅的日志记录方法。它允许开发人员在不修改代码的情况下切换不同的日志框架。
Logback
是SLF4J的一个实现,它提供了可靠、快速且灵活的日志记录功能。Logback在功能完整度和性能上超越了当时已有的日志标准库。
4.6 Log4j 2的推出(2012年)
2014年,Apache推出了Log4j 2[5],这是一个全新的日志框架,不兼容Log4j 1.x。
Log4j 2全面借鉴了SLF4J和Logback的设计,并提供了更丰富的功能和配置选项。它支持异步日志打印、多种输出方式、日志级别、过滤器等。
Log4j 2还提供了各种桥接包,以便与SLF4J和其他日志框架进行集成。
五、主流日志框架
如上面介绍的,当前Java项目使用日志采用的是日志门面+日志实现框架
的方式。下面分别介绍。
5.1 日志门面
目前流行的有两种日志门面框架JCL和SLF4J。
SLF4J
:一个非常流行的日志门面,提供了一套简单的日志记录接口,并且可以与多种日志框架(如Log4j、Logback等)配合使用。
JCL
(Jakarta Commons Logging):早期的一个日志门面,目前在一些老项目中仍然在使用。
Log4j2
:Log4j2提供了门面功能,这意味着它可以作为一个中间层,与不同的日志实现进行集成。然而,在实际应用中,开发者更倾向于使用专门的日志门面框架(如SLF4J)来与Log4j2等日志实现进行集成。
5.2 日志实现
下面对主要的日志实现框架进行介绍。
JUL(Java Util Logging)
Java自带的日志框架,功能相对基础,性能一般,但对于简单的日志需求来说足够用了。主要包括Logger、Handler和Formatter三个核心组件。Logger是日志记录器,用于生成日志记录;Handler是日志处理器,负责将日志信息输出到不同的目的地,如控制台、文件等;Formatter是日志格式化器,负责定义日志的输出格式。
Log4j
一个老牌的日志框架,功能非常强大,可以自定义很多日志的细节,比如日志级别、输出格式、输出目的地等。它使用Layout来控制日志内容的输出格式,常用的有HTMLLayout、SimpleLayout和PatternLayout等。其中,PatternLayout是最强大的格式化器,允许自定义输出格式。log4j 1.x版本2005年已经不再维护,建议升级到Log4j 2.x。如果一定要使用,推荐使用reload4j,详见下面介绍。
reload4j
reload4j是Apache log4j版本1.2.17的分支,旨在解决安全问题,提供相似功能和API并进行了安全修复与改进,可作为log4j 1.x的直接替代品并与SLF4J等日志门面集成使用。
Logback
由Log4j的原开发者之一主导开发,Spring Boot默认日志框架,轻量级,性能优秀,功能也比较全面。Logback是一个轻量级、性能优秀的日志框架,默认配置下与Spring Boot配合良好。它支持编程式配置和配置文件配置两种方式,可以灵活控制日志的输出格式、级别和目的地。
Log4j2
Log4j2是Log4j的升级版,参考了Logback的一些优秀设计,修复了一些bug,性能和功能都带来了极大提升。它支持异步日志记录,可以显著减少日志记录对主程序性能的影响,尤其是在高并发场景下。
slf4j-simple
slf4j-simple是一个轻量级的日志框架,作为SLF4J的简单实现,提供基本的日志记录功能并默认将日志输出到控制台。
六、日志框架的使用
在实际开发中,我们通常使用日志门面(JCL或者SLF4J)来记录日志,而不是直接使用具体的日志实现框架。这样做的好处是,可以在不修改代码的情况下,通过配置来切换不同的日志实现框架。虽然现在主流使用的是SLF4J,这里我也会介绍一下JCL的使用。
6.1 日志框架的集成
下面是SLF4J官网给出的原理图:
【图】SLF4J原理图
1、没有绑定任何日志实现,门面技术SLF4J是不能够实现任何功能的。包括自带的slf4j-simple官方提供的也必须导入相应的jar包配合使用。
2、 logback和simple(包括nop-禁止打印日志)都是SLF4J出现之后提供的日志实现,API完全遵循SLF4J设计,使用只需要导入对应的jar包即可。
3、 log4j和JUL时间线是在slf4j之前的,要配合使用就必须通过适配桥接技术(slf4j-log4j12和slf4j-jdk14),完成与日志门面的连接。
4、 当导入slf4j-nop后将不会使用任何日志框架
注意: 在SLF4J环境下,若同时导入多个日志实现框架,默认使用先导入的。在实际应用中,一般只集成一种日志实现。
6.2 日志框架的桥接
如果你的项目中已经使用了JCL、Log4j 1.x等老的日志框架,但想迁移到使用SLF4J的API,这时候你可以使用桥接器
[6]来平滑过渡而不用修改原有代码,只需要修改依赖即可完成。
下面是SLF4J桥接旧版日志框架的原理图:
【图】SLF4J桥接旧版日志框架的原理图
上图表明了对旧版的框架log4j 1.x、JCL和java.util.logging(JUL)的API的调用迁移到SLF4J的API,需要引入如下包:
log4j-over-slf4j
: 允许Log4j 1.x用户(但不允许Log4j 2.x)将现有应用程序/库迁移到SLF4J,而无需更改原有代码,只需将log4j.jar文件替换为log4j-over-slf4j.jar
jcl-over-slf4j.jar
:为了简化从JCL到SLF4J的迁移,SLF4J发布了文件jcl-over-slf4j.jar。这个jar文件实现了 JCL的公共API,但在底层使用了SLF4J,因此命名为 “JCL over SLF4J”。
jul-to-slf4j.jar
:它路由所有传入的JUL记录到SLF4j API。
注意:
1、log4j-over-slf4j.jar、slf4j-reload4j.jar和slf4j-log4j.jar不能同时存在,否则会导致无限循环。
2、jul-to-slf4j.jar和slf4j-jdk14.jar不能同时存在,否则会导致无限循环。
七、JCL 门面框架示例
JCL有两个基本的抽象类:Log(日志记录器)、LogFactory(日志工厂,负责创建Log实例)。公共测试代码如下:
package org.log.demo;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;public class App {public static void main(String[] args) {Log logger = LogFactory.getLog(App.class);logger.info("Hello World");}
}
7.1 JCL+JUL
JCL自带JUL实现。 增加依赖:
<!--引入commons-logging日志接口-->
<dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version>
</dependency>
运行结果:
11月 26, 2024 9:03:48 下午 org.log.demo.App main
信息: Hello World
查看堆栈,调用了JDK自带的java.util.logging.Logger#getLogger(java.lang.String)方法获取Logger。
【图】JUL的Logger
7.2 JCL+log4j 1.x
JCL支持log4j。直接添加依赖并配置日志格式,即可调用。这里需要注意的是,log4j 1.x只支持到1.2.17版本,更安全的版本是使用reload4j。下面分别给出两种使用方式,区别在于引入的依赖不同。
首先,需要增加配置文件log4j.properties
,内容如下:
#配置日志级别,引用控制器
log4j.rootLogger=INFO,console
#配置控制台输出器
log4j.appender.console=org.apache.log4j.ConsoleAppender
#配置自定义格式器
log4j.appender.console.layout=org.apache.log4j.PatternLayout
#配置自定义转换模式
log4j.appender.console.layout.conversionPattern=[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5p] [%t] [%-4rms] [%c#%M-%L] %m%n
然后,增加依赖。 使用log4j 1.2.17及以前的版本。依赖如下:
<!--引入JCL日志接口-->
<dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version>
</dependency>
<!-- log4j日志框架 -->
<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version>
</dependency>
使用更安全的版本reload4j。依赖如下:
<!--引入JCL日志接口--><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>2.0.16</version></dependency>
可以看到slf4j-log4j12的2.0.16版本中pom会重定位到slf4j-reload4j依赖
也就是说上面引入slf4j-log4j12,等价于:
<!--引入JCL日志接口-->
<dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version>
</dependency>
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-reload4j</artifactId><version>2.0.16</version>
</dependency>
运行结果:
[2024-11-26 21:20:20.285] [INFO ] [main] [0 ms] [org.log.demo.App#main-10] Hello World
通过下面调用栈可以看到调用了log4j 1.2.17包的org.apache.log4j.Category#log(String,Priority, Object, Throwable)打印日志。
【图】log4j 1.2.17包
通过下面调用栈可以看到调用了reload4j 1.2.22包的org.apache.log4j.Category#log(String,Priority, Object, Throwable)打印日志。
【图】reload4j 1.2.22包
7.3 JCL+SLF4J
为了减少代码改动,即仍然使用JCL的API,但是又想依赖SLF4J的日志实现,如logback,可以通过引入桥接包jcl-over-slf4j实现。
步骤如下:
不直接引入 JCL 的核心包(比如 commons-logging)。
引入 jcl-over-slf4j,这是将JCL调用桥接到SLF4J的库。
引入 slf4j-api,这是SLF4J的API包。
引入你选择的日志实现,比如 logback-classic、log4j2等SLF4J的实现框架。
配置logback配置文件logback.xml
测试代码
依赖如下:
<!--引入slf4j日志接口-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.16</version>
</dependency>
<!-- 使用logback实现slf4j日志-->
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.5.12</version>
</dependency>
<!--引入jcl日志桥接器,将jcl日志接口桥接到slf4j日志接口上-->
<dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>2.0.16</version>
</dependency>
需要在resources下增加logback配置文件logback.xml
,内容如下:
<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><root level="debug"><appender-ref ref="STDOUT" /></root>
</configuration>
调用logback的ch.qos.logback.classic.Logger#log(org.slf4j.Marker, java.lang.String, int, java.lang.String, java.lang.Object[], java.lang.Throwable)输出日志。
【图】logback-classic包
如果将依赖改为log4j2,需要修改pom.xml如下:
<!--引入jcl日志桥接器,将jcl日志接口桥接到slf4j日志接口上--><dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>2.0.16</version><exclusions><exclusion><artifactId>slf4j-api</artifactId><groupId>org.slf4j</groupId></exclusion></exclusions></dependency><!--添加log4j2与slf4j的桥接器--><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j-impl</artifactId><version>2.24.2</version></dependency>
需要在resources下增加log4j2配置文件log4j2.xml,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN"><Appenders><!-- 定义一个控制台输出的Appender --><Console name="Console" target="SYSTEM_OUT"><!-- 定义日志输出的格式 --><PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/></Console></Appenders><Loggers><!-- 定义一个根Logger,它接收所有的日志记录 --><Root level="info"><!-- 将根Logger的日志输出到之前定义的Console Appender --><AppenderRef ref="Console"/></Root></Loggers>
</Configuration>
调用log4j2-core的org.apache.logging.log4j.core.Logger#logMessage输出日志。
2024-11-26 22:16:39 [main] INFO org.log.demo.App - Hello World
【图】log4j2-core包
八、SLF4J 门面框架示例
SLF4J通过两个核心组件Logger和LoggerFactory来输出日志。 公共测试代码如下:
package org.log.demo;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class App {public static void main(String[] args) {Logger logger = (Logger) LoggerFactory.getLogger(App.class);logger.info("Hello World");}
}
8.1 SLF4J+logback
logback直接实现了SLF4J的接口。 添加依赖:
<!--引入slf4j日志接口-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.16</version>
</dependency>
<!-- 使用logback实现slf4j日志-->
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.5.12</version>
</dependency>
配置文件logback.xml
,内容如下:
<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><root level="debug"><appender-ref ref="STDOUT" /></root>
</configuration>
输出日志:
2024-11-26 22:25:05 [main] INFO org.log.demo.App - Hello World
调用logback的ch.qos.logback.classic.Logger#info(java.lang.String)打印日志。
【图】logback-classic包
8.2 SLF4J+log4j2
添加依赖:
<!--添加log4j2与slf4j的桥接器--><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j-impl</artifactId><version>2.24.2</version></dependency>
需要在resources下增加log4j2配置文件log4j2.xml,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN"><Appenders><!-- 定义一个控制台输出的Appender --><Console name="Console" target="SYSTEM_OUT"><!-- 定义日志输出的格式 --><PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/></Console></Appenders><Loggers><!-- 定义一个根Logger,它接收所有的日志记录 --><Root level="info"><!-- 将根Logger的日志输出到之前定义的Console Appender --><AppenderRef ref="Console"/></Root></Loggers>
</Configuration>
输出日志:
2024-11-26 22:28:13 [main] INFO org.log.demo.App - Hello World
调用log4j-api包的org.apache.logging.log4j.spi.AbstractLogger#logMessage(java.lang.String, org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String)输出日志。
【图】log4j-api包
8.3 SLF4J+reload4j
添加依赖:
<!--引入slf4j日志接口--><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.16</version></dependency>
<!-- 使用reload4j实现slf4j日志--><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-reload4j</artifactId><version>2.0.16</version></dependency>
添加配置文件log4j.properties,内容如下:
log4j.rootLogger=INFO,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.conversionPattern=[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5p] [%t] [%-4rms] [%c#%M-%L] %m%n
输出日志:
[2024-11-26 22:32:02.847] [INFO ] [main] [0 ms] [org.log.demo.App#main-9] Hello World
调用reload4j包的org.apache.log4j.Category#log(java.lang.String, org.apache.log4j.Priority, java.lang.Object, java.lang.Throwable)输出日志。
【图】reload4j包
该种方式与如下配置log4j 1.x依赖结果是一样的。
<!--使用log4j1.2.x-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>2.0.16</version>
</dependency>会重定位到reload4j的依赖。<distributionManagement><relocation><groupId>org.slf4j</groupId><artifactId>slf4j-reload4j</artifactId><version>2.0.16</version> </relocation>
</distributionManagement>
8.4 SLF4J+JUL
添加依赖:
<!--引入slf4j日志接口-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.16</version>
</dependency>
<!--使用(JUL)java.util.logging实现slf4j日志-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-jdk14</artifactId><version>2.0.16</version>
</dependency>
输出日志:
11月 26, 2024 10:37:45 下午 org.log.demo.App main
信息: Hello World
调用JDK包的java.util.logging.Logger#log(java.util.logging.LogRecord)输出日志。
【图】JDK包
8.5 SLF4J+simple
添加依赖:
<!--引入slf4j日志接口-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.16</version>
</dependency>
<!--使用simple实现slf4j日志-->
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>2.0.16</version>
</dependency>
输出日志:
[main] INFO org.log.demo.App - Hello World
调用slf4j-simple包的org.slf4j.simple.SimpleLogger#innerHandleNormalizedLoggingCall输出日志。
【图】slf4j-simple包
九、日志规约
在日志开发中,需要遵守一些规约来确保日志的规范性和可读性。
阿里巴巴Java开发手册中提供了以下日志规约:
应用中不可直接使用日志系统(如Log4j、Logback)中的API,而应依赖使用日志框架(如SLF4J、JCL)中的API。
尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。
十、总结
本文详细介绍了Java日志框架的基本概念、主流框架以及使用方法。通过了解日志框架的组成和特性,开发者可以更好地选择和使用适合自己的日志框架,从而更有效地进行日志记录和管理。在实际开发中,建议遵守日志规约,确保日志的规范性和可读性。
参考资料
[1]Log4j: https://logging.apache.org/log4j/1.x/
[2]reload4j: https://reload4j.qos.ch/
[3]SLF4J: https://www.slf4j.org/manual.html
[4]Logback: https://logback.qos.ch/
[5]Log4j 2: https://logging.apache.org/log4j/2.12.x/
[6]桥接器: https://www.slf4j.org/legacy.html
原创 程序员Ink