简介

slf4j(Simple Logging Facade for Java)是简单日志门面框架,主要提供了日志接口,不提供实现。使用了Facade设计模式。

与common-logging对比

之前,slf4j的功能由common-logging来完成,二者都是日志框架的抽象层,有什么区别呢?

1.slf4j的日志接口更高效,提供了占位符式的打印日志接口,避免了字符串的拼接代价。同时是否打印日志的逻辑在接口内部实现,效率更高。

2.更重要的,slf4j可以避免common-logging动态查找算法的bug。slf4j是在编译期决定日志实现类的,而common-logging是在运行时通过classloader决定实现类,后者存在一定bug(具体可以google“common-logging classloader issue”)。

总之,能用slf4j就用slf4j。

架构

【Java】slf4j 日志_android

我们的应用程序直接依赖slf4j的接口,也就是slf4j-api包。这个日志接口层可以由不同的日志包来实现,比如log4j、logback等等。每一种实现要想与接口对接,就要提供适配器,这样就产生了上图中slf4j-xxx.jar包。

使用

考虑maven的场景。理论上,我们需要三个jar包:slf4j-api.jar、具体实现jar和适配jar。但是我们其实只需要引入slf4j-xxx.jar即可,这个jar包会包含另外两个jar包。

例子:构建一个maven项目,其关于日志的依赖只有:

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>

查看最终的dependency:

【Java】slf4j 日志_slf4j_02

可以看到所需的三种jar都被导入。

String name = "liyao";
Logger logger = LoggerFactory.getLogger(App.class);
logger.info("name: {}", name);

使用时,占位符式的api十分方便。

实现细节

1.如何定位实现:

这是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 " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
}
}
return logger;
}

public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}

public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
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");
}

private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}

最终是在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);
}
}

上面代码很长,其实只有一句“StaticLoggerBinder.getSingleton”完成了加载。看下StaticLoggerBinder的import。

import org.slf4j.impl.StaticLoggerBinder;

也就是这里会在类路径下寻找这个StaticLoggerBinder类。

接着点进去看下这个类的实现:

public class StaticLoggerBinder implements LoggerFactoryBinder {
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
public static String REQUESTED_API_VERSION = "1.6.99";
private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();
private final ILoggerFactory loggerFactory = new Log4jLoggerFactory();

public static final StaticLoggerBinder getSingleton() {
return SINGLETON;
}

private StaticLoggerBinder() {
try {
Level var1 = Level.TRACE;
} catch (NoSuchFieldError var2) {
Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
}

}

public ILoggerFactory getLoggerFactory() {
return this.loggerFactory;
}

public String getLoggerFactoryClassStr() {
return loggerFactoryClassStr;
}
}

这是一个非接口的类,所以会在编译器决定。其内部实际上负责返回具体日志实现类的工厂类实例。而且这个类已经不是slf4j-api.jar内的类了,而是slf4j-log4j12.jar内的类:

【Java】slf4j 日志_jar_03

看到这里应该明朗了,每种具体的日志实现类都要提供一个StaticLoggerBinder类,当具体使用时,只需要导入其中一个jar到类路径即可。slf4j-api的getLogger方法就可以通过staticbinder类拿到具体实现类的工厂,进而构建具体日志实现类的实例。由于这里的StaticLoggerBinder类是非interface的,所以在编译器就可以确定,所以这是一种静态绑定。

这与common-logging的classloader运行时加载方式不同。那如果有多个实现类,也就是多个staticbinder类在类路径下呢?这时slf4j会发出警告,具体绑定哪一个是不确定的,与虚拟机具体的执行有关,所以我们需要保证只有一个staticbinder类在类路径下。

2.适配器:

使用slf4j-xxx.jar工厂返回的日志实例其实是一个adapter,其本身不会提供日志功能的实现,而是转交给具体的日志实现类完成,下面是Log4jLoggerAdapter的debug方法:

public void debug(String format, Object arg1, Object arg2) {
if (this.logger.isDebugEnabled()) {
FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
this.logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
}

}

可以看到这里转交给了log变量完成,而log变量就是log4j日志实现类的实例。slf4j-xxx.jar仅仅是完成适配。

lombok

最后奉上一个小技巧,我们可以使用@slf4j注解来完成日志变量的定义和实例化,这样就不需要每一次都在类里写Log log = Factory.getLogger()这样的方法了。lombok会为我们自动生成一个log变量,例子:

@slf4j
public class A {
public void f(){
log.info("name: {}", "ly");
}
}

在idea下使用需要再导入一个idea里的lombok插件,这样就不会报编译错误。