04、slf4j(日志门面)
@[toc]
前言
本篇博客主要介绍现如今主流的日志门面技术slf4j
,Springboot
中推荐使用该日志门面技术。其他日志框架内容可见日志专栏。
所有博客文件目录索引(包含日志框架系列学习):博客目录索引(持续更新)
一、认识slf4j
1.1、slf4j概述
用户手册:http://www.slf4j.org/manual.html
slf4j
(Simple Logging Facade For Java):为所有的日志框架提供了一套标准、规范的API框架,主要是提供了接口,具体的实现交由对应的日志框架,例如Log4j
、Logback
、Log4j2
等。其自己本身也提供了简单的日志实现(slf4j-simple
)。
现如今对于一般的Java项目而言,日志框架会选择slf4j-api
作为门面,配置上具体的实现框架,中间使用桥接器来完成桥接。
介绍其中两个类:日志实例Logger
以及LogFactory.getLogger()
(工厂)获取日志实例。
1.2、第三方jar包
想要使用slf4j
日志门面,需要使用第三方jar
包,下面为pom.xml
的对应坐标:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
若是想要之后切换日志框架,最好先引入指定版本的slf4j-api
(尽管之后引入的对应的slf4j
日志实现框架中有对应api
依赖),来进行统一的API管理。
1.3、切换日志框架详略图
我们去到slf4j官网的用户手册网页即可查看到下图:
application
下面的SLF4J API
表示slf4j的日志门面,包含三种情况:
- ①若是只导入
slf4j
日志门面没有导入对应的日志实现框架,那么日志功能将会是默认关闭的,不会进行日志输出的。 - ②蓝色图里
Logback
、slf4j-simple
、slf4j-nop
出来的比较晚就遵循了slf4j
的API
规范,也就是说只要导入对应的实现就默认实现了对应的接口,来实现开发。 - ③对于中间两个日志实现框架
log4j
(slf4j-log4j12
)、JUL
(slf4j-jdk14
)由于出现的比slf4j
早,所以就没有遵循slf4j
的接口规范,所以无法进行直接绑定,中间需要加一个适配层(Adaptation layer
),通过对应的适配器来适配具体的日志实现框架,其对应的适配器其实就间接的实现了slf4j-api
的接口规范。
注意:在图中对于logback
需要引入两个jar
包,不过在maven
中有一个传递的思想,当配置logback-classic
时就会默认传递core
信息,所以我们只需要引入logback-classic
的jar
包即可。
1.4、相关注意点
在使用slf4j日志门面的过程中,若是引入了两个日志实现框架会报以下错误,并会默认实现第一个引入的日志实现:
- 这里是同时配置
simple
以及logback
情况
注意:以pom.xml
中配置顺序有关!!!
二、实际应用
2.1、配合自身简单日志实现(slf4j-simple)
若想使用自身的日志实现框架,需要引入第三方jar
包slf4j-simple
(slf4j自带实现类):
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
</dependency>
- 其中该坐标包含了对应的
slf4j-api
的依赖,可以不用手动导入slf4j-api
。
测试程序:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//获取Logger实例
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、打印日志记录
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符输出
String name = "changlu";
int age = 20;
LOGGER.info("报错,name:{},age:{}",name,age);
//3、打印堆栈信息
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("报错",e);
}
}
}
- 默认日志等级为
INFO
,能够实现占位符输出,并且可以在日志等级方法中传入异常实例,来打印对应的日志信息。
注意点:若是我们只使用日志门面而没有导入指定的日志实现框架,调用Logger
实例并调用日志方法会出现以下错误:
2.2、配置logback日志实现
引入logback-classic
的jar包,其中包含有slf4j-api
以及logback-core
的依赖,所以只需要引入该依赖即可:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
测试程序:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//获取Logger实例
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、打印日志记录
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符输出
String name = "changlu";
int age = 20;
LOGGER.info("报错,name:{},age:{}",name,age);
//3、打印堆栈信息
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("报错",e);
}
}
}
2.3、配置Log4j日志实现(需适配器)
①首先添加日志框架实现依赖
之前在1.3中介绍,对于Log4j
、JUL
这些比较早出现的日志实现框架需要有对应的适配层,在这里我们引入对应的适配器slf4j-log412
的依赖坐标:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
slf4j-log4j12
坐标中实际就包含了Log4j
以及slf4j-api
依赖,所以我们添加该坐标即可。
②添加log4j.properties
配置文件
# rootLogger日志等级为trace,输出到屏幕上
log4j.rootLogger = trace,console
# console
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern= [%-5p]%r %l %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
测试程序:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//获取Logger实例
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、打印日志记录
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符输出
String name = "changlu";
int age = 20;
LOGGER.info("报错,name:{},age:{}",name,age);
//3、打印堆栈信息
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("报错",e);
}
}
}
2.4、配置JUL日志实现(需适配器)
对于slf4j
日志门面实现JUL
日志框架需要使用是适配器来实现slf4
j的日志接口,我们直接添加对应适配器依赖如下:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.5.6</version>
</dependency>
JUL
是我们jdk
自带的日志框架,所以不需要额外引入jar包,引入slf4j-jdk14
坐标,其中就包含了slf4j-api
的依赖,所以我们只需要引入一个坐标即可。
测试程序:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//获取Logger实例
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、打印日志记录
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符输出
String name = "changlu";
int age = 20;
LOGGER.info("报错,name:{},age:{}",name,age);
//3、打印堆栈信息
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("报错",e);
}
}
}
2.4、添加slf4j-nop依赖(日志开关)
当添加了slf4j-nop
坐标后,其相当于一个日志开关,导入实现以后就不会使用任何实现框架:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.27</version>
</dependency>
测试程序:
public class LogTest {
//获取Logger实例
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
}
}
- 默认就关闭了
slf4j
的日志框架使用。
三、原理分析
3.1、初始绑定日志实现原理
在slf4j-api
中我们通常使用下面的方法来获取logger
实例:
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
获得的实例实际上跟我们导入jar
包有关,那么它是如何进行初始配置的呢?看下面源码:
public final class LoggerFactory {
public static Logger getLogger(Class<?> clazz) {
//1、调用一个重载方法,传入类名
Logger logger = getLogger(clazz.getName());//见2
if (DETECT_LOGGER_NAME_MISMATCH) {
Class<?> autoComputedCallingClass = Util.getCallingClass();
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
autoComputedCallingClass.getName()));
Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
}
}
return logger;
}
//2、根据类名来获取logger实例
public static Logger getLogger(String name) {
//获取ILoggerFactory接口的实现类(接口方法是getLogger())
ILoggerFactory iLoggerFactory = getILoggerFactory();//见3
return iLoggerFactory.getLogger(name);
}
//3、获取ILoggerFactory的实例
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
//执行初始化方法
performInitialization();//见4
}
}
}
...
}
//4、执行初始化操作
private final static void performInitialization() {
bind();//见5
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
//5、绑定操作
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
//下面两行比较关键,这行是查找可能的静态日志执行器路径使用Set来接收
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();//见6
//查看set中是否超过1个路径,若是则进行窗口输出提示信息
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);//见7
}
//
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
....
}
//6、这里是查找有关org/slf4j/impl/StaticLoggerBinder.class路径,都放置到set中返回
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
//从类加载器中进行查找是否有org/slf4j/impl/StaticLoggerBinder.class路径
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
//这里依旧是根据查找到的路径继续往下延伸查找并添加到Set中去
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
}
52行的查找
org/slf4j/impl/StaticLoggerBinder.class
路径操作存放到Set中返回。
看到上面方法6中是不是有个疑惑,查找org/slf4j/impl/StaticLoggerBinder.class
这个相关路径有什么用,与我们要加载对应的日志实现有什么关系呢?
我们在本次源码过程中引入两个日志实现框架slf4j-log4j12
、slf4j-jdk14
(这两个都是slf4j
为较早出现的日志设置的适配器),引入jar包之后,我们尝试搜索一下StaticLoggerBinder
这个类:
好家伙原来slf4j
实现的相关适配器的名称都叫StaticLoggerBinder
啊,我们继续看下去,看下jdk14的吧(就是JUL):
该工厂类中的实例loggerFactory
是获取了一个JDK14的工厂类,那么我们继续看向适配器中内容:
重写了getLogger()
方法,其中实例化了JUL
的logger
实例,调用有参构造传入并且创建了一个JDK14
的适配器,我们再看下这个适配器中都做了些什么:
好家伙其中包含了各个日志等级的方法,其中都包含了JUL
的日志操作。
53行中调用的
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
来报告多个绑定日志实现框架
public final class LoggerFactory {
//7、来进行报告含有多个日志框架的路径
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
//该方法判断是否set中数量>1
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {//见8
//若是超过1个的话就会报该问题(就是我们之前1.4中的注意点报错)
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
//8、判断set中的容量大小是否>1
private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
return binderPathSet.size() > 1;
}
}
该方法就是来检测是否有多个日志实现框架导入,若是有则报出提示信息。
四、桥接旧的日志实现框架
介绍桥接器
直接举场景来说明:对于一些老项目直接使用的是Log4j
或JUL
的日志实现框架,并没有使用到日志门面来进行管理日志框架,当项目需要迭代升级时,我们想把原先的日志实现框架切换为logback
,此时会出现一个问题,若是我们直接将对应的日志jar包更改为logback
,那么项目中会出现大量报错,因为原先引入的包是import org.apache.log4j.Logger;
,此时就会出现问题,我们需要重新修改大量的代码,需要耗费大量的时间与精力。
解决方案:在slf4j
中可以使用桥接器从而让我们不用修改一行代码实现日志框架的切换。在slf4j中附带了几个桥接木块,这些模块对于log4j
、JCL
和JUL
的API
调用重定向(其实就是全限定名与原来的完全相同)。
下图包含了对应的解决方案:
- 可用
log4j-over-slf4j.jar
替代Log4j
4.1、log4j-over-slf4j桥接器使用
解决过程
问题描述
模拟场景:老项目直接使用的是org.apache.log4j.Logger
,现今项目迭代升级,需要使用Logback
日志框架。
我们首先将log4j
jar包移除,之后引入logback-classic
依赖坐标,此时就会出现下方情况:
解决方案:使用桥接器
log4j-over-slf4j
引入坐标依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
- 不用修改任何代码即可替换日志框架。
原理分析
- 从导入的包来看,其中的全限定类名与原本
Log4j
的一毛一样,接着看是如何达到无缝衔接的。
同样是下方的获取logger
实例方法:
import org.apache.log4j.Logger;
public class LogTest {
public static final Logger LOGGER = Logger.getLogger(LogTest.class);
}
查看源码:
//即引入的log4j-over-slf4j坐标
package org.apache.log4j;
public class Logger extends Category {
//1、
public static Logger getLogger(Class clazz) {
return getLogger(clazz.getName());//见2
}
//2、
public static Logger getLogger(String name) {
return Log4jLoggerFactory.getLogger(name);//见3
}
//4、有参构造
protected Logger(String name) {
super(name);//调用的是Category的有参构造 去5
}
}
//log4j的工厂类
class Log4jLoggerFactory {
//3、获取logger实例
public static Logger getLogger(String name) {
Logger instance = (Logger)log4jLoggers.get(name);
if (instance != null) {
return instance;
} else {
//重要的点来了:注意看这个方法
Logger newInstance = new Logger(name);//回到上面的Logger类中的4方法
Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
}
//Loger类的父类
public class Category {
protected Logger slf4jLogger;
//5、有参构造
Category(String name) {
this.name = name;
//注意这个方法,LoggerFactory.getLogger()获取的是slf4j的对应Logger
this.slf4jLogger = LoggerFactory.getLogger(name);
if (this.slf4jLogger instanceof LocationAwareLogger) {
this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
}
}
}
- 简单来说就是
slf4j
的开发者提供了一个与Log4j
的全限定类名相同的一个包,其中的方法名称与Log4j
的都相同,在getLogger()
方法中实际获取到了slf4j
对应的Logger
实例。
如下图:log4j
是灰色表示的是移除掉,使用log4j-over-slf4j
4.2、jul、jcl桥接器
当使用单独的日志实现框架想要替换成如logback
日志框架时可使用对应的桥接器来进行替代:
<!-- jul -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
替换了之后别忘了引入对应要替换的日志实现框架。
三个slf4j日志实现框架与桥接器不能同时使用
以下的jar
包不能同时出现:
log4j-over-slf4j.jar
(桥接器)和slf4j-log4j12.jar
jcl-over-slf4j.jar
和slf4j-jcl.jar
jul-to-slf4j.jar
和slf4j-jdk14.jar
为什么不能同时出现呢?我们借第一组进行查看:
①首先看下相应的依赖导入
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
log4j-over-slf4j
坐标依赖
slf4j-log4j12
坐标依赖,注意该包的全限定类名与log4j
的权限定类名一致。
②我们运行下程序查看一下
import org.apache.log4j.Logger;
public class LogTest {
//获取Logger实例
public static final Logger LOGGER = Logger.getLogger(LogTest.class);
public static void main(String[] args) {
LOGGER.error("error");
}
}
为什么会出现这种情况呢,看下面原理图一下子懂了:
- 其实说白了就是
log4j
与log4j-over-slf4j
的权限定包名都是相同的,当在slf4j-log4j12
的适配器中若是找到log4j-over-slf4j
中的log4j
时此时就会出现无限死循环,也就导致栈溢出了。
前后引入jar包位置改变不会报错
不过我发现了一个问题:如果pom.xml
中的坐标前后位置,两个jar
包都导入了也不会报错
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
- 先引入
slf4j-log4j12
再引入log4j-over-slf4j
运行就不会报错,可能是在slf4j-log4j12
的适配器中找Logger
时,pom.xml
中log4j
首先被加载到了所以就不会报错了。
说明:尽管这样引入不会报错,我们也一定不要这样子引入桥接器与slf4j
的日志实现框架,这样也没必要。
总结
1、对于slf4j
切换日志框架我们实际上就只需要引入slf4j
提供的各个日志实现依赖即可(对应坐标其中包含了slf-api
以及对应实现框架依赖)。
2、对于一开始就没有进行使用日志门面而只是单单使用日志框架的项目,若是想要不修改代码进行切换日志框架,我们就要考虑使用slf4j
桥接器来进行日志框架切换。
3、slf4j
提供的日志框架实现尽量不要与桥接器同时使用,否则极有可能会报错!
参考资料
[1] 视频:2020年Java进阶教程,全面学习多种java日志框架
- 点赞
- 收藏
- 关注作者
评论(0)