【Log】(一)Java 中的日志框架 JUL、Log4j
【Log】(二)Java 中的日志框架 JCL、SLF
【Log】(三)Java 中的日志框架 logback、log4j2
前言
JUL
用起来简单、方便,不需要引入第三方依赖,适合小型应用。Log4j
同样适用起来简单,但它配置更为强大。那么,我们该如何选择这两个日志框架呢?其实,这根据我们项目需求而定的。
举个简单例子:在项目初期,功能简单,我们可以使用比较简单的日志框架 JUL
。因为,它使用起来灵活,不需要导入第三方依赖。随着功能的日益完善,当初的日志框架无法满足现在的需求,那么就得进行日志的升级了,如:切换成 Log4j
。
如果从 JUL
切换成 Log4j
,那么代码一定会受到影响,且之前记录的日志信息也得修改(改动大),这并不是我们程序员希望看到的。为了解决这一问题,Apache 组织站出来了,它将当时主流的日志框架(JUL
、Log4j
)统一一套 API。那么,后期在软件开发阶段需要关联一套统一的 API 就可以操作某一个日志实现框架的具体日志记录了。即:日志框架修改了,但这套 API 是没变的。而这套 API 就是指 JCL
。
1. JCL 学习
1.1 JCL 介绍
JCL
:全称 Jakarta Common Logging
,是 Apache 提供的一个通用日志 API。
它为 “所有的 Java 日志实现”提供了一个统一的接口,它自身也提供了一个日志的实现,但功能非常弱(SimpleLog
)。所以,一般不会单独使用它。它允许开发者使用不同的具体日志实现工具:Log4j
、JUL
。
JCL
有两个基本的抽象类:Log
、LogFactory(负责创建 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. 总结
我们为什么要使用日志门面:
- 面向接口开发,不再依赖具体的实现类,减少代码的耦合
- 项目通过导入不同的日志实现类,可以灵活地切换日志框架
- 统一 API,方便开发者学习
- 统一配置,便于项目日志的管理
1.3 JCL 原理
根据上面案例我们知道:当我们没有引入日志的实现的依赖时,它会使用日志的默认实现 JUL
;但当我们引入了 log4j
后,自然就使用 log4j
日志。那么,jcl
到底是如何做到的呢?
1、 通过 LogFactory
动态地加载 Log 实现
接口 Log
有 4 个实现类:JDK14Logger
、Log4jLogger
、Jdk13LumberjackLogger
、SimpleLog
通过 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在于提供接口,具体的实现可以交由其它日志框架。如:logback
、log4j
、log4j2
。
当然,SLF
自己也提供了较为简单的实现,但一般很少用。对于一般的 java 项目而言,日志框架会选择 slf4j-api
作为门面,配上具体的实现框架(log4j
、logback
等。即: slf + logback
、slf + log4j2
),中间使用桥接器完成桥接。SLF
是目前市面上最流行的日志门面。现在的项目中,基本上都是使用 SLF
作为日志系统。
SLF
日志门面主要提供两大功能:
- 日志框架的绑定
- 日志框架的桥接
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
的依赖,统一日志接口。并且,它需要引入具体的实现,共有三种情况:
- 没有引入具体的实现:那么,日志功能将不能起作用
- 引入了一类实现(上图的蓝色框:
slf4j-logback
、slf4j-simple
、slf4j-nop
),由于它们的设计比SLF
要晚,所以默认遵循SLF
的规范,只需要导入它们的依赖就可使用 - 引入了另一类实现(
log4j
、jdk14
),它们的设计比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 的日志绑定原理
LoggerFactory
:Logger
是根据 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) {
// ...
}
}
说明:
findPossibleStaticLoggerBinderPathSet();
:通过类加载器在类路径下(包括 jar 包)查找包含org.slf4j.impl.StaticLoggerBinder.class
类的路径。即:日志实现的 jar 就包含reportMultipleBindingAmbiguity()
:如果引入了多于1个的日志实现,则会记录下来StaticLoggerBinder.getSingleton();
:调用此方法时,会导致类加载,并返回日志实现的单例实例(即使引入了多个日志实现的类,那也只会返回同一个实例)。
2.5 SLF 的日志桥接器
Java 工程中使用的日志框架依赖于 SLF 以外的 API。为了使日志框架看上去是 SLF API 的实现,将使用 SLF 附带了几个桥接模块。这些模块将对 Log4j、jcl 的调用重定向,就好像它们是对 SLF API 一样。
桥接解决的项目中日志的遗留问题,当系统中存在之前的日志 API,可以通过桥接转换到 SLF 的实现。
步骤:
- 先去除之前老的日志框架的依赖
- 添加 SLF 提供的桥接组件
- 为项目添加 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
(用的它的实现)
【注意】:桥接器和适配器不能一起使用。否则,就是出现 栈溢出。