1.SLF4J
SLF4J全称 Simple Logging Facade for Java,它为Java下的日志系统提供了一套统一的门面(接口)。通过引入SLF4J,我们可以使项目的logging与logging具体的实现分离,在提供了一致的接口的同时,提供了灵活选择logging实现的能力。
在SLF4J之前,Apache Common Logging(即Jakarta Commons Logging,简称JCL)也提供了类似的功能。它与SLF4J的区别在于:
- JCL即提供了统一的接口,也提供了一套默认的实现;SLF4J则只提供了接口层
- JCL采用运行时绑定,通过Classloader体系加载相应的logging实现;SLF4J采用了编译期绑定
- SLF4J在接口易用性上更有优势,大大减少了不必要的日志拼接:
- JCL下,为了避免无效的字符串拼接,一般需要按照如下方式输出日志:
if(log.isInfoEnabled()) { log.info("AnalyseOrderLogic.checkBusinessValid:" + channelCoopId + "," + JSON.toJSONString(entities)); }
- SLF4J则提供了占位符"{}",只在必要的情况下才会进行日志字符串处理和拼接:
log.info("AnalyseOrderLogic.checkBusinessValid:{},{}", channelCoopId, JSON.toJSONString(entities));
2.SLF4J的使用
SLF4J的使用非常简单:
- 引入SLF4J依赖
- 引入一种logging的SLF4J实现,比如SLF4J LOG4J 12 Binding,或logback-classic
之后,就可以正常使用SLF4J打印日志了,demo如下:
package some.package;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public MyClass {
Logger logger = LoggerFactory.getLogger(MyClass.class);
public void someMethod() {
logger.info("Hello world");
}
}
让我们从0开始搭建一个基于SLF4J的项目
2.1 引入SLF4J
【注】也可以从github上下载本节demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b nop origin/nop
- 创建工程并引入slf4j-api依赖
$ mvn archetype:generate -DgroupId=cn.jinlu.slf.demo -DartifactId=slf-demo -Dversion=0.1-SNAPSHOT -DpackageName=cn.jinlu.slf.demo -DarchetypeArtifactId=maven-archetype-quickstart
- 引入slf4j-api依赖
在pom.xml中添加依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
- 使用logger
package cn.jinlu.slf.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main( String[] args )
{
logger.info( "Hello, {}!", App.class.getSimpleName());
}
}
此时运行App.main()
,会发现没有日志打印,但是有如下错误信息:
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.
出现该问题的原因是,SLF4J只提供了一个同一的日志接口/门面(Facade),如果找到任何实现,则绑定到默认的NOPLoggerFactory
。此时日志系统不会生效,而是打印出上述错误信息并继续执行。
因此,为了打印日志,我们还需要引入日志得实现类。
看看此时的项目依赖关系:
$ mvn dependency:tree
...
[INFO] cn.jinlu.slf.demo:slf-demo:jar:0.1-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] \- junit:junit:jar:4.10:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.1:test
...
2.2 引入Log4J作为SLF4J的实现
Logback是流行的log框架-Log4J的继任者。相比Log4J,logback做了大量的改进,比如提供了更高的性能,原生支持SLF4J等。
【注】也可以从github上下载本节demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b log4j origin/log4j
- 引入log4j依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
- 在
src/main/resources
下创建log4j.xml
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>
<appender name="myConsole" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
</layout>
</appender>
<!-- 指定logger的设置,additivity指示是否遵循缺省的继承机制-->
<logger name="cn.jinlu.slf.demo" additivity="false">
<level value ="info"/>
<appender-ref ref="myConsole" />
</logger>
<!-- 根logger的设置-->
<root>
<level value ="warn"/>
<appender-ref ref="myConsole"/>
</root>
</log4j:configuration>
再次运行App.main()
,可以看到如下日志输出:
[29 17:18:45,209 INFO ] [main] demo.App - Hello, App!
最后,检查一下依赖关系:
$ mvn dependency:tree
[INFO] Scanning for projects...
...
[INFO] cn.jinlu.slf.demo:slf-demo:jar:0.1-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] +- org.slf4j:slf4j-log4j12:jar:1.7.25:compile
[INFO] | \- log4j:log4j:jar:1.2.17:compile
[INFO] \- junit:junit:jar:4.10:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.1:test
可见,slf4j-log4j12
自动引入了log4j的实现log4j
。
2.2 将slf4j的实现修改为logback
【注】也可以从github上下载本节demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b logback origin/logback
引入slf4j之后,对日志实现的改动变得更加灵活,比如如果我们希望从log4j迁移到性能更好的logback,那么我们可以:
- 修改依赖关系,将对
slf4j-log4j12
依赖修改为对logback-classic
的依赖:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
- 在
src/main/resources
下删除log4j.xml- 如果没有配置文件,logback会默认创建一个
BasicConfigurator
默认配置,将DEBUG
级别及以上的日志输出到Console。
- 如果没有配置文件,logback会默认创建一个
再次运行App.main()
,可以看到如下日志输出:
[29 17:18:45,209 INFO ] [main] demo.App - Hello, App!
此时工程的依赖关系如下,可见logback-classic自动引入了logback-core的实现。
$ mvn dependency:tree
...
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ slf-demo ---
[INFO] cn.jinlu.slf.demo:slf-demo:jar:0.1-SNAPSHOT
[INFO] +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] \- junit:junit:jar:4.10:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.1:test
3.SLF4J静态绑定源码解析
在第一章中,我们指出,SLF4J相比JCL的一大优势是采用了静态绑定,避免了在OSGI等场景中通过classloader动态绑定造成的困扰。现在我们看看SLF4J静态绑定的过程。
参考2.1节中App.java
的代码,使用SLF4J时,
- 通过LoggerFactory.getLogger(Class<?>)获取一个Logger
private static final Logger logger = LoggerFactory.getLogger(App.class)
- 在该类内部的任意处通过该logger打印不同级别的日志
logger.info( "Hello, {}!", App.class.getSimpleName());
首先看如何获取一个Logger
3.1 创建或获取一个Logger
一个典型的SLF4J类关系图如下所示。这里我们忽略slf4j-api中的辅助类(位于包org.slf4j.helpers
内),以及不常用的Marker
和MDC
功能。
就像demo代码那样,SLF4J非常简单。使用SLF4J只需要通过LoggerFactory.getLogger获取一个Logger对象,并通过该Logger对象进行日志记录即可。其他一切细节,都通过SLF4J及SLF4J-XXX-binder进行了屏蔽。这个binder用来将具体的logging实现与SLF4J进行绑定。
- 在1.7及更早的版本中,该绑定都是通过继承
org.slf4j.spi
中的接口来实现,并且SLF4J对继承该接口的类的类名也进行了约定。因此图中org.slf4j.impl
包含了这些类的实现,这些类都必须并且类名也必须遵照SLF4J的约定,且位于logging实现包中。比如以logback为例:
在SLF4J的门面类中,会通过代码硬编码的方式获取指定类名的单例(StaticXxxBinder),并通过org.slf4j.spi
中的接口获取相关的资源。
- 在1.8版本中,引入了SPI自动服务发现。具体请参考第4章。
3.1.1 LoggerFactory.getLogger(Class<?>)
调用LoggerFactory.getLogger(Class<?>)
的源码如下。getLogger
会通过类的全限定名从LoggerFactory
工厂中获取Logger。
public final class LoggerFactory {
...
// 2.通过getILoggerFactory获取或创建一个可用的LoggerFactory,并通过该Factory获取或创建一个通过name指定的Logger。
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
// 1.将clazz的全限定名作为String,调用geteLogger(String)方法
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
// 如果开启了检测命名错误,那么如果clazz不存在,则会打印错误信息。此处忽略相关处理
...
}
return logger;
}
}
3.1.2 getILoggerFactory()创建Logger工厂
在performInitialization()
中,SLF4J调用bind()
进行实现绑定,如果绑定成功,则会进行版本检查。SLF4J要求slf4j-api的版本必须与其实现的版本对应,否则可能会发生兼容性问题(SLF4J的1.8版与更早的版本,比如1.6和1.7存在兼容性问题)。
绑定成功后,每次调用getILoggerFactory()
,则会通过StaticLoggerBinder.getSingletion().getLoggerFactory()
获取一个ILoggerFactory
接口派生的工厂对象,用来创建具体的Logger
实例。
public final class LoggerFactory {
// 使用volatile的INITIALIZATION_STATE确保只会发生一次绑定
static volatile int INITIALIZATION_STATE = UNINITIALIZED;
...
// 3.绑定SLF4J实现
private final static void performInitialization() {
// 4.bind()方法是SLF4J实现绑定的关键
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 5.如果初始化成功,则检查SLF4J的实现支持的版本号是否与SLF4J匹配
// SLF4J要求binder的版本与slf4j-api的版本匹配,否则打印一条警告信息,因为slf4j可能会不工作。
versionSanityCheck();
}
}
...
// 1.获取ILoggerFactory的实现
public static ILoggerFactory getILoggerFactory() {
// 2.通过volatile的INITIALIZATION_STATE确保只会发生一次绑定
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
// 6.如果初始化成功,则调用StaticLoggerBinder单例的getLoggerFactory()方法获得LoggerFactory工厂对象
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
}
3.1.3 bind()方法绑定SLF4J实现(初始化)
最后,来看一下bind()
方法的实现,注意代码中的注释。
在第2步中,我们可以看到SLF4J静态绑定的方式。它强制了StaticLoggerBinder的很多实现细节:
- 必须是一个单例
- 必须提供一个静态的getSingleton()方式创建/获取单例
- 类的全限定名必须是"org.slf4j.impl.StaticLoggerBinder"
- 必须提供一个
static final String REQUESTED_API_VERSION
对象指定支持的版本
对其他几个Binder:StaticMarkerBinder和StaticMDCBinder,SLF4J也有类似的强制实现要求。
public final class LoggerFactory {
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()) {
// 1.针对非android系统,通过ClassLoader寻址org/slf4j/impl/StaticLoggerBinder.class的可用实现。
// 如果超过1个,则发出警告信息。最终SLF4J会选择其中的一个进行绑定。
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
// 2.静态绑定,创建StaticLoggerBinder的单例
StaticLoggerBinder.getSingleton();
// 3.修改初始化状态
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 4.报告实际绑定的StaticLoggerBinder信息
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);
}
}
}
4.SLF4J 1.8版的改进
【注】也可以从github上下载本节demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b slf4j18 origin/slf4j18
SLF4J 1.8中最大的改进就是摒弃了hard code的代码绑定(参考3.1.3,注释2),而是使用了更加优雅、耦合更松的SPI方式进行服务发现。我们看看1.8版本中对日志绑定的改进:
- 提供了
org.slf4j.spi.SLF4JServiceProvider
服务接口用于SPI绑定 - 改进了
org.slf4j.LoggerFactory.bind()
的实现,采用SPI方式进行SLF4JServiceProvider
服务发现和绑定 - 不再支持1.8版本以前的按照约定的类型
StaticXxxBinder
约定类名进行绑定的方式 - 去除了3.1.3节中对
StaticLoggerBinder
的所有强制约定
可见,1.8版本和之前的版本是完全不兼容的,且1.8版本明显更加优雅。
4.1 SLF4JServiceProvider
类图如下,只要将该接口的实现暴露成一个SPI服务,SLF4J就可以正常绑定到该logging实现上。
4.2 绑定
通过代码及注释,可以发现:
-
bind()
只会通过SPI服务发现的方式寻找可用的日志服务。
因此,如果采用了1.8版本的slf4j-api
,则不支持1.8的日志实现不会被加载 - 如果发现了多于一个基于SPI的日志服务,则打印告警,并默认绑定第一个被发现的服务
- 如果没有发现基于SPI的日志服务,则默认绑定到SPI的NOP日志服务,并尝试通过指定全限定名的方式(
org.slf4j.impl.StaticLoggerBinder
)寻址旧版本的日志服务,如果找到了则发出版本mismatch告警,但是不会尝试加载老版本的日志服务。
因此,如果使用新版本的SLF4J(1.8及以上),务必使用对应的binder类,避免引起兼容性问题。
除了采用了更优雅的服务发现机制,在其他方面,SLF4J 1.8与之前版本差别很小。
class LoggerFactory {
private final static void performInitialization() {
// 1.执行绑定
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 2.成功,则进行版本检查
// 注意:老版本不支持SPI,压根不会运行到这里
versionSanityCheck();
}
}
private final static void bind() {
try {
// 3.SPI服务发现
List<SLF4JServiceProvider> providersList = findServiceProviders();
// 4.SPI发现多于一个日志实现则发出警告信息
reportMultipleBindingAmbiguity(providersList);
if (providersList != null && !providersList.isEmpty()) {
// 5.绑定第一个被发现的logging服务
PROVIDER = providersList.get(0);
PROVIDER.initialize();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 6.报告真实绑定的logging信息
reportActualBinding(providersList);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_PROVIDER.getSubstituteLoggerFactory().clear();
} else {
// 7.如果通过SPI没有发现可用服务,则默认采用NOP日志
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("No SLF4J providers were found.");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_PROVIDERS_URL + " for further details.");
// 8.尝试寻找老版本的日志服务(通过寻址`org/slf4j/impl/StaticLoggerBinder.class`)
// 找到则打印警告信息,但不会尝试绑定老日志服务
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
}
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
}
4.3 logback对服务发现机制的改进
目前最新的SLF4J 1.8版处于slf4j-api:1.8.0-beta-2
版本,对应的logback-classic版本为logback-classic:1.3.0-alpha4
(官方对应1.8.0-beta-1
,但是与beta-2
兼容)。
为了兼容1.8的SLF4J,logback-classic提供了SPI服务配置文件,如下图。这样,在启动阶段,SLF4J就可以通过ServiceLoader找到logback-classic并进行注册了。
同时,最新版的logback也去掉了org.slf.impl
包,彻底摒弃了老版本SLF4J的支持。
4.4 SPI
关于SPI服务的深度剖析,请参考笔者之前的博文『Service Provider Interface详解 (SPI)』