【Log】(二)Java 中的日志框架 JCL、SLF

【Log】(一)Java 中的日志框架 JUL、Log4j
【Log】(二)Java 中的日志框架 JCL、SLF
【Log】(三)Java 中的日志框架 logback、log4j2


前言

JUL 用起来简单、方便,不需要引入第三方依赖,适合小型应用。Log4j 同样适用起来简单,但它配置更为强大。那么,我们该如何选择这两个日志框架呢?其实,这根据我们项目需求而定的。

举个简单例子:在项目初期,功能简单,我们可以使用比较简单的日志框架 JUL。因为,它使用起来灵活,不需要导入第三方依赖。随着功能的日益完善,当初的日志框架无法满足现在的需求,那么就得进行日志的升级了,如:切换成 Log4j

如果从 JUL 切换成 Log4j,那么代码一定会受到影响,且之前记录的日志信息也得修改(改动大),这并不是我们程序员希望看到的。为了解决这一问题,Apache 组织站出来了,它将当时主流的日志框架(JULLog4j)统一一套 API。那么,后期在软件开发阶段需要关联一套统一的 API 就可以操作某一个日志实现框架的具体日志记录了。即:日志框架修改了,但这套 API 是没变的。而这套 API 就是指 JCL

1. JCL 学习

1.1 JCL 介绍

JCL:全称 Jakarta Common Logging ,是 Apache 提供的一个通用日志 API。

它为 “所有的 Java 日志实现”提供了一个统一的接口,它自身也提供了一个日志的实现,但功能非常弱(SimpleLog)。所以,一般不会单独使用它。它允许开发者使用不同的具体日志实现工具:Log4jJUL

JCL 有两个基本的抽象类:LogLogFactory(负责创建 Log)

1.2 快速入门

新建一个 Maven 工程,引入一个 commons 依赖,POM 文件:

<dependency>
	<groupId>commons-logging</groupId>
	<artifactId>commons-logging</artifactId>
	<version>1.2</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

1. 入门案例:

public class JclTest {

    @Test
    public void testQuick() {
        Log log = LogFactory.getLog(JclTest.class);
        log.info("info");
        log.error("error");
    }

}

运行后:
在这里插入图片描述
这种格式,由于没有导入日志的实现依赖,就使用默认的 JDK14 Logger(JUL)

2. 导入 Log4j:

<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

运行后:
在这里插入图片描述
弹出警告:Log4j 没有系统配置。来,咱们加上 log4j.properties 配置文件

# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=info,使用的 appender 是 console
log4j.rootLogger = trace,console

# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%p]%r  %l %d{yyyy-MM-dd HH:mm:ss} %m%n

运行后:
在这里插入图片描述
这种日志格式,就是使用了 log4j

看,修改日志的实现,不需要修改代码。这就是面向接口设计的理念。

3. 总结

我们为什么要使用日志门面:

  1. 面向接口开发,不再依赖具体的实现类,减少代码的耦合
  2. 项目通过导入不同的日志实现类,可以灵活地切换日志框架
  3. 统一 API,方便开发者学习
  4. 统一配置,便于项目日志的管理

1.3 JCL 原理

根据上面案例我们知道:当我们没有引入日志的实现的依赖时,它会使用日志的默认实现 JUL;但当我们引入了 log4j 后,自然就使用 log4j 日志。那么,jcl 到底是如何做到的呢?

1、 通过 LogFactory 动态地加载 Log 实现

在这里插入图片描述
接口 Log 有 4 个实现类:JDK14LoggerLog4jLoggerJdk13LumberjackLoggerSimpleLog

通过 LogFactory 动态地获取 Log(哪个实现类)

2、日志门面支持的日志实现数组

private static final String[] classesToDiscover = {
        "org.apache.commons.logging.impl.Log4JLogger",
        "org.apache.commons.logging.impl.Jdk14Logger",
        "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
        "org.apache.commons.logging.impl.SimpleLog"
};

注意:这个数组中的元素的顺序!!第一个是: Log4JLogger

3、获取具体日志的实现

for(int i=0; i<classesToDiscover.length && result == null; ++i) {
    result = createLogFromClass(classesToDiscover[i], logCategory, true);
}

循环 jcl 中已经实现了的 4 个类,然后调用方法 createLogFromClass()

重点看方法 createLogFromClass()

