前言
虽然在新的项目中,我们一般使用推荐的SLF4J + 日志实现框架(Logback等)组合方式,但是对于一些旧的项目,已经使用了SLF4J之外的日志框架(如Log4j 1.x等),而且这些旧的代码我们无法直接修改源码,如果我们想使用SLF4J的API,那么就需要使用各种SLF4J的桥接器来实现。
注意,对于可以直接修改源码的项目,应该直接将旧的日志API修改为SLF4J API,而非使用本文介绍的SLF4J桥接器。修改源码的方式可以参考:SLF4J Migrator[1]。本文介绍的桥接器只适用于无法修改项目依赖的包的源码的情形。
为什么要桥接到 SLF4J
可能有的同学会有此疑问,明明之前的日志框架(如log4j 1.x)用得好好的,为什么要桥接到SLF4J门面日志框架呢?
首先我们要从企业级项目的思维来考虑这个问题,如果只是很小的项目记录日志,确实没有必要。但是对于企业级的项目来说,很有必要。
这里给出几个关键的原因:
1、日志门面(Facade)的统一性:SLF4J 作为一个日志门面,提供了一个简单的日志记录抽象。它允许开发者在编写代码时,不必关心底层使用的是哪个具体的日志框架。通过使用这些桥接器,即使项目中已经使用了其他日志框架(如 log4j、JCL、JUL),也可以轻松地将它们适配到 SLF4J 的接口上,从而实现日志记录的统一性。
2、避免日志框架的冲突:在大型项目中,可能会包含多个使用不同日志框架的库或模块。这些库或模块可能会因为日志框架的冲突而导致运行时错误。通过使用桥接器,可以将这些不同的日志框架适配到 SLF4J 上,从而避免冲突。
3、便于日志框架的切换:随着时间的推移,项目的需求可能会发生变化,可能需要切换到另一个日志框架。如果项目中使用了 SLF4J 作为日志门面,并且使用了相应的桥接器,那么切换日志框架将变得非常简单。只需替换桥接器和底层日志框架的依赖,而无需修改大量的日志记录代码。
4、解耦和模块化:桥接器的使用有助于将日志记录逻辑与业务逻辑解耦。通过将日志记录抽象到 SLF4J 层面上,开发者可以更加专注于业务逻辑的实现,而不必担心日志记录的具体实现细节。这有助于提高代码的可读性和可维护性。
5、性能优化:虽然桥接器本身可能会引入一些性能开销(因为需要额外的调用转换),但在某些情况下,它们可以帮助优化性能。例如,如果底层日志框架的性能不佳,但项目又无法立即切换到另一个性能更好的日志框架,那么可以使用桥接器来暂时缓解性能问题,并在后续逐步迁移到更好的日志框架上。
6、兼容性:有些老旧的库或框架可能只支持特定的日志框架(如 log4j 1.x)。为了与这些库或框架兼容,可以使用相应的桥接器来将它们适配到 SLF4J 上,从而允许项目使用更现代、更灵活的日志框架。
这些好处使得SLF4J和相应的桥接器成为许多企业级Java项目中日志记录的首选方案。
桥接器的原理
如果你的项目中已经使用了JCL、Log4j 1.x等老的日志框架,但希望使用SLF4J的API,这时候你可以使用SLF4J桥接器[2]来平滑过渡而不用修改原有代码,只需要修改依赖即可完成。
下面是SLF4J桥接旧的日志框架的原理图:
【图】SLF4J桥接旧的日志框架的原理图
上图表明了对旧的日志框架log4j 1.x、JCL、java.util.logging(JUL)和log4j 2的API的调用迁移到SLF4J的API,需要引入的包:
1、log4j-over-slf4j
[3] : 允许Log4j 1.x用户(但不允许Log4j 2.x)将现有应用程序/库迁移到SLF4J,而无需更改原有代码,只需将log4j.jar文件替换为log4j-over-slf4j.jar
2、jcl-over-slf4j
[4] :为了简化从JCL到SLF4J的迁移,SLF4J发布了jcl-over-slf4j.jar。这个jar文件实现了 JCL的公共API,但在底层使用了SLF4J,因此命名为 “JCL over SLF4J”。
3、jul-to-slf4j
[5] :它路由所有传入的JUL记录到SLF4j API。
4、log4j-to-slf4j
[6] :log4j 2迁移到SLF4J API,需要使用log4j-to-slf4j,这个包在上图没有体现,可参见:Log4j 2 与 SLF4J 互转的核心:log4j-slf4j-impl 和 log4j-to-slf4j
注意,这些桥接器的作用是为了将旧的日志API迁移到SLF4J API,在包的依赖时需要搞清楚其作用,不要造成循环。
具体如下:
1、log4j-over-slf4j.jar和slf4j-reload4j.jar(或slf4j-log4j.jar)不能同时存在,否则它们的API会互相重定向到对方API,导致无限循环。
2、jul-to-slf4j.jar和slf4j-jdk14.jar不能同时存在,否则它们的API会互相重定向到对方API,导致无限循环。
3、log4j-to-slf4j和log4j-slf4j-impl不能同时存在,否则它们的API会互相重定向到对方API,导致无限循环。
SLF4J + 日志实现框架组合方式的示例,参见【java开发】一文理清 Java 日志框架的来龙去脉
桥接器与适配器模式
所谓的桥接器本质上来说就是将一套API在不改变调用代码的情况下,将底层实现重定向到另一套API,这也是设计模式中适配器模式的基本原理。
这里,我将结合log4j 1.x的API桥接到SLF4J API的依赖包log4j-over-slf4j的源码讲解适配器模式的应用。
本文源码分析使用的log4j-over-slf4j版本是2.0.16,logback-classic版本是1.5.12,slf4j-api版本是2.0.16。
演示代码
为了演示将log4j 1.x桥接到SLF4J,需要完成以下两步:
1、移除log4j 1.x依赖。
2、添加SLF4J API和实现框架Logback,以及log4j 1.x桥接器依赖。
<!--log4j 1.x/reload4j 使用slf4j接口-->
<dependency><groupId>org.slf4j</groupId><artifactId>log4j-over-slf4j</artifactId><version>2.0.16</version>
</dependency>
<!--logback 实现 slf4j2-->
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.5.12</version>
</dependency>
<!-- 移除log4j 1.x 的依赖-->
<!-- <dependency>-->
<!-- <groupId>log4j</groupId>-->
<!-- <artifactId>log4j</artifactId>-->
<!-- <version>1.2.17</version>-->
<!-- </dependency>-->
其中log4j-over-slf4j会自动引入依赖slf4j-api。不需要显式引入。
【图】演示代码的依赖
在resources目录下添加Logback配置文件logback.xml,内容如下:
<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern></encoder></appender><root level="debug"><appender-ref ref="STDOUT" /></root>
</configuration>
测试代码如下:
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;public class HelloWorld {private static final Logger logger = LogManager.getLogger(HelloWorld.class);public static void main(String[] args) {logger.info("Hello, World!");logger.debug("This is a debug message.");logger.error("An error occurred.");}
}
其中,Logger类和LogManager类都是log4j 1.x中的核心类。
运行结果:
2024-11-29 14:27:40 INFO org.learn.HelloWorld - Hello, World!
2024-11-29 14:27:40 DEBUG org.learn.HelloWorld - This is a debug message.
2024-11-29 14:27:40 ERROR org.learn.HelloWorld - An error occurred.
关于SLF4J获取Logger和打印日志的源码分析不是本文重点,可以参见:【Java开发】SLF4J 门面日志框架原理分析
适配器模式分析
让我们先回顾一下什么是适配器模式。
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许不兼容的接口协同工作。
适配器模式的主要要素包括:
1、目标接口(Target Interface):这是客户端期望使用的接口。
2、适配者(Adaptee):这是现有的类,它具有不同的接口,但提供了所需的功能。
3、适配器(Adapter):这是一个类,它实现了目标接口,并持有适配者的引用。适配器负责将客户端的请求转换为适配者的请求。
适配器模式分为类适配器模式和对象适配器模式两种:
类适配器模式
:通过继承适配者,并实现目标接口。
对象适配器模式
:通过组合适配者,并实现目标接口。
关于适配器模式,参见:设计模式--适配器模式
log4j-over-slf4j 中的适配器模式
在log4j-over-slf4j中,org.apache.log4j.Logger类的父类org.apache.log4j.Category 类中实现了适配器模式。
1. 目标接口(Target Interface)
在 Category 类中,目标接口(这里只是类比适配器模式中的目标接口角色)是 Category 本身。这个类定义了日志记录的方法,如 debug, info, warn, error, fatal 等。客户端期望使用这些方法来记录日志。
2. 适配者(Adaptee)
适配者是 SLF4J 的 Logger 接口及其实现类 slf4jLogger。SLF4J 提供了日志记录的功能,但其接口与 log4j 的 Category 类不完全相同。例如,SLF4J 没有 fatal 方法,而是使用带有 FATAL 标记的 error 方法。
3. 适配器(Adapter)
Category 类充当适配器的角色。它持有一个 SLF4J 的 Logger 实例。Category 类通过委托的方式将日志记录的请求转发给 SLF4J 的 Logger 实例。
Category源码分析
1、构造函数:
protected org.slf4j.Logger slf4jLogger;
private org.slf4j.spi.LocationAwareLogger locationAwareLogger;Category(String name) {this.name = name;slf4jLogger = LoggerFactory.getLogger(name);if (slf4jLogger instanceof LocationAwareLogger) {locationAwareLogger = (LocationAwareLogger) slf4jLogger;}
}
构造函数中,Category 实例创建时会初始化一个 SLF4J 的 Logger 实例,并检查是否是 LocationAwareLogger。
2、日志记录方法:
public void debug(Object message) {differentiatedLog(null, CATEGORY_FQCN, LocationAwareLogger.DEBUG_INT, message, null);
}public void info(Object message) {differentiatedLog(null, CATEGORY_FQCN, LocationAwareLogger.INFO_INT, message, null);
}public void warn(Object message) {differentiatedLog(null, CATEGORY_FQCN, LocationAwareLogger.WARN_INT, message, null);
}public void error(Object message) {differentiatedLog(null, CATEGORY_FQCN, LocationAwareLogger.ERROR_INT, message, null);
}public void fatal(Object message) {differentiatedLog(FATAL_MARKER, CATEGORY_FQCN, LocationAwareLogger.ERROR_INT, message, null);
}void differentiatedLog(Marker marker, String fqcn, int level, Object message, Throwable t) {String m = convertToString(message);if (locationAwareLogger != null) {locationAwareLogger.log(marker, fqcn, level, m, null, t);} else {switch (level) {case LocationAwareLogger.TRACE_INT:slf4jLogger.trace(marker, m, (Throwable) t);break;case LocationAwareLogger.DEBUG_INT:slf4jLogger.debug(marker, m, (Throwable) t);break;case LocationAwareLogger.INFO_INT:slf4jLogger.info(marker, m, (Throwable) t);break;case LocationAwareLogger.WARN_INT:slf4jLogger.warn(marker, m, (Throwable) t);break;case LocationAwareLogger.ERROR_INT:slf4jLogger.error(marker, m, (Throwable) t);break;}}
}
这些方法通过调用 differentiatedLog 方法将日志记录的请求转发给 SLF4J 的 Logger 实例。differentiatedLog 方法会根据不同的日志级别调用相应的 SLF4J 方法。
3、日志级别检查方法:
public boolean isDebugEnabled() {return slf4jLogger.isDebugEnabled();
}public boolean isInfoEnabled() {return slf4jLogger.isInfoEnabled();
}public boolean isWarnEnabled() {return slf4jLogger.isWarnEnabled();
}public boolean isErrorEnabled() {return slf4jLogger.isErrorEnabled();
}
这些方法直接委托给 SLF4J 的 Logger 实例,检查当前日志级别是否启用。
在上面的演示代码中main方法就是客户端,调用了log4j 1.x的打印日志方法,这里以info为例进行分析,debug和error方法的原理类似。
在获取Logger实例时,会实例化其父类Category。其中适配者slf4jLogger是使用的Logback实现类ch.qos.logback.classic.Logger。
【图】获取到了适配者slf4jLogger
通过上面分析可以看出,Category 类封装了日志打印相关的目标API,同时在这些API中将日志记录的请求委托给 SLF4J 的 Logger 实例,从而实现了适配器模式。这样,客户端可以继续使用 log4j 的 Category 类进行日志记录,而实际的日志记录工作由 SLF4J 完成。下图是log4j 1.x的info方法的调用栈,最终调用的是Logback的实现。
【图】调用Logback的方法打印日志
至此,log4j-over-slf4j 中实现的适配器模式原理分析完毕。
推荐阅读
关于Java日志框架系列,之前已经从使用和源码层面进行了介绍,具体内容参见:
【Java开发】SLF4J 门面日志框架原理分析
【Java开发】Log4j 2 与 SLF4J 互转的核心:log4j-slf4j-impl 和 log4j-to-slf4j
【Java开发】一文理清 Java 日志框架的来龙去脉
参考资料
[1]SLF4J Migrator: https://www.slf4j.org/migrator.html
[2]Bridging legacy APIs: https://www.slf4j.org/legacy.html
[3]log4j-over-slf4j: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-to-slf4j
[4]jcl-over-slf4j: https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j
[5]jul-to-slf4j: https://mvnrepository.com/artifact/org.slf4j/jul-to-slf4j
[6]log4j-to-slf4j: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-to-slf4j
更多内容,请关注公众号 程序员Ink
个人观点,仅供参考