日志框架出现的历史顺序:Log4j → JUL → JCL → slf4j → logback → log4j2
前言:JCL日志门面淘汰史
常见的日志门面:JCL、slf4j。为啥日志门面已经有JCL了,还要推出slf4j??
JCL设计缺陷
当时只考虑了主流的日志框架JUL、Log4j,随着技术的发展,后面会出现很多优秀的日志,这些技术我们要使用的话,它默认是不支持的,就表示我们需要在开发时修改源代码进行扩展,一般在企业开发时我们是不会这么做的,因此JCL就被淘汰了
☁ JCL被淘汰了,那么还有谁能站出来帮我们统一管理和维护所有的API呢?答:slf4j,它可以支持现在所有的主流日志框架,以及未来出现新的日志技术进行扩展的接口
SL4J 的使用
简单日志门面(Simple Logging Facade For Java)
SL4J 主要是为了给Java日志访问提供一个标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j 和 logback等,当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到,对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间试验桥接器完成桥接
★ 描述
SL4J 是目前市面上最流行的日志门面,现在的项目中,基本都是使用SL4J作为我们的日志系统。SL4J日志门面主要提供两大功能:
- 日志框架的绑定
- 日志框架的桥接
▎入门案例
创建普通的maven项目即可
1. 导入相关slf4j依赖
<dependencies>
<!--junit测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!--slf4j内置的日志实现(功能较为简单,一般使用桥接第三方的日志实现) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
2. 测试用例
public class Demo {
// 全局logger对象
private static final Logger logger = LoggerFactory.getLogger(Demo.class);
@Test
public void testQuick(){
// 1.普通输出(级别依次由高到低)
logger.error("error msg");
logger.warn("warn msg");
logger.info("info msg"); // 默认级别
logger.debug("debug msg");
logger.trace("trace msg");
// 2.占位符输出,用{}括号表示占位符
String name = "梅花十三";
int age = 18;
logger.info("用户:{},{}",name,age);
// 3.将系统的异常输出
try {
int i = 1/0;
}catch (Exception e){
//e.printStackTrace();
logger.error("异常信息:",e);
}
}
}
3. 测试结果
➳ 说明:slf4j不仅提供普通的日志信息打印,还包含了很多重载方法,比如对系统异常的输出
slf4j 日志绑定(Binding)
在入门案例中,我们使用sf4j 绑定了内置的日志简单实现框架
它是如何绑定其他日志主流框架呢?
如果只引入日志门面依赖,而未引入相关日志实现框架,则功能默认处于关闭状态。
1. 我们可以打开sf4j官网:SLF4J,点击用户手册,查看具体的流程,如下:
2. 滑至最下方,可以看到官网提供的绑定日志实现的具体流程图
➳ 绑定说明
情况一:图中最左侧第一个显示的 /dev/null表示,如果只引入slf4j-api 日志门面依赖,而不引入具体的日志实现框架,日志功能默认处于关闭状态,无法进行任何的日志输出。
情况二:图中三个深蓝色模块分别是:logback、slf4j 内置的简单日志实现框架、nop。这三个日志框架设计是比slf4j晚的,因此默认就支持slf4j的API规范,只需要导入其实现即可
情况三:中间两个湖绿色的模块是 log4j、jdk14(JUL: java.util.logging)。这两个日志实现框架设计比较早,默认是没有遵循slf4j的API规范,无法进行直接绑定,中间需要加一层适配层 Adaptation layer,通过这个适配器来适配具体的实现,间接的遵循了slf4j的API规范
一、绑定logback
从功能和性能上而言都比log4j更强大
1. 导入两个实现包依赖(在上述入门案例中的pom.xml 基础上,增加logback的依赖即可)
<dependencies>
<!--junit测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!--slf4j 内置的简单实现 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<!-- logback 日志实现框架-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.9</version>
</dependency>
</dependencies>
❥ 小知识:导入logback-classic.jar 一个依赖即可,因为maven有传递思想,会自动传递core包
2. 测试用例输出结果如下
3. 把slf4j的内置实现框架给注释掉
4. 继续运行测试用例:控制台的输出字体颜色变成了黑色,simple的日志实现框架字体是红色的
注释掉simple的日志框架依赖,如果还出现错误,记得把maven刷新一下
!! 注意:默认日志级别是debug,图中注释说明有误
二、绑定slf4j-simple
slf4j 内置的日志实现框架,功能简单,一般不大使用
在上述入门案例代码中,用的就是simple的日志实现依赖,这里就不再二次贴出演示了
三、绑定slf4j-nop
日志开关,导入slf4j-nop.jar依赖后,表示slf4j将不会使用任何实现框架了
与不绑定具体的日志实现框架的效果是一样的,日志功能将被关闭。
1. 引入slf4j-nop依赖
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!--slf4j-nop 日志开关,一旦引入依赖,所有日志实现框架将失效-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.36</version>
</dependency>
2. 测试用例执行效果
!! 注意:如果引入sf4j-api 不引入其他具体实现日志,虽然日志也会失效,但是控制台会报警告, 引入sf4j-nop需要注释掉其他具体实现日志,否则会报警告,并且还会正常打印日志
四、绑定log4j
Apache下的一款开源的日志框架,可以控制日志信息输出到控制台、文件、数据库。
前面也说了,log4j 和 jdk14(JUL: java.util.logging)这两个日志实现框架设计比较早,默认是没有遵循slf4j的API规范,无法进行直接绑定,中间需要加一层适配层 Adaptation layer
♦ 要点:通过这个适配器来适配具体的实现,间接遵循slf4j的API规范
1. 增加Adaptation layer适配器依赖 + log4j依赖
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!-- 绑定log4j日志实现框架,需要导入适配器 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.36</version>
</dependency>
<!-- log4j 日志实现 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
2. 运行测试用例如下
可以看到log4j日志实现绑定成功了,上述警告提示找不到该logger对象的appenders处理器,它希望我们设置并初始化一个系统配置信息 ,这个问题我在《日志实现框架(1):Log4j》章节讲过,这里不再解释,我们把之前搭建的配置文件,复制到当前项目的资源目录下
3. 搭建log4j.properties配置文件
4. 再次运行测试用例:可以看到日志信息正常输出,表示log4j绑定成功了,当前使用的是log4j
五、绑定JUL
全称 Java Util Logging,Java原生的日志框架,JDK自带的,使用无需另外引用第三方类库
♦ 要点:通过这个适配器来适配具体的实现,间接遵循slf4j的API规范
1. JUL是JDK自带的,无需引入依赖就能使用,但是采用的是slf4j技术门面,默认不支持JUL,因此需要引入Adaptation layer适配器依赖
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!-- 绑定jul 日志实现,需要导入适配器 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.36</version>
</dependency>
2. 测试用例运行结果:成功绑定并使用的日志实现框架是JUL
▎绑定日志的实现(Binding)
如上所述,SLF4J 支持各种日志框架,SLF4J发行版附带了几个称为“SLF4J绑定”的 jar 文件,每个绑定对应一个受支持的框架。
☛ 使用 slf4j 的日志绑定流程
① .添加 slf4j-api 的依赖
②.使用slf4j的API在项目中进行统一的日志记录
③.绑定具体的日志实现框架
a.绑定已经实现了slf4j的日志框架,直接添加对应依赖
b.绑定没有实现slf4j的日志框架, 先添加日志的适配器,再添加实现类的依赖
④.slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)
1. 查看LoggerFactory 日志工厂获取logger日志记录器的源码实现
// 全局logger对象
private static final Logger logger = LoggerFactory.getLogger(Demo.class);
提示:logger对象是从日志工厂中获取的,因此来查看下日志工厂是如何初始化进行logger装载的
2. 在getILoggerFactory获取工厂方法中,包含了 performInitialization() 初始化工厂方法,如下
这里涉及到一个名为INITIALIZATION_STATE 的静态变量,用来记录当前初始化的状态。默认是UNINITIALIZED,第一次调用getILoggerFactory()方法时,检查到INITIALIZATION_STATE == UNINITIALIZED,就会调用performInitialization()方法来进行初始化。
performInitialization()方法在初始化完成时,会设置INITIALIZATION_STATE为SUCCESSFUL_INITIALIZATION。这样后面的switch语句就会调用StaticLoggerBinder.getSingleton().getLoggerFactory()来返回需要的ILoggerFactory。
3. 日志工厂是通过bind()方法,来绑定具体的日志实现类
4. findPossibleStaticLoggerBinderPathSet () 作用是查找当前classpath下的StaticLoggerBinder的实现,如果有多个的话,则reportMultipleBindingAmbiguity()和reportActualBinding()方法会在绑定前后打印相应的信息。
findPossibleStaticLoggerBinderPathSet()方法其实就是通过classloader的getResources()方法找到所有的名为”org/slf4j/impl/StaticLoggerBinder.class”的resource。
5. 我们再回来看bind()方法,其实真正绑定的代码只有一行
这里调用了StaticLoggerBinder.getSingleton()方法。StaticLoggerBinder类的权限定名,恰好和findPossibleStaticLoggerBinderPathSet()方法中查找的一致。
思考:StaticLoggerBinder类从哪里来? 具体的log api实现要如何做才能和slf4j绑定和装载?
在slf4j-api的源代码中,的确有对应的package和类存在
但是打开打包好的slf4j-api.jar,却发现根本没有这个impl 的包。
在slf4j-api项目的pom.xml文件中,我们可以找到下面的内容:
这里通过调用ant在打包为jar文件前,将package org.slf4j.impl和其下的class都删除掉了。
实际impl包内的代码,只是用来占位以保证可以编译通过。需要在运行时再进行绑定。
6. 我们再来看,具体的log api实现要如何做才能和slf4j绑定和装载
如下,在引入了slf4j 内置的日志实现框架simple 以及 JUL日志实现框架中,可以找到其jar包的该类路径:”org/slf4j/impl/StaticLoggerBinder.class”,
!! 注意:同样在slf4j支持的其他日志实现框架依赖的jar包中,例如slf4j-log4j12、slf4j-jdk14、logback-classic 都能找到该类路径的同名类
7. 继续看回StaticLoggerBinder的代码
8. 以slf4j-simple为例:这里的getLoggerFactory()方法会返回slf4j-simple实现的SimpleLoggerFactory。
9. SimpleLoggerFactory实现slf4j定义的 ILoggerFactory接口,getLogger()方法中负责创建SimpleLogger对象并返回 (为了提高性能做了cache)。
➳ 结论:因此从这里就能得出,slf4j 绑定的具体实现类接口,是在bind() 方法完整绑定的。
其他日志实现框架流程也是如此,类似的slf4j-log4j12中会返回Log4jLoggerFactory,而Log4jLoggerFactory中通过调用log4j的LogManager来创建log4j的Logger对象并通过Log4jLoggerAdapter类来包装为slf4j的Logger(adapter模式)。
log4jLogger = LogManager.getLogger(name);
Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
★ SLF4J 绑定日志框架原理解析
slf4j在运行时绑定具体的日志实现框架。关键之处就在于 org.slf4j.impl.StaticLoggerBinder
- SLF4J 通过LoggerFactory 加载日志具体的实现对象
- LoggerFactory在初始化的过程中,会通过performinitialization()方法绑定具体的日志实现
- 在绑定具体实现的时候,通过类加载器,加载org/slf4g/impl/StaticLoggerBinder.class
- 所以,只要是一个日志实现框架,在org/slf4g/impl 包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J 所加载
Logger 和 LoggerFactory是slf4j定义好的。LoggerFactory通过装载StaticLoggerBinder类来绑定具体的日志实现框架,得到其日志实现框架中ILoggerFactory接口的实现类。该LoggerFactory的实现类会创建其对应日志实现框架的logger对象。并调用logger对象来写日志。
➳ 结论:业务代码此时不知道底层是哪个日志实现框架,从而摆脱对具体日志实现框架的依赖
桥接旧的日志框架(Bridging)
解决项目中日志的遗留问题,当系统中存在之前的日志API,可通过桥接转换到slf4j的实现
slf4j桥接说明官网:Log4j Bridge
通常,您所依赖的某些组件依赖于 SLF4J 以外的日志记录 API。您还可以假设这些组件在不久的将来不会切换到 SLF4J。为了处理这种情况,SLF4J 附带了几个桥接模块,这些模块将对 log4j、JCL 和 java.util.logging API 的调用重定向到好像它们是对 SLF4J API 进行的。
➳ 操作流程
- 先去除之前老的日志框架的依赖
- 添加SLF4J提供的桥接组件
- 为项目添加SLF4J的具体实现
▎举例说明
场景:项目初期未考虑到架构层次问题,日志框架是log4j,随着项目功能迭代,现有日志框架不满足业务需求,需升级为日志门面技术slf4j + logback具体的日志实现框架。
诉求:我们希望是不想修改原有log4j原有代码!
➳ 操作步骤:
- 去除原有log4j依赖
- 引入log4j-over-slf4j.jar 桥接依赖(作用:将应用程序迁移到 SLF4J,而无需更改一行代码)
- 引入slf4j-api.jar 日志门面依赖
- 引入目标使用日志框架 logback-classic.jar 依赖
1. 假设现在的项目采用的是log4j ,依赖如下
<dependencies>
<!--junit测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!-- log4j 日志实现 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
</dependencies>
测试用例执行结果
2. 更换日志实现框架为logback
<dependencies>
<!--junit测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!-- log4j 日志实现 -->
<!-- 1. 去除log4j依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>-->
<!-- 2.引入新的日志框架 -->
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!-- logback 日志实现框架-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.9</version>
</dependency>
</dependencies>
✷ 问题:如果仅仅只是把原log4j 依赖去除,加上新的logback日志框架依赖,那么原log4j的代码会出现 报红异常,导致代码不可运行
➳ 解决:只需要再引入slf4j提供的log4j的桥接依赖即可,无需调整代码
<!-- log4j 桥接器 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.36</version>
</dependency>
3. 测试代码用例
➳ 结论:增加了log4j的桥接依赖后,无需修改原log4j的代码,虽然是log4j的代码,但控制台日志输出框架为logback
原理:加入桥接依赖后,原log4j代码不会直接调用log4j底层,而是会委托给SLF4J 所加载,再由SLF4J去调用新引入的logback日志框架,从而不需要修改原log4j的代码,也能替换新的日志实现框架!
▎桥接旧的日志框架注意事项
桥接器和适配器不能同时出现,否则会出现死循环,造成栈溢出!
例如,假设我引入了slf4j技术门面,并绑定了log4j日志实现框架,但是我又引入了log4j桥接器
<dependencies>
<!--junit测试类 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!--slf4j 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!-- 绑定log4j日志实现框架,需要导入适配器 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.36</version>
</dependency>
<!-- log4j 日志实现 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
<!-- log4j 桥接器 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
运行测试用例,报栈溢出
➳ 注意问题总结
- jcl-over-slf4j.jar 桥接依赖 和 slf4j-jcl.jar 适配器不能同时部署,前一个jar文件将导致JCL将日志系统的选择委托给SLF4J,后一个jar文件将导致SLF4J将日志系统的选择委托给JCL,从而导致无限循环
- Log4j-over-slf4j.jar 桥接依赖 和 slf4j-log4j12.jar (SLF4J 绑定的log4j日志实现) 不能同时出现
- Jul-to-slf4j.jar桥接依赖 和 slf4j-jdk14.jar (SLF4J 绑定的JUL日志实现) 不能同时出现
- 所有的桥接都只对Logger日志记录器对象有效,如果程序中调用了内部的配置类或者是Appender,Filter等对象,将无法产生效果