// 只看重点逻辑
private Log createLogFromClass(String logAdapterClassName,
                               String logCategory,
                               boolean affectState)
    throws LogConfigurationException {
    Class c;
    Log logAdapter = null;
    for (;;) {
	    try {
	        c = Class.forName(logAdapterClassName, true, currentCL);
	    } catch (NoClassDefFoundError e) {
	    	...
	    	break;
	    }
	    constructor = c.getConstructor(logConstructorSignature);
	    Object o = constructor.newInstance(params);
	    if (o instanceof Log) {
            logAdapterClass = c;
            logAdapter = (Log) o;
            break;
        }
    }
    return logAdapter;
}

这个方法的逻辑:通过 Class.forName(String className) 来加载 Log 的实现类(来自于 classesToDiscover 数组),如果此类存在,则通过反射创建其实例并返回;如果不存在,则直接返回。

再看看 for 循环中,如果返回的结果不为 null,则跳出循环,继续往下执行;否则,继续循环。

那么,当我们没有引入 log4j 的依赖时,也是存在 Log4JLogger 的(jcl 的实现类)。并且,它是位于要循环的数组中的第一个元素,但是,它并不会加载成功。因为 Log4JLogger 类中引入了 log4j 的依赖,依赖于 log4j。如:import org.apache.log4j.Logger; 所以,在调用 Class.forName(String className) 时会失败。然后继续循环,尝试加载第二个元素 Jdk14Logger,这个依赖就存在 jcl 依赖中,所以,它就会加载成功。

没有引入 log4j 依赖时,org.apache.commons.logging.impl.Log4JLogger 类:
在这里插入图片描述
那么,当我们引入 log4j 依赖时,org.apache.commons.logging.impl.Log4JLogger 类就显得可用了,所以,它会一次加载成功,不用循环数组

JCL 只支持以上4种实现类,如果还想支持其它三方日志框架,就得修改 jcl 源码了。难道使用起来这么困难吗?其实不然,早在2014年,jcl 就被 apache 组织给淘汰了,后期就推出了更加优秀的日志门面框架。

2. SLF 学习

2.1 SLF 介绍

SLF 主要是为了给 java 日志访问提供一套标准、规范的 API 框架。其主要yiyi在于提供接口,具体的实现可以交由其它日志框架。如:logbacklog4jlog4j2

当然,SLF 自己也提供了较为简单的实现,但一般很少用。对于一般的 java 项目而言,日志框架会选择 slf4j-api 作为门面,配上具体的实现框架(log4jlogback 等。即: slf + logbackslf + log4j2),中间使用桥接器完成桥接。SLF 是目前市面上最流行的日志门面。现在的项目中,基本上都是使用 SLF 作为日志系统。

SLF 日志门面主要提供两大功能:

  1. 日志框架的绑定
  2. 日志框架的桥接

2.2 快速入门

新建一个 Maven 工程,引入一个 slf4j-api 依赖,POM 文件:

<!--slf日志门面-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.26</version>
</dependency>
<!--slf内置简单的实现-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>1.7.21</version>
</dependency>

入门案例:

public class SlfTest {

    public static final Logger LOGGER = LoggerFactory.getLogger(SlfTest.class);

    @Test
    public void testQuick() {
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");

		// 使用占位符
        String name = "zzc";
        Integer age = 24;
        LOGGER.info("用户信息:{}, {}", name, age);
    }

}

运行后,结果如下:
在这里插入图片描述

2.3 SLF 的日志绑定

上面的案例是使用了 SLF 绑定了 简单的日志实现。那么,SLF 是如何绑定其它的主流日志框架呢?SLF4J 官网的用户手册 上有详细的介绍:
在这里插入图片描述
上面这副图就表明了 SLF 是如何绑定其它的主流日志框架。

如果 Java 应用中需要使用日志记录的话,则首先需要引入 slf4j-api.jar 的依赖,统一日志接口。并且,它需要引入具体的实现,共有三种情况:

  1. 没有引入具体的实现:那么,日志功能将不能起作用
  2. 引入了一类实现(上图的蓝色框:slf4j-logbackslf4j-simpleslf4j-nop),由于它们的设计比 SLF 要晚,所以默认遵循 SLF 的规范,只需要导入它们的依赖就可使用
  3. 引入了另一类实现(log4jjdk14),它们的设计比 SLF 要早,在设计之初,并没有遵循 SLF 的规范,无法直接进行绑定。所以,需要添加一个适配层 Adaptation layer。通过适配器进行适配,从而间接地遵循了 SLF 的规范。

