1.SLF4J
SLF4J全稱 Simple Logging Facade for Java,它為Java下的日志系統(tǒng)提供了一套統(tǒng)一的門面(接口)豹缀。通過引入SLF4J,我們可以使項(xiàng)目的logging與logging具體的實(shí)現(xiàn)分離巾乳,在提供了一致的接口的同時(shí)滤愕,提供了靈活選擇logging實(shí)現(xiàn)的能力。
在SLF4J之前她肯,Apache Common Logging(即Jakarta Commons Logging佳头,簡稱JCL)也提供了類似的功能。它與SLF4J的區(qū)別在于:
- JCL即提供了統(tǒng)一的接口晴氨,也提供了一套默認(rèn)的實(shí)現(xiàn)康嘉;SLF4J則只提供了接口層
- JCL采用運(yùn)行時(shí)綁定,通過Classloader體系加載相應(yīng)的logging實(shí)現(xiàn)籽前;SLF4J采用了編譯期綁定
- SLF4J在接口易用性上更有優(yōu)勢亭珍,大大減少了不必要的日志拼接:
- JCL下,為了避免無效的字符串拼接枝哄,一般需要按照如下方式輸出日志:
if(log.isInfoEnabled()) { log.info("AnalyseOrderLogic.checkBusinessValid:" + channelCoopId + "," + JSON.toJSONString(entities)); }
- SLF4J則提供了占位符"{}"肄梨,只在必要的情況下才會進(jìn)行日志字符串處理和拼接:
log.info("AnalyseOrderLogic.checkBusinessValid:{},{}", channelCoopId, JSON.toJSONString(entities));
2.SLF4J的使用
SLF4J的使用非常簡單:
- 引入SLF4J依賴
- 引入一種logging的SLF4J實(shí)現(xiàn),比如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的項(xiàng)目
2.1 引入SLF4J
【注】也可以從github上下載本節(jié)demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b nop origin/nop
- 創(chuàng)建工程并引入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());
}
}
此時(shí)運(yùn)行App.main()
,會發(fā)現(xiàn)沒有日志打印,但是有如下錯誤信息:
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.
出現(xiàn)該問題的原因是懂牧,SLF4J只提供了一個同一的日志接口/門面(Facade)蝙茶,如果找到任何實(shí)現(xiàn)宵呛,則綁定到默認(rèn)的NOPLoggerFactory
茄厘。此時(shí)日志系統(tǒng)不會生效,而是打印出上述錯誤信息并繼續(xù)執(zhí)行稠茂。
因此柠偶,為了打印日志,我們還需要引入日志得實(shí)現(xiàn)類睬关。
看看此時(shí)的項(xiàng)目依賴關(guān)系:
$ 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的實(shí)現(xiàn)
Logback是流行的log框架-Log4J的繼任者嚣州。相比Log4J,logback做了大量的改進(jìn)共螺,比如提供了更高的性能该肴,原生支持SLF4J等。
【注】也可以從github上下載本節(jié)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
下創(chuàng)建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的設(shè)置藐不,additivity指示是否遵循缺省的繼承機(jī)制-->
<logger name="cn.jinlu.slf.demo" additivity="false">
<level value ="info"/>
<appender-ref ref="myConsole" />
</logger>
<!-- 根logger的設(shè)置-->
<root>
<level value ="warn"/>
<appender-ref ref="myConsole"/>
</root>
</log4j:configuration>
再次運(yùn)行App.main()
匀哄,可以看到如下日志輸出:
[29 17:18:45,209 INFO ] [main] demo.App - Hello, App!
最后秦效,檢查一下依賴關(guān)系:
$ 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的實(shí)現(xiàn)log4j
涎嚼。
2.2 將slf4j的實(shí)現(xiàn)修改為logback
【注】也可以從github上下載本節(jié)demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b logback origin/logback
引入slf4j之后阱州,對日志實(shí)現(xiàn)的改動變得更加靈活,比如如果我們希望從log4j遷移到性能更好的logback法梯,那么我們可以:
- 修改依賴關(guān)系苔货,將對
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會默認(rèn)創(chuàng)建一個
BasicConfigurator
默認(rèn)配置立哑,將DEBUG
級別及以上的日志輸出到Console夜惭。
- 如果沒有配置文件,logback會默認(rèn)創(chuàng)建一個
再次運(yùn)行App.main()
,可以看到如下日志輸出:
[29 17:18:45,209 INFO ] [main] demo.App - Hello, App!
此時(shí)工程的依賴關(guān)系如下铛绰,可見logback-classic自動引入了logback-core的實(shí)現(xiàn)诈茧。
$ 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靜態(tài)綁定源碼解析
在第一章中,我們指出捂掰,SLF4J相比JCL的一大優(yōu)勢是采用了靜態(tài)綁定敢会,避免了在OSGI等場景中通過classloader動態(tài)綁定造成的困擾。現(xiàn)在我們看看SLF4J靜態(tài)綁定的過程这嚣。
參考2.1節(jié)中App.java
的代碼鸥昏,使用SLF4J時(shí),
- 通過LoggerFactory.getLogger(Class<?>)獲取一個Logger
private static final Logger logger = LoggerFactory.getLogger(App.class)
- 在該類內(nèi)部的任意處通過該logger打印不同級別的日志
logger.info( "Hello, {}!", App.class.getSimpleName());
首先看如何獲取一個Logger
3.1 創(chuàng)建或獲取一個Logger
一個典型的SLF4J類關(guān)系圖如下所示姐帚。這里我們忽略slf4j-api中的輔助類(位于包org.slf4j.helpers
內(nèi))互广,以及不常用的Marker
和MDC
功能。
就像demo代碼那樣卧土,SLF4J非常簡單。使用SLF4J只需要通過LoggerFactory.getLogger獲取一個Logger對象像樊,并通過該Logger對象進(jìn)行日志記錄即可尤莺。其他一切細(xì)節(jié),都通過SLF4J及SLF4J-XXX-binder進(jìn)行了屏蔽生棍。這個binder用來將具體的logging實(shí)現(xiàn)與SLF4J進(jìn)行綁定颤霎。
- 在1.7及更早的版本中,該綁定都是通過繼承
org.slf4j.spi
中的接口來實(shí)現(xiàn)涂滴,并且SLF4J對繼承該接口的類的類名也進(jìn)行了約定友酱。因此圖中org.slf4j.impl
包含了這些類的實(shí)現(xiàn),這些類都必須并且類名也必須遵照SLF4J的約定柔纵,且位于logging實(shí)現(xiàn)包中缔杉。比如以logback為例:
在SLF4J的門面類中,會通過代碼硬編碼的方式獲取指定類名的單例(StaticXxxBinder)搁料,并通過org.slf4j.spi
中的接口獲取相關(guān)的資源或详。
- 在1.8版本中系羞,引入了SPI自動服務(wù)發(fā)現(xiàn)。具體請參考第4章霸琴。
3.1.1 LoggerFactory.getLogger(Class<?>)
調(diào)用LoggerFactory.getLogger(Class<?>)
的源碼如下椒振。getLogger
會通過類的全限定名從LoggerFactory
工廠中獲取Logger。
public final class LoggerFactory {
...
// 2.通過getILoggerFactory獲取或創(chuàng)建一個可用的LoggerFactory梧乘,并通過該Factory獲取或創(chuàng)建一個通過name指定的Logger澎迎。
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
// 1.將clazz的全限定名作為String,調(diào)用geteLogger(String)方法
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
// 如果開啟了檢測命名錯誤选调,那么如果clazz不存在夹供,則會打印錯誤信息。此處忽略相關(guān)處理
...
}
return logger;
}
}
3.1.2 getILoggerFactory()創(chuàng)建Logger工廠
在performInitialization()
中学歧,SLF4J調(diào)用bind()
進(jìn)行實(shí)現(xiàn)綁定罩引,如果綁定成功,則會進(jìn)行版本檢查枝笨。SLF4J要求slf4j-api的版本必須與其實(shí)現(xiàn)的版本對應(yīng)袁铐,否則可能會發(fā)生兼容性問題(SLF4J的1.8版與更早的版本,比如1.6和1.7存在兼容性問題)横浑。
綁定成功后剔桨,每次調(diào)用getILoggerFactory()
,則會通過StaticLoggerBinder.getSingletion().getLoggerFactory()
獲取一個ILoggerFactory
接口派生的工廠對象徙融,用來創(chuàng)建具體的Logger
實(shí)例洒缀。
public final class LoggerFactory {
// 使用volatile的INITIALIZATION_STATE確保只會發(fā)生一次綁定
static volatile int INITIALIZATION_STATE = UNINITIALIZED;
...
// 3.綁定SLF4J實(shí)現(xiàn)
private final static void performInitialization() {
// 4.bind()方法是SLF4J實(shí)現(xiàn)綁定的關(guān)鍵
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 5.如果初始化成功,則檢查SLF4J的實(shí)現(xiàn)支持的版本號是否與SLF4J匹配
// SLF4J要求binder的版本與slf4j-api的版本匹配欺冀,否則打印一條警告信息树绩,因?yàn)閟lf4j可能會不工作。
versionSanityCheck();
}
}
...
// 1.獲取ILoggerFactory的實(shí)現(xiàn)
public static ILoggerFactory getILoggerFactory() {
// 2.通過volatile的INITIALIZATION_STATE確保只會發(fā)生一次綁定
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
// 6.如果初始化成功隐轩,則調(diào)用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實(shí)現(xiàn)(初始化)
最后饺饭,來看一下bind()
方法的實(shí)現(xiàn),注意代碼中的注釋职车。
在第2步中瘫俊,我們可以看到SLF4J靜態(tài)綁定的方式。它強(qiáng)制了StaticLoggerBinder的很多實(shí)現(xiàn)細(xì)節(jié):
- 必須是一個單例
- 必須提供一個靜態(tài)的getSingleton()方式創(chuàng)建/獲取單例
- 類的全限定名必須是"org.slf4j.impl.StaticLoggerBinder"
- 必須提供一個
static final String REQUESTED_API_VERSION
對象指定支持的版本
對其他幾個Binder:StaticMarkerBinder和StaticMDCBinder悴灵,SLF4J也有類似的強(qiáng)制實(shí)現(xiàn)要求扛芽。
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系統(tǒng),通過ClassLoader尋址org/slf4j/impl/StaticLoggerBinder.class的可用實(shí)現(xiàn)积瞒。
// 如果超過1個川尖,則發(fā)出警告信息。最終SLF4J會選擇其中的一個進(jìn)行綁定茫孔。
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
// 2.靜態(tài)綁定空厌,創(chuàng)建StaticLoggerBinder的單例
StaticLoggerBinder.getSingleton();
// 3.修改初始化狀態(tài)
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 4.報(bào)告實(shí)際綁定的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版的改進(jìn)
【注】也可以從github上下載本節(jié)demo:
$ git clone git@github.com:jinluu/slf-demo.git
$ git checkout -b slf4j18 origin/slf4j18
SLF4J 1.8中最大的改進(jìn)就是摒棄了hard code的代碼綁定(參考3.1.3庐船,注釋2),而是使用了更加優(yōu)雅嘲更、耦合更松的SPI方式進(jìn)行服務(wù)發(fā)現(xiàn)筐钟。我們看看1.8版本中對日志綁定的改進(jìn):
- 提供了
org.slf4j.spi.SLF4JServiceProvider
服務(wù)接口用于SPI綁定 - 改進(jìn)了
org.slf4j.LoggerFactory.bind()
的實(shí)現(xiàn),采用SPI方式進(jìn)行SLF4JServiceProvider
服務(wù)發(fā)現(xiàn)和綁定 - 不再支持1.8版本以前的按照約定的類型
StaticXxxBinder
約定類名進(jìn)行綁定的方式 - 去除了3.1.3節(jié)中對
StaticLoggerBinder
的所有強(qiáng)制約定
可見赋朦,1.8版本和之前的版本是完全不兼容的篓冲,且1.8版本明顯更加優(yōu)雅。
4.1 SLF4JServiceProvider
類圖如下宠哄,只要將該接口的實(shí)現(xiàn)暴露成一個SPI服務(wù)壹将,SLF4J就可以正常綁定到該logging實(shí)現(xiàn)上。
4.2 綁定
通過代碼及注釋毛嫉,可以發(fā)現(xiàn):
-
bind()
只會通過SPI服務(wù)發(fā)現(xiàn)的方式尋找可用的日志服務(wù)诽俯。
因此,如果采用了1.8版本的slf4j-api
承粤,則不支持1.8的日志實(shí)現(xiàn)不會被加載 - 如果發(fā)現(xiàn)了多于一個基于SPI的日志服務(wù)暴区,則打印告警,并默認(rèn)綁定第一個被發(fā)現(xiàn)的服務(wù)
- 如果沒有發(fā)現(xiàn)基于SPI的日志服務(wù)辛臊,則默認(rèn)綁定到SPI的NOP日志服務(wù)仙粱,并嘗試通過指定全限定名的方式(
org.slf4j.impl.StaticLoggerBinder
)尋址舊版本的日志服務(wù),如果找到了則發(fā)出版本mismatch告警彻舰,但是不會嘗試加載老版本的日志服務(wù)伐割。
因此,如果使用新版本的SLF4J(1.8及以上)刃唤,務(wù)必使用對應(yīng)的binder類隔心,避免引起兼容性問題。
除了采用了更優(yōu)雅的服務(wù)發(fā)現(xiàn)機(jī)制尚胞,在其他方面硬霍,SLF4J 1.8與之前版本差別很小。
class LoggerFactory {
private final static void performInitialization() {
// 1.執(zhí)行綁定
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 2.成功辐真,則進(jìn)行版本檢查
// 注意:老版本不支持SPI,壓根不會運(yùn)行到這里
versionSanityCheck();
}
}
private final static void bind() {
try {
// 3.SPI服務(wù)發(fā)現(xiàn)
List<SLF4JServiceProvider> providersList = findServiceProviders();
// 4.SPI發(fā)現(xiàn)多于一個日志實(shí)現(xiàn)則發(fā)出警告信息
reportMultipleBindingAmbiguity(providersList);
if (providersList != null && !providersList.isEmpty()) {
// 5.綁定第一個被發(fā)現(xiàn)的logging服務(wù)
PROVIDER = providersList.get(0);
PROVIDER.initialize();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 6.報(bào)告真實(shí)綁定的logging信息
reportActualBinding(providersList);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_PROVIDER.getSubstituteLoggerFactory().clear();
} else {
// 7.如果通過SPI沒有發(fā)現(xiàn)可用服務(wù)崖堤,則默認(rèn)采用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.嘗試尋找老版本的日志服務(wù)(通過尋址`org/slf4j/impl/StaticLoggerBinder.class`)
// 找到則打印警告信息侍咱,但不會嘗試綁定老日志服務(wù)
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
}
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
}
4.3 logback對服務(wù)發(fā)現(xiàn)機(jī)制的改進(jìn)
目前最新的SLF4J 1.8版處于slf4j-api:1.8.0-beta-2
版本,對應(yīng)的logback-classic版本為logback-classic:1.3.0-alpha4
(官方對應(yīng)1.8.0-beta-1
密幔,但是與beta-2
兼容)楔脯。
為了兼容1.8的SLF4J,logback-classic提供了SPI服務(wù)配置文件胯甩,如下圖昧廷。這樣堪嫂,在啟動階段,SLF4J就可以通過ServiceLoader找到logback-classic并進(jìn)行注冊了木柬。
同時(shí)皆串,最新版的logback也去掉了org.slf.impl
包,徹底摒棄了老版本SLF4J的支持眉枕。
4.4 SPI
關(guān)于SPI服務(wù)的深度剖析恶复,請參考筆者之前的博文『Service Provider Interface詳解 (SPI)』