前言
在软件开发过程中,日志记录是一项至关重要的任务,它不仅有助于开发人员调试和追踪问题,还能帮助运维团队监控系统健康状况。然而,不同的日志框架有着不同的API,这导致应用程序在切换日志框架时需要修改大量代码,增加了维护成本。为了解决这一问题,门面模式被引入到日志管理中,通过创建一个统一的接口来降低应用程序与底层日志框架之间的耦合度。本文将探讨两种常见的日志门面——JCL (Jakarta Commons Logging) 和 SLF4J (Simple Logging Facade for Java),并介绍它们是如何简化日志管理的。
一、日志门面概述
1. 门面模式(外观模式)
我们先谈一谈GoF23种设计模式其中之一。门面模式,也称为外观模式,其核心为:外部一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。外观模式主要体现了Java中的一种好的封装性。更简单的说,就是对外提供的接口要尽可能的简单。
2. 日志门面
每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加应用程序代码对日志框架的耦合性。为了解决这个问题,就是在日志框架和应用程序之间架设一个沟通的桥梁,对于应用程序来说,无论底层的日志框架如何改变,都不需要任何感知。只要门面服务做的足够好,随意换另外一个日志框架,应用程序不许哟啊修改任意一行代码,就可以直接上线。我们去餐厅吃饭,需要和前台进行一个沟通,日志门面就相当于这里的前台。
二、JCL
JCL全称为Jackarta Commons Logging,是Apache提供的一个通用日志API。用户可以自由选择第三方的日志组件作为具体实现,项log4j。,或者jdk自带的JUL,common-loggin会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。当然,common-loggin内部有一个Simple logger的简单实现,但是功能很弱。所以使用common-loggin,通常都是配合着log4j以及其他日志框架来使用。使用它的好处就是,代码依赖是common-loggin而非log4j的API,避免了和具体的日志API直接耦合,在有必要时,可以更改日志实现的第三方库。
JCL有两个基本的抽象类:
- Log:日志记录器
- LogFactory:日志工厂(负责创建Log实例。
1. JCL组件结构
2. JCL案例
首先,我们导入JCL的依赖:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
同时,导入junit测试后编写如下代码的程序:
@Test
public void test() {
Log log = LogFactory.getLog(JCLTest.class);
log.info("info信息");
}
运行结果如下图所示:
JCL使用原则:如果有log4j,优先使用log4j,如果没有任何第三方日志框架的时候,我们使用的就是JUL 。
三、SLF4J
1. SLF4J简介
简单日志门面SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交给其他日志框架,例如log4j和logback等。当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。所以我们可以得出SLF4J最重要的两个功能就是对于日志框架的绑定以及日志框架的桥接。
2. SLF4J桥接技术
通常,我们依赖的某些组件依赖于SLF4J以外的日志API。我们可能还假设这些组件在不久的将来不会切换到SLF4J。为了处理这种情况。SLF4J附带了几个桥接模块,这些模块会将对log4j、JCL和java.util.logging API的调用重定向行为,就好像是对SLF4J API进行的操作一样。
3. 快速入门
导入SLF4J相关依赖:
<!-- slf4j日志门面核心依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<!-- sl4j自带的简单日志实现 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.13</version>
</dependency>
编写如下代码:
Logger logger = LoggerFactory.getLogger(this.getClass());
String msg = "你好,世界!";
logger.info("message:{}", msg); // 动态输出字符串信息
补充:对于异常对象e的打印,我们不需要用{}来进行字符串拼接。示例:
Logger logger = LoggerFactory.getLogger(this.getClass());
try {
Class.forName("aaa");
} catch (ClassNotFoundException e) {
logger.error("具体错误是:", e);
}
运行结果如下:
补充:SLF4J对日志的级别划分为trace、debug、info、warn、error五个级别。
注意:在没有任何其他日志实现框架集成的基础上,SLF4J使用的是自带的框架slf4j-simple,slf4j-simple也必须以单独依赖的形式导入进来。
4. SLF4J集成日志实现
有以下三种情况对日志实现进行绑定:
4.1 集成nop
slf4j-nop 表示不记录日志,在我们使用slf4j-nop的时候,首先还是需要导入实现依赖:
<!-- sl4j的nop -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.13</version>
</dependency>
对于nop来说,控制台不会打印任何日志。
4.2 集成Logback
集成Logback同样需要导入依赖,其导入方式与相关测试的运行结果如下所示:
<!-- logback日志实现 logback-classic已经涵盖logback-core这个依赖了 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
4.3 集成Log4j
对于Log4j来说,其出现时间在SLF4J之前,所以SLF4J需要使用适配器集成Log4j。相关依赖与相关测试的结果如下所示:
<!-- 导入log4j适配器依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.13</version>
</dependency>
<!-- log4j依赖 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
4.4 集成JUL
与Log4j一样,JUL也需要导入相关的适配器依赖,由于JUL是Java自带的日志框架,所以导入适配器后不需要再导入其他依赖。相关依赖与相关测试的结果如下所示:
<!-- 导入jul适配器依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.13</version>
</dependency>
注意:以上四个集成前提是均需要先导入slf4j日志门面核心依赖即slf4j-api。
4.5 同时集成多个日志实现
当我们导入了多个日志实现依赖时,SLF4J会在控制台打印出有多个日志实现的警告信息。那么,在集成了多个日志实现的情况下,我们会优先使用哪一个日志实现呢?对于导入的多个日志实现依赖,哪一个日志实现依赖书写靠前,就使用哪一个日志实现,具体示例如下:
5. SLF4J桥接器的使用
通常,我们在项目开发中使用的日志组件依赖于日志记录 SLF4J 以外的 API,我们可以假设这些组件会在不久的将来切换到 SLF4J。在这种情况下,SLF4J 随附多个桥接模块重定向对 log4j 1.x、JCL 和 java.util.logging API 的调用,表现得好像它们是针对 SLF4J API 制作的。下图说明了这个想法(图片来自官网):
假设我们项目中使用的日志框架是Log4j,现在随着业务的变更,我们需要将日志框架转换成SLF4J + Logback 的一个日志组合,那么我们怎么才能在不惊动原先代码的情况下,对日志进行一个转换呢?我们需要将原型的Log4j依赖注释掉,导入SLF4J核心依赖、针对于Log4j的桥接器和Logback日志实现依赖,就能在不惊动原来代码的情况下,对日志进行一个转换了。
<!-- log4j依赖 -->
<!-- <dependency> -->
<!-- <groupId>log4j</groupId> -->
<!-- <artifactId>log4j</artifactId> -->
<!-- <version>1.2.12</version> -->
<!-- </dependency> -->
<!-- slf4j日志门面核心依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<!-- log4j桥接器 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>2.0.13</version>
</dependency>
<!-- logback日志实现 logback-classic已经涵盖logback-core这个依赖了 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
同样的一段代码,在转换前与转换后的日志输出格式如下图所示:
注意:适配器和桥接器不能一起导入。桥接器如果在适配器之前导入,则运行报错;桥接器如果在适配器之后导入,则不会执行桥接器(执行适配器),没有任何意义。
总结
通过使用这些日志门面,开发者可以在不影响现有代码的情况下轻松更换日志框架,极大地提高了应用的可维护性和可扩展性。总之,无论是选择 JCL 还是 SLF4J,都可以显著减少因日志框架变化带来的代码改动,提高开发效率,同时保持系统的稳定性和可维护性。