使用 SLF 的日志绑定流程:

1、添加 slf4j-api 的依赖
2、使用 slf4j 的 API 在项目中进行统一的日志记录
3、 绑定具体的日志实现

1、绑定了已经实现sfl4j的日志框架,直接添加对应的依赖
2、绑定了没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖

4、slf4j 有且仅有一个日志实现框架的绑定(如果出现多个,默认使用第一个依赖日志实现)

接下来就对剩下的日志实现进行案例演示

logback:

<!--logback-->
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>1.2.3</version>
 </dependency>

logback-classic 包含了 logback-core 的依赖。

【注意】:引入了 logback-classic 的依赖后,得去掉的 slf4j-simple 的依赖(入门案例时引入的)

入门案例代码不变,运行后:
在这里插入图片描述
nop:

slf4j-nop 是日志的开关,引入它后,表示系统关闭日志系统

<!-- nop 日志的开关-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-nop</artifactId>
  <version>1.7.2</version>
</dependency>

运行后,控制台并不会输出日志信息

log4j:

<!--绑定log4j实现,需要导入适配器-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

需要引入 log4j.properties,这里就拿之前的就行。

运行结果如下:
在这里插入图片描述
jdk14:

<!--绑定jdk14实现-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-jdk14</artifactId>
  <version>1.7.25</version>
</dependency>

运行结果如下:
在这里插入图片描述

2.4 SLF 的日志绑定原理

LoggerFactoryLogger 是根据 LoggerFactory 获取的实例

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

LoggerFactory#getILoggerFactory():获取具体的工厂实例

public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == 0) {
    	//...
        performInitialization();
    }

    switch(INITIALIZATION_STATE) {
    //...
    case 3:
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    //...
}

LoggerFactory#bind():绑定所有的实现类

private static final void bind() {
    String msg;
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        if (!isAndroid()) {
        	// 1.找出所有的日志实现
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            // 2.如果实现类大于1,则会记录下来
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
		
		// 3.进行日志实现类org.slf4j.impl.StaticLoggerBinder.class加载
        StaticLoggerBinder.getSingleton();
        INITIALIZATION_STATE = 3;
        reportActualBinding(staticLoggerBinderPathSet);
        //...
    } catch (NoClassDefFoundError var2) {
        // ...
    }
}

说明:

  1. findPossibleStaticLoggerBinderPathSet();:通过类加载器在类路径下(包括 jar 包)查找包含org.slf4j.impl.StaticLoggerBinder.class 类的路径。即:日志实现的 jar 就包含
  2. reportMultipleBindingAmbiguity():如果引入了多于1个的日志实现,则会记录下来
  3. StaticLoggerBinder.getSingleton();:调用此方法时,会导致类加载,并返回日志实现的单例实例(即使引入了多个日志实现的类,那也只会返回同一个实例)

2.5 SLF 的日志桥接器

Java 工程中使用的日志框架依赖于 SLF 以外的 API。为了使日志框架看上去是 SLF API 的实现,将使用 SLF 附带了几个桥接模块。这些模块将对 Log4j、jcl 的调用重定向,就好像它们是对 SLF API 一样。

桥接解决的项目中日志的遗留问题,当系统中存在之前的日志 API,可以通过桥接转换到 SLF 的实现。

步骤:

  1. 先去除之前老的日志框架的依赖
  2. 添加 SLF 提供的桥接组件
  3. 为项目添加 SLF 的具体实现

案例 ------ log4j:

假若之前,工程中使用的是 log4j

<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

然后,在类路径下添加:log4j.properties

添加测试代码:

public class Log4jTest {

    @Test
    public void testQuick() {
        Logger logger = Logger.getLogger(com.zzc.log.log4j.Log4jTest.class);
        logger.fatal("fatal");
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }

}

运行结果后:
在这里插入图片描述
现在,我需要在工程中使用 SLF API(SLF + logback),而不使用 log4j

引入依赖:

<!--slf日志门面-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.26</version>
</dependency>
<!--logback-->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>
<!--配置log4j的桥接器-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>log4j-over-slf4j</artifactId>
  <version>1.7.24</version>
</dependency>

测试代码不变

运行结果如下:
在这里插入图片描述
输出格式:logback(用的它的实现)

【注意】:桥接器和适配器不能一起使用。否则,就是出现 栈溢出。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值