文章目录
背景
日志是进行软件开发必不可少的一项功能,也存在很多开源日志库可供使用,如slf4j,log4j,log4j2,logback,这些开源日志库直接有什么联系呢?我们应该如何使用它们呢?
现有日志体系
此部分内容来源于log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析
上图不是非常精准,但是能够比较清晰地展示现有Java日志体系的主体架构.Java日志体系大体可以分为三个部分:日志门面接口,适配器,日志框架具体实现.
- 日志框架具体实现:Java日志框架有很多种,最简单的是Java自带的java.util.logging,而最经典的是log4j,后来又出现了一个比log4j性能更好的logback,应用程序可以直接使用这些具体日志框架的API来满足日志输出需求,但是由于各个日志框架之间的API通常是不兼容的,这样做就使得应用程序丧失了更换日志框架的灵活性。
- 日志门面:比直接使用具体日志框架API更合理的选择是使用日志门面接口.日志门面接口提供了一套独立于具体日志框架实现的API,应用程序通过使用这些独立的API就能够实现与具体日志框架的解耦,如slf4j。
- 适配器:日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的s器,说白了,所谓“桥接器”,不过就是对某套API的伪实现。这种实现并不是直接去完成API所声明的功能,而是去调用有类似功能的别的API。这样就完成了从“某套API”到“别的API”的转调
需要注意的是,前面说过,上图并不精准,这只是主要部分,实际情况并不总是简单的“日志门面接口–>桥接器–>日志框架”这一条单向线。实际上,独立的桥接器有时候是不需要的,而且也并不是只有将日志门面API转调到具体日志框架API的桥接器,也存在将日志框架API转调到日志门面API的桥接器。
桥接器实现原理分析
环境
此小节介绍桥接器的实现原理,我们以slf4j+log4j为例.
日志门面为slf4j,maven如下:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
注:slf4j-api的1.7.X
版本与1.8.X
版本获取ILoggerFactory
的方式不同,桥接器实现也不同,我们以1.7.X
版本为例,下桥接器版本必须是1.7.25
,与此一致.
日志框架具体实现为log4j,maven如下:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
桥接器使用slf4j->log4j桥接器,maven如下啊:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
分析
注:在下文代码注释中,会表明此方法是日志门面还是桥接器的代码
我们从最简单的获取logger开始
//Logger和LoggerFactory都是日志门面jar包中的类
private static Logger logger = LoggerFactory.getLogger(Main.class);
继续看源码:
//日志门面
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
获取ILoggerFactory
重点在getILoggerFactory()
,此过程又分为3个步骤:
第一个步骤加载org/slf4j/impl/StaticLoggerBinder.class文件
/**
* 日志门面
* 通过ClassLoader加载"org/slf4j/impl/StaticLoggerBinder.class"
*/
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
注意:虽然StaticLoggerBinder
是在org.slf4j.impl
包下,但日志门面jar包中并无此类,此类是由桥接器实现的.比如在我的电脑上,URL就是file:/E:/mavenRepo/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class
,可以看出,其是在slf4j-log4j12-1.7.25.jar
中.
桥接器的原理其实就是在其jar包中存在StaticLoggerBinder.class
,这样就可在运行时动态绑定底层的日志实现框架.
第二个步骤选取一个StaticLoggerBinder.class来创建一个单例
当项目中有多个桥接器时,其会找到多个StaticLoggerBinder.class
,比如我们增加一个slf4j和log4j2的桥接器,其maven如下:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.9.1</version>
</dependency>
这时就会检测到两个StaticLoggerBinder.class
的文件路径,代码如下:
/**
* 日志门面
* 若在class path中找到多个绑定类,则打印警告信息
*/
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
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.");
}
}
此时slf4j会打印警告信息:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/E:/mavenRepo/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/E:/mavenRepo/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
此时代码继续:
private final static void bind() {
...
StaticLoggerBinder.getSingleton();
...
}
由于存在多个org.slf4j.impl.StaticLogerBinder
,这时会调用哪一个类的代码呢?
经过实践发现,其会调用在class path中较早出现的org.slf4j.impl.StaticLogerBinder
的代码.如在maven中,会调用在pom.xml中定义较靠前的桥接器的代码.
第三个步骤返回一个ILoggerFactory实例
return StaticLoggerBinder.getSingleton().getLoggerFactory();
返回桥接器中指定的ILoggerFactory
,在此例中就是org.slf4j.impl.Log4jLoggerFactory
,其在桥接器中定义.
获取Logger
/**
* 桥接器
*/
public Logger getLogger(String name) {
Logger slf4jLogger = loggerMap.get(name);
if (slf4jLogger != null) {
return slf4jLogger;
} else {
org.apache.log4j.Logger log4jLogger;
//调用日志框架实现生成"org.apache.log4j.Logger "
if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
log4jLogger = LogManager.getRootLogger();
else
log4jLogger = LogManager.getLogger(name);
//使用适配器包装"org.apache.log4j.Logger "
Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
桥接器会调用的日志框架实现的相关代码生成其内部的Logger(此Logger与org.slf4j.Logger
)不兼容,再通过适配器包装日志框架实现内部的Logger.
总结
- 建议在应用中使用日志门面API打印日志,日后可以通过调整桥接器和日志框架实现方便的修改日志实现
- slf4j在获取日志框架实现时扫描class path,寻找
org.slf4j.impl.StaticLoggerBinder
,桥接器就是通过提供该类实现桥接功能