前言
用了这么长时间的日志框架,有时候用得很顺利,有的时候又很迷惑,比如在项目中引入一个新的jar包,会出现日志框架冲突的时候,要不就是日志莫名的打印不出来,要不就是项目启动异常。今天花点时间来整理,梳理一下这些日志框架之间的关系。
日志框架概览
目前使用比较广泛的日志接口是common-logging和slf4j,common-logging是apache提供的一个通用的日志接口,slf4j是log4j的作者创作的日志接口。
小故事:apache说服log4j以及其他的日志来按照commons-logging的标准编写,但是由于commons-logging的类加载有点问题,log4j的作者就创作了slf4j。至于到底使用哪个,由用户来决定。
所为日志接口,就是common-logging或者slf4j只定义了日志的接口,而日志输出的具体实现,交给实现了日志接口的框架,比如log4j、logback等。
slf4j日志接口
slf4j全称为Simple Logging Facade for JAVA,java简单日志门面,这是从其他的博主文章中摘录出来的,我借用过来记录一下:
从这张图中就能看的很清楚,不同的日志框架(包括:log4j、logback等)的基础是slf4j,也就是说slf4j是一个日志门面,slf4j只是提供了一套标准的日志接口,没有具体的实现,它只提供了一个slf4j-api-*.jar包,负责日志实现的工作交给了各个日志框架。各个日志框架在实现的时候,又有点区别,一个明显的区别就是是否需要引入一个连接层jar包,比如log4j日志框架,需要引入一个连接层slf4j-log412.jar,而像logback日志框架,就不需要引入一个连接层jar,引入和不引入连接层实质上的区别是什么呢?我们后面在分析。
参照这个图,我们就清楚的知道,如果在项目中使用log4j作为日志框架,就需要引入3个jar包
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
如果是log4j2日志框架,就回引入不同的jar,毕竟log4j2和log4j架构还是有区别的。
如果是使用logback作为日志框架,需要引入下面的jar包,没有什么中间的连接层jar包。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
使用slf4j的常见代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestLog {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public static void main(String[] s){
logger.info("test");
}
}
slf4j查找日志实现框架的过程,以logback日志框架为例进行简单的分析:
slf4j正式加载日志框架的动作是在类实例化完成的最后阶段,也就是给下面这条语句赋值的时候:
private Logger logger = LoggerFactory.getLogger(this.getClass());
LoggerFactory是slf4j-api 包中的类,在这个LoggerFactory里有几处关键的地方:
package org.slf4j;
public final class LoggerFactory {
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
public static Logger getLogger(String name) {
//③创建Logger工厂
ILoggerFactory iLoggerFactory = getILoggerFactory();
//⑤创建Logger
return iLoggerFactory.getLogger(name);
}
private static Set findPossibleStaticLoggerBinderPathSet() {
LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
//①加载指定的binder
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
//省略代码
} catch (IOException var4) {
Util.report("Error getting resources from path", var4);
}
return staticLoggerBinderPathSet;
}
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == 0) {
INITIALIZATION_STATE = 1;
performInitialization();
}
switch(INITIALIZATION_STATE) {
case 1:
return TEMP_FACTORY;
case 2:
throw new IllegalStateException("org.slf4j.LoggerFactory could not be successfully initialized. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
case 3:
//②加载日志工厂类
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case 4:
return NOP_FALLBACK_FACTORY;
default:
throw new IllegalStateException("Unreachable code");
}
}
}
上面3个方法的调用顺序是getLogger()->getILoggerFactory()->findPossibleStaticLoggerBinderPathSet()
先看看①处代码,加载了一个指定的Binder类StaticLoggerBinder.class,这个类有两个特点,
- 第一个特点是,这个类显然是写死在代码里面的,
- 第二个特点是,这个类不是slf4j-api包里面的类,
那这个类在哪儿呢?这就得在logback的jar包中去发现了,看看下面这张截图
这就很明显了,这个StaticLoggerBinder类成了slf4j和logback日志框架的一个桥梁,这个Binder类做了什么事情,不是现在我们关注的重点,
接着看②处代码,StaticLoggerBinder.getSingleton()获取了StaticLoggerBinder的一个实现单例,getLoggerFactory做了什么事情,看看代码
package org.slf4j.impl;
public class StaticLoggerBinder implements LoggerFactoryBinder {
private LoggerContext defaultLoggerContext = new LoggerContext();
//④返回Logger工厂类
public ILoggerFactory getLoggerFactory() {
if (!this.initialized) {
return this.defaultLoggerContext;
} else if (this.contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException("contextSelector cannot be null. See also http://logback.qos.ch/codes.html#null_CS");
} else {
return this.contextSelectorBinder.getContextSelector().getLoggerContext();
}
}
void init() {
try {
try {
//⑥加载日志的配置文件
(new ContextInitializer(this.defaultLoggerContext)).autoConfig();
} catch (JoranException var2) {
Util.report("Failed to auto configure default logger context", var2);
}
//省略代码
} catch (Throwable var3) {
Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", var3);
}
}
}
④代码说明:StaticLoggerBinder类中getLoggerFactory方法,就是返回了一个LoggerContext,这个是什么东西,接着看代码
package ch.qos.logback.classic;
import org.slf4j.ILoggerFactory;
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {
}
看到这个类的定义就知道了,LoggerContext是处于logback-classic.jar包中的一个ILoggerFactory的实现类,ILoggerFactory是slf4j-api中定义的接口
回头看看上面③处的代码,LoggerFactory就是这样被创建出来的,⑤处的代码就是调用ch.qos.logback.classic.LoggerContext.getLogger()方法了,这就是logback要完成的事情了。
顺便说一下日志配置文件logback.xml在哪儿被加载的?代码⑥处,也是在StaticLoggerBinder类中完成的,ContextInitializer().autoConfig()加载配置文件logback.xml
从上面的分析看得出StaticLoggerBinder至少做了两件重要的事情:
- 创建ILoggerFactory的实现类LoggerContext
- 加载日志框架的配置文件logback.xml
SLF4J 会在编译时会绑定org.slf4j.impl.StaticLoggerBinder,该类里面实现对具体日志方案的绑定接入。任何一种基于slf4j 的实现都要有一个这个类。
log4j日志框架也是一样的道理,它也实现了这个StaticLoggerBinder类,从上面的我们讲到log4j需要引入的jar包就知道,引入了slf4j-log4j12这个包,这个包是干什么的呢?我们不在此处分析,看图:
虽然不知道slf4j-log4j12这个包是干什么的,但是现在至少分析出来可以得出结论,没有这个包,StaticLoggerBinder类就缺失,log4j就不能和slf4j集成。
common-logging日志接口
使用common-logging的常见代码
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class Test {
private Log logger = LogFactory.getLog(this.getClass());
public static void main(String[] s){
logger.info("test");
}
}
动态查找原理:
Log 是一个接口声明。LogFactory 的内部会去装载具体的日志系统,并获得实现该Log 接口的实现类。LogFactory 内部装载日志系统的流程如下:
- 首先,寻找org.apache.commons.logging.LogFactory 属性配置。
- 否则,利用JDK1.3 开始提供的service 发现机制,会扫描classpah 下的META-INF/services/org.apache.commons.logging.LogFactory文件,若找到则装载里面的配置,使用里面的配置。
- 否则,从Classpath 里寻找commons-logging.properties ,找到则根据里面的配置加载。
- 否则,使用默认的配置:如果能找到Log4j 则默认使用log4j 实现,如果没有则使用JDK14Logger 实现,再没有则使用commons-logging 内部提供的SimpleLog 实现。
从上述加载流程来看,只要引入了log4j 并在classpath 配置了log4j.xml ,则commons-logging 就会使log4j 使用正常,而代码里不需要依赖任何log4j 的代码。
slf4j与common-logging比较
- common-logging通过动态查找的机制,在程序运行时自动找出真正使用的日志库。由于它使用了ClassLoader寻找和载入底层的日志库, 导致了象OSGI这样的框架无法正常工作,因为OSGI的不同的插件使用自己的ClassLoader。 OSGI的这种机制保证了插件互相独立,然而却使Apache Common-Logging无法工作。
- slf4j在编译时静态绑定真正的Log库,因此可以再OSGI中使用。另外,SLF4J 支持参数化的log字符串,避免了之前为了减少字符串拼接的性能损耗而不得不写的if(logger.isDebugEnable()),现在你可以直接写:logger.debug(“current user is: {}”, user)。拼装消息被推迟到了它能够确定是不是要显示这条消息的时候,但是获取参数的代价并没有幸免。
日志框架切换
现实情况,假如我们现在的项目,正在使用的是slf4j日志接口,日志实现框架是logback,运行一段时间之后,引入了一个其他组件,这个组件使用的是common-logging作为日志接口,日志实现框架是log4j,一个项目中不能使用两套日志框架,必须得保留一个,logback作为项目目前使用的框架,优先考虑保留,自然就得把log4j排除掉,这个时候怎么实现呢?如果强行把common-logging的jar包和log4j的jar包排除,程序启动肯定是会报错的。
解决的方案需要引入一种桥接模式,关键在于一个jar包jcl-over-slf4j.jar,就是commons-logging通过jcl-over-slf4j来选择slf4j作为底层的日志输出对象,而slf4j又选择logback来作为底层的日志输出对象。几个关键步骤:
- 引入jcl-over-slf4j的jar包
- 去掉commons-logging的jar包,使用jcl-over-slf4j将commons-logging的底层日志输出切换到slf4j
- 去掉log4j的jar包
jcl-over-slf4j.jar里面到底做了哪些事情,首先看看包里面有哪些类?
- 第一个需要注意的地方,路径org.apache.commons.logging.LogFactory,org.apache.commons.logging.Log和和common-logging.jar包的路径是一样的。这就是为什么我们去掉了commons-logging的jar包之后,程序不会报错的原因。程序依然是可以正常编译的,对应程序来说,我们删除了commons-logging.jar包是无感的。
- 第二个需要注意的地方,Log的实现类换成了SLF4JLog,SLF4JLog implements Log,SLF4JLogFactory继承了LogFactory
看看整体的类图
再次回顾一下我们的日志使用程序,使用common-logging的常见代码
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class LogTest {
private Log logger = LogFactory.getLog(this.getClass());
public static void main(String[] s){
logger.info("test");
}
}
LogFactory.getLog()实质上是调用到了SLF4JLogFactory.getInstance(),getInstance方法返回的是Log的实现类SLF4JLog,所以引用变量logger实际指向的实例是SLF4JLog,当我们执行logger.info(“test”)的时候,被SLF4JLog转发到了logback日志框架去输出日志。
整个大致类图:
其他的日志框架的装换,用到了相似的方法,不再一一分析。别的日志接口,转成slf4j的方法,这张图总结得很好,借用过来记录一下。
参考:
Java日志框架SLF4J和log4j以及logback的联系和区别
java日志组件介绍
SLF4j 和 common-logging