作为Java开发,平时虽然日志使用的比较多,但是其深入的原理压根就没想过要去研究,有一种日用而不知的感觉。
扪心自问,这么简单的问题,确实不清楚。
之后,便是知耻而后勇,便有了对平常使用的日志的仔细研究。
下面,笔者以我们通常用的最多的spring-boot-starter-web说起。
相信大家对下面的这个依赖很熟悉:
org.springframework.boot spring-boot-starter-web
这个是我们目前搭建springboot项目最常见的依赖引入。
在idea中,我们通过ctrl+鼠标左键,一步步点击进去,可以看到:
org.springframework.boot spring-boot-starter 2.5.0 compile
继续:
org.springframework.boot spring-boot-starter-logging 2.5.0 compile
最终我们看到了在项目中默认使用的log框架:
... ch.qos.logback logback-classic 1.2.3 compile org.slf4j jul-to-slf4j 1.7.30 compile
笔者研究的便是slf4j及其实现logback的关系。
在进入正题之前,我们先回顾我们常见的一个设计模式:外观模式。
其定义为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。详见:JAVA设计模式之门面模式(外观模式) | 菜鸟教程
而我们的slf4j便是相当于一个Facade层,所用的日志打印都是通过slf4j来转发,但是具体的功能实现是由logback来实现,当然也可以由别的依赖来实现,比如slf4j-simple。
首先我们看springboot框架默认实现:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestLogger {
private static final Logger log = LoggerFactory.getLogger(TestLogger.class);
public static void main(String[] args) {
log.info("test---------->>>>>>>>>>>><<<<<<<");
}
}
归根结底,所有的用法都只是片面,我们要理解原理,还是要从源码入手。进入getLogger方法:
public static Logger getLogger(Class> clazz) {
Logger logger = getLogger(clazz.getName());
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 http://www.slf4j.org/codes.html#loggerNameMismatch for an explanation");
}
}
return logger;
}
重点关注
getLogger(clazz.getName())
继续点击:
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
日志打印的具体实现便在
getILoggerFactory()
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == 0) {
Class var0 = LoggerFactory.class;
synchronized(LoggerFactory.class) {
if (INITIALIZATION_STATE == 0) {
INITIALIZATION_STATE = 1;
performInitialization();
}
}
}
switch(INITIALIZATION_STATE) {
case 1:
return SUBST_FACTORY;
case 2:
throw new IllegalStateException("org.slf4j.LoggerFactory in failed state. Original exception was thrown EARLIER. 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");
}
}
日志打印的重点便在于返回
StaticLoggerBinder.getSingleton().getLoggerFactory()
这个对象来实现具体的日志打印工作,那StaticLoggerBinder这个类又是从哪里来,是要干什么的呢?
我们通过实际代码执行可以知道,StaticLoggerBinder便是logback这个jar包提供对slf4j日志接口LoggerFactoryBinder的具体实现,也就是说实际的日志打印slf4j不能执行,只能通过接口的实现类StaticLoggerBinder来进行执行。
这样的好处便在于slf4j相当于只是提供一个接口或者说标准,但是具体的执行可以由其实现类来执行,这样只要是实现了slf4j标准接口的任意日志框架便都可以来执行日志打印。
通过这种方式slf4j可以同时支持多种日志框架,且无需任何配置,只需要引入特定的jar包让其拥有指定全类名的StaticLoggerBinder类即可。
那么这样也会导致另一个问题,如果系统引入了多个同时实现slf4j接口的类,那么系统怎么办,是否会报错?
这种特殊情况,slf4j也有做处理,其处理方式便是通过打印所有的引入实现类,然后由JVM虚拟机选择一个合适的实现类来执行日志打印。
这样既不会影响系统日志执行,也能使程序员通过日志,清楚的看出系统中存在哪些日志的实现类。
其具体代码在上午
getILoggerFactory()方法中的performInitialization()执行:
private static final void performInitialization() {
bind();
if (INITIALIZATION_STATE == 3) {
versionSanityCheck();
}
}
在bind执行:
private static final void bind() {
try {
String msg;
try {
Set staticLoggerBinderPathSet = null;
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = 3;
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError var7) {
......
重点便在于
findPossibleStaticLoggerBinderPathSet();//找到潜在的slf4j实现类
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);//打印所有的实现类全类名
reportActualBinding(staticLoggerBinderPathSet);//打印实际的实现类全类名
详细方法如下:
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 {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while(paths.hasMoreElements()) {
URL path = (URL)paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException var4) {
Util.report("Error getting resources from path", var4);
}
return staticLoggerBinderPathSet;
}
可以看到,其是通过
STATIC_LOGGER_BINDER_PATH
即,
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class
也就是指定的全限定类名来进行加载的。
这样就涉及到一个问题, 不同的jar包依赖是可以创建同样的全限定类名的,这样会导致
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
出现多个具有同样全限定类名的类被重复找到。
那么在slf4j有多个实现的时候,如何保证其加载指定的实现呢?
以实现框架slf4j-sample和logback为例,在工程的pom文件中:
sif4j-sample在前,默认引入logback在后,其运行结果如下:
可以看到 sif4j-sample和logback有着一个完全相同的全限定类名:StaticLoggerBinder,那么系统到底选用哪一个呢?通过日志打印我们可以看到系统选择的是sif4j-sample,这是由于在pom文件中,其引入在logback之前,JVM虚拟机加载class文件时,便会选择优先引入的StaticLoggerBinder类。
反之,如果默认引入在前,则会使用默认logback。
如果只需要引入一个日志实现,也可以显示的注释或者排除掉另外一个。
参考:Java日志框架:slf4j作用及其实现原理 - 五月的仓颉 - 博客园