三、原理分析
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日志框架。
我们首先将log4jjar包移除,之后引入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的权限定类名一致。
②我们运行下程序查看一下
mport 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提供的日志框架实现尽量不要与桥接器同时使用,否则极有可能会报错!