为什么要用slf4j
市面上的日志框架实在太多了!log4j,logback,java.util.logging,log4j2,将来也可能会有其他新的更优秀的日志框,你也可能根据公司的需求自己开发一套日志组件。 那么问题来了,如果我用log4j,来到了一个项目组用的是logback,那是不是又要从看一遍API文档开始了(虽然大部分的日志框架的用法都大同小异)。但无形中就增加了学习成本。
slf4j就是解决这样的问题的。
什么是slf4j
slf4j其实是抽象出来的一套日志接口,定义的是一个统一的规范,事实上,它自身不提供具体实现。 建一个空项目,pom.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>logtest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
</project>
测试类:
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestLog {
@Test
public void testLog() {
Logger logger = LoggerFactory.getLogger(TestLog.class);
logger.error("Yan");
}
}
运行测试方法testLog(),报错如下:
显然,提示没有找到日志的具体实现。这就印证了上面的说法! 我们随便加上一个日志实现,pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>logtest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
</project>
再次运行测试用例
正常了
让我们再来试试多个日志实现框架一起
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>logtest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>
</project>
运行结果如下:
日志框架加载的时候,提示发现了多个实现层,但是最终选定了logback作为具体实现,并且正确输出了日志。
这种设计模式叫做门面模式,好处就是,我们无论实现层用的是什么日志框架,我们在使用的时候,都是用的org.slf4j.Logger.java接口中的方法,而屏蔽了不同日志框架的差异性,省去了不必要的学习成本。
slf4j是如何做到这些的
我们从测试方法中的 LoggerFactory.getLogger(TestLog.class) 点进去一观究竟
进入到bind()方法,就是我们的重头戏了:
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
} else {
failedBinding(ncde);
throw ncde;
}
} catch (java.lang.NoSuchMethodError nsme) {
String msg = nsme.getMessage();
if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
INITIALIZATION_STATE = FAILED_INITIALIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x.");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
先看第7行的findPossibleStaticLoggerBinderPathSet()方法,这里将获取前面多次提到的StaticLoggerBinder,也就是slf4j和真正的日志实现之间连接的纽带,由于不一定只引入一种日志实现框架,因此获取的是一个Set集合。点进去看一下
这里其实就是用类加载器去获取classpath下的 org/slf4j/impl/StaticLoggerBinder.class,而这个class只有在logback等具体实现框架中有,因此,如果只引入slf4j,这里获得Set就是空的,并且在bind()方法走到 StaticLoggerBinder.getSingleton() 这一步的时候抛出 NoClassDefFoundError,并报出
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
而如果引入了多个日志实现框架,在 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet) 这一步将打印出如下信息:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/72037717/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/72037717/.m2/repository/org/slf4j/slf4j-simple/1.7.25/slf4j-simple-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/72037717/.m2/repository/org/slf4j/slf4j-log4j12/1.7.21/slf4j-log4j12-1.7.21.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
在接下来的**reportActualBinding(staticLoggerBinderPathSet)**中将会确定真正使用的日志实现框架
这一步其实是让编译器来决定选择哪一个StaticLoggerBinder,这里只是打印出结果而已。 此时我们需要做一件事情,那就是决定以哪个日志实现框架来继续往下研究到底是如何取得Logger对象,进而实现后期的打印日志的功能的。因为performInitialization()往后的事情就和具体日志实现不可分割了。(从严格意义上来讲,这已经超出了本文标题所指的讨论范围了,但我还是决定刨根问底)。 我这里选定的是log4j日志框架,这是我在工作中最常用的框架。
log4j在slf4j中的具体实现
log4j和logback不太一样。由于apache log4j本身并没有考虑去兼容实现slf4j框架,所以slf4j-log4j12这个包就作为适配器出现连接了两者。这是典型的适配器模式。
可以看出来 步骤a返回的ILoggerFactory的运行时类是Log4jLoggerFactory。 来到更上一层
步骤d可以观察到 apache的log4j中也有一个Logger类,但我们最终获得Logger类其实是slf4j中的,运行时类是Log4jLoggerAdapter,这个类你可以看作是org.apache.log4j.Logger的适配器,后续我们在使用org.slf4j.Logger.java的info(),error()等方法时,它会传递调用org.apache.log4j.Logger的info(),error()方法。是不是真相大白了?接下来的就是把org.slf4j.Logger一直抛至测试业务调用层了。