背景
想必小伙伴們都用過日志铝噩,雖然日志看起來可有可無衡蚂,但是等到出問題的時候,就比較棘手骏庸。所以說日志框架使用好不好毛甲,規(guī)范不規(guī)范,直接影響了解決生產(chǎn)環(huán)境故障的效率具被,日志框架選的不好玻募、用的不好,有可能影響環(huán)境的性能硬猫,也有可能影響排查問題的難易程度补箍。
那么如何來正確使用日志框架呢?
阿里Java開發(fā)手冊--日志規(guī)約第一條: 【強制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j啸蜜、Logback)中的 API,而應(yīng)依賴使用日志框架 SLF4J 中的 API辈挂,使用門面模式的日志框架衬横,有利于維護和各個類的日志處理方式統(tǒng)一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
為什么會有此規(guī)范呢终蒂?我們先來了解下日志框架蜂林。
日志框架
Java中的日志框架分如下幾種:
Log4j Apache Log4j是一個基于Java的日志記錄工具遥诉。它是由Ceki Gülcü首創(chuàng)的,現(xiàn)在則是Apache軟件基金會的一個項目噪叙。
不支持占位符
具體日志實現(xiàn)Log4j 2 Apache Log4j 2是apache開發(fā)的一款Log4j的升級產(chǎn)品矮锈。
不支持占位符
具體日志實現(xiàn)Commons Logging Apache基金會所屬的項目,是一套Java日志接口睁蕾,之前叫Jakarta Commons Logging苞笨,簡稱JCL 后更名為Commons Logging。
不支持占位符
日志門面Logback 一套日志組件的實現(xiàn)(slf4j陣營)子眶。
不支持占位符
具體日志實現(xiàn)Jul (Java Util Logging),自Java1.4以來的官方日志實現(xiàn)瀑凝。
不支持占位符
具體日志實現(xiàn)Slf4j 類似于Commons Logging,是一套簡易Java日志門面臭杰,本身并無日志的實現(xiàn)粤咪。(Simple Logging Facade for Java,縮寫Slf4j)渴杆。
支持占位符
日志門面
重點來看下Slf4j
官方說明
Simple Logging Facade for Java(SLF4J): 可用作各種日志框架的簡單外觀或抽象寥枝,例如java.util.logging,logback和log4j磁奖。SLF4J允許最終用戶在部署時插入所需的日志記錄框架囊拜。
為什么阿里要強制 依賴使用日志框架 SLF4J 中的 API 或者說Slf4j的特點: 除了得益于使用了門面模式,還有一個重要特性 支持占位符点寥。
門面模式
門面(Facade)模式艾疟,對外隱藏了系統(tǒng)的復(fù)雜性,并向客戶端提供了可以訪問的接口敢辩,門面模式的好處是將客戶端和子系統(tǒng)松耦合蔽莱,方便子系統(tǒng)的擴展和維護。
正是門面模式這樣的特點戚长,使用Slf4j門面盗冷,不管日志組件使用的是log4j還是logback等等,對于調(diào)用者而言并不關(guān)心使用的是什么日志組件同廉,而且對于日志組件的更換或者升級仪糖,調(diào)用的地方也不要做任何修改。
占位符
Slf4j中有一個很重要的特性:占位符迫肖,{}可以拼接任意字符串锅劝,相比如其他框架的優(yōu)點即不需要用+來拼接字符串,也就不會創(chuàng)建新的字符串對象蟆湖。
使用注意點:
基本原則:嵌入式組件(如庫和框架)不應(yīng)聲明對任何SLF4J綁定的依賴性故爵,而只依賴于slf4j-api。
如果庫聲明了對slf4j-api特定綁定的傳遞依賴時隅津,該種綁定強加給最終用戶诬垂, 并非SLF4J的目的劲室。
請注意,聲明對綁定的非傳遞依賴性(例如结窘,scope用于test 或 provide)不會影響最終用戶很洋。
源碼分析
首先使用靜態(tài)工廠來獲取Logger對象,傳入的class隧枫,最終會轉(zhuǎn)化為name喉磁,每個類的日志處理可能不同,所以根據(jù)傳入類的名字來判斷類的實現(xiàn)方式
public static Logger getLogger(Class clazz) {
return getLogger(clazz.getName());
}
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
真正核心的在getILoggerFactory()中悠垛,首先判斷初始化的狀態(tài)INITIALIZATION_STATE线定,如果沒有初始化UNINITIALIZED,那么會更改狀態(tài)為正在初始化ONGOING_INITIALIZATION确买,并執(zhí)行初始化performInitialization()斤讥,初始化完成之后,判斷初始化的狀態(tài)湾趾,如果初始化成功SUCCESSFUL_INITIALIZATION芭商,那么會通過StaticLoggerBinder獲取日志工廠getLoggerFactory(),這里又涉及到了單例模式搀缠。
public static ILoggerFactory getILoggerFactory() {
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("org.slf4j.LoggerFactory could not be successfully initialized. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
case ONGOING_INITIALIZATION:
return TEMP_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
接著我們分析performInitialization是如何初始化的铛楣,首先是執(zhí)行bind()方法,然后判斷如果狀態(tài)為初始化成功SUCCESSFUL_INITIALIZATION艺普,執(zhí)行版本檢查簸州,主要是檢查jdk版本與slf4j的版本,看是否匹配歧譬。
private static final void performInitialization() {
bind();
if (INITIALIZATION_STATE == 3) {
versionSanityCheck();
}
}
bind()方法岸浑,首先獲取實現(xiàn)日志的加載路徑,檢查路徑是否合法瑰步,然后初始化StaticLoggerBinder的對象矢洲,尋找合適的實現(xiàn)方式使用。
private static final void bind() {
try {
Set staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
emitSubstituteLoggerWarning();
} 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 http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.");
} else {
failedBinding(ncde);
throw ncde;
}
} catch (NoSuchMethodError nsme) {
String msg = nsme.getMessage();
if ((msg != null) && (msg.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1)) {
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);
}
}
可以看出缩焦,bind()方法中最重要的方法就是尋找實現(xiàn)方式findPossibleStaticLoggerBinderPathSet读虏,具體方法實現(xiàn)如下:
private static Set findPossibleStaticLoggerBinderPathSet() {
Set staticLoggerBinderPathSet = new LinkedHashSet();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration paths;
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 ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
Slf4j的絕妙之處就在于此,類加載器加載類袁滥,也就是說尋找StaticLoggerBinder.class文件盖桥,然后只要實現(xiàn)了這個類的日志組件,都可以作為一種實現(xiàn)题翻,如果有多個實現(xiàn)葱轩,那么誰先加載就使用誰,這個地方涉及JVM的類加載機制藐握。
橋接
Slf4j與其他日志組件的橋接(Bridge)
- slf4j-log4j12-1.7.13.jar
log4j1.2版本的橋接器 - slf4j-jdk14-1.7.13.jar
java.util.logging的橋接器 - slf4j-nop-1.7.13.jar
NOP橋接器 - slf4j-simple-1.7.13.jar
一個簡單實現(xiàn)的橋接器 - slf4j-jcl-1.7.13.jar
Jakarta Commons Logging 的橋接器. 這個橋接器將SLF4j所有日志委派給JCL - logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar)
slf4j的原生實現(xiàn)靴拱,logback直接實現(xiàn)了slf4j的接口,因此使用slf4j與logback的結(jié)合使用也意味更小的內(nèi)存與計算開銷
Slf4j Manual中有一張圖清晰的展示了接入方式猾普,如下:
Bridging legacy APIs(橋接遺留的api)
- log4j-over-slf4j-version.jar * 將log4j重定向到slf4j
- jcl-over-slf4j-version.jar
將commos logging里的Simple Logger重定向到slf4j - jul-to-slf4j-version.jar
將Java Util Logging重定向到slf4j
橋接注意事項
在使用slf4j橋接時要注意避免形成死循環(huán)袜炕,在項目依賴的jar包中不要存在以下情況
- log4j-over-slf4j.jar和slf4j-log4j12.jar同時存在
從名字上就能看出,前者重定向給后者初家,后者又委派給前者偎窘,會形成死循環(huán) - jul-to-slf4j.jar和slf4j-jdk14.jar同時存在
從名字上就能看出,前者重定向給后者溜在,后者又委派給前者陌知,會形成死循環(huán)
小結(jié)
切記阿里Java開發(fā)手冊--日志規(guī)約第一條
【強制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j、Logback)中的 API掖肋,而應(yīng)依賴使用日志框架 SLF4J 中的 APILog使用基本原則
在嵌入式組件(如庫和框架)開發(fā)時仆葡,不應(yīng)聲明對任何SLF4J綁定實現(xiàn)的依賴性,而只依賴于slf4j-api志笼。為了更好的了解Slf4j沿盅,你需要了解:
1. JVM類加載機制
2. 設(shè)計模式:門面模式、橋接模式簡單總結(jié)Slf4j的原理:
1. 通過工廠類纫溃,提供一個的接口腰涧,用戶可以通過這個門面,直接使用API實現(xiàn)日志的記錄紊浩。
2. 而具體實現(xiàn)由Slf4j來尋找加載窖铡,尋找的過程,就是通過類加載加載org/slf4j/impl/StaticLoggerBinder.class的文件坊谁,只要實現(xiàn)了這個文件的日志實現(xiàn)系統(tǒng)费彼,都可以作為一種實現(xiàn)方式。
3. 如果找到很多種方式呜袁,那么就尋找一種默認的方式敌买。
4. 這就是日志接口的工作方式,簡單高效阶界,關(guān)鍵是完全解耦虹钮,不需要日志實現(xiàn)部分提供任何的修改配置,只需要符合接口的標準就可以加載進來膘融,有利于維護和各個類的日志處理方式統(tǒng)一芙粱。
參考:
https://www.slf4j.org/manual.html
https://www.slf4j.org/faq.html#slf4j_compatible
歡迎關(guān)注公眾號,有更多精彩文章