java log4j logback jcl_内部分享:如何解决Java日志框架冲突问题。
來源:https://urlify.cn/E7zEfq
# 前言
Java 有很多的日志框架可以選擇,當(dāng)同一個(gè)項(xiàng)目中出現(xiàn)多種日志框架時(shí)就很容易出現(xiàn)日志框架沖突的問題,導(dǎo)致日志打印不出來。本文將以一次典型的日志沖突排查問題為例,提供排查步驟和思路,最后分析日志框架沖突的原因。
一般行文思路都是先講 Why,再講 How,這里我顛倒了,因?yàn)橐话阌龅絾栴}的時(shí)候我們對問題背后的根本原因是一無所知的,如果我們已經(jīng)知道問題的原因,那么問題也就迎刃而解了。因此我希望先復(fù)現(xiàn)我當(dāng)時(shí)在對日志框架了解不多的情況下排查問題的思路和步驟,如何在面對未知問題找到破題思路是非常重要的技能。
# 一次典型的日志沖突排查
問題背景
在 A 工程中,日志框架配置選用了 Log4j2,master 分支上日志打印正常,但開發(fā)分支增加了代碼之后日志打印不出來。項(xiàng)目的依賴中包含了 Log4j2、Logback 等日志框架。
排查思路與過程
排查問題的時(shí)候首先必須要有明確的思路,即大膽假設(shè),小心求證,不能像無頭蒼蠅一樣亂試。從問題的現(xiàn)象看,直覺上可以得出幾個(gè)假設(shè):
服務(wù)器環(huán)境有問題
開發(fā)分支的 Log4j2 配置有問題
接下來就是驗(yàn)證假設(shè),首先多申請幾臺(tái)機(jī)器部署項(xiàng)目分支,發(fā)現(xiàn)問題仍然存在,可以排除第一個(gè)假設(shè)。其次找到另一個(gè)工程 B 跟 A 工程對比 Log4j2 的配置,也沒有發(fā)現(xiàn)明細(xì)的差異,可以排除第二個(gè)假設(shè)。
在已有假設(shè)都驗(yàn)證失敗的情況下,需要收集更多的信息作出判斷,接下來就是要用對照實(shí)驗(yàn)收集信息。于是我分別斷點(diǎn)了 A 和 B 兩個(gè)工程,觀察它們?nèi)罩緦?shí)體的類型是否一致。結(jié)果發(fā)現(xiàn)兩者的日志實(shí)體類型不一樣,A 的日志實(shí)現(xiàn)是 Logback,B 的日志實(shí)現(xiàn)是 Log4j2,很明顯 A 打印不出日志是因?yàn)槿罩緦?shí)體不對,但是兩者都是用的同一個(gè) LoggerFactory 創(chuàng)建 Logger 的。從對照實(shí)驗(yàn)的結(jié)果來看,可以得出一個(gè)假設(shè):依賴沖突導(dǎo)致了 A 運(yùn)行時(shí)使用日志實(shí)體不是 Log4j2。
至此我們已經(jīng)找到了問題的大致方向,接下來就是要排包。排包一般有兩種思路:
暴力求解:把所有可能沖突的日志包排掉,一個(gè)個(gè)試。
精準(zhǔn)爆破:利用類加載的信息判斷運(yùn)行時(shí)加載的具體是哪個(gè) jar
暴力求解的方式太花費(fèi)時(shí)間了,所以我用的第二種方式。
獲取日志實(shí)體的方式如下:
private static final Logger LOGGER = LoggerFactory .getLogger(xxx.class);LoggerFactory 的代碼如下:
public abstract class LoggerFactory extends LogFactory { public static Logger getLogger(Class clazz) { ClassLoader oldTccl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(LoggerFactory.class.getClassLoader()); return getLogger(getLog(clazz)); } finally { Thread.currentThread().setContextClassLoader(oldTccl); } }}從代碼上可以發(fā)現(xiàn),getLog方法是來自父類LogFactory,當(dāng)我去嘗試獲取LogFactory的實(shí)現(xiàn)時(shí)候,發(fā)現(xiàn)竟然有 3 個(gè) jar 中都有同樣包名的LogFactory實(shí)現(xiàn)。于是我斷點(diǎn)了 A 和 B 工程的代碼,用 IDEA 的運(yùn)行代碼功能執(zhí)行以下命令獲取LogFactory的加載信息。
結(jié)果發(fā)現(xiàn) B 工程使用是spring-jcl,A 使用的是jcl-over-slf4j,然后排除掉 A 中jcl-over-slf4j,問題解決。
上面的排查過程中,關(guān)鍵的地方有兩點(diǎn):
定位到問題的根源是類加載沖突,確定排查方向。
通過斷點(diǎn)獲取沖突類的加載信息,快速定位到?jīng)_突的 jar。
# 為什么日志框架會(huì)沖突
問題至此就解決了,但是還有一個(gè)更深入的問題沒有解決:為什么同時(shí)存在多個(gè)日志框架的時(shí)候就會(huì)出現(xiàn)沖突呢?在解決完問題之后,我深入研究了日志框架的歷史和設(shè)計(jì),發(fā)現(xiàn)原來這跟日志框架的實(shí)現(xiàn)機(jī)制有關(guān)系。
日志框架的歷史
首先要從日志框架的發(fā)展歷史開始說起。
首先登場是Java Util Log,簡稱JUL,是JDK 中自帶的 log 功能。雖然是官方自帶的,JUL 的使用卻不廣泛,主要是因?yàn)楣δ鼙容^簡單,不好用。
然后Log4j 1.x就登場了:它是 Gülcü 設(shè)計(jì)實(shí)現(xiàn)的日志框架,設(shè)計(jì)非常優(yōu)秀,是非常廣泛使用的框架。
Commons Logging:簡稱 JCL,是 Apache 的項(xiàng)目。JCL 是一個(gè) Log Facade,只提供 Log API,不提供實(shí)現(xiàn),用 Adapter 來使用 Log4j 或者 JUL 作為 Log Implementation。目的是統(tǒng)一日志接口規(guī)范,適配多種日志實(shí)現(xiàn)。
SLF4J/Logback:SLF4J(The Simple Logging Facade for Java) 和 Logback 也是 Gülcü 創(chuàng)立的項(xiàng)目,其創(chuàng)立主要是為了提供更高性能的實(shí)現(xiàn)。其中,SLF4j 是類似于JCL 的Log Facade,Logback 是類似于Log4j 的 Log Implementation。這老哥覺得 JCL 的接口設(shè)計(jì)不好,所以重新設(shè)計(jì)了一套。
Log4j2:維護(hù) Log4j 的人為了不讓 Log4j 的用戶被 SLF4J/Logback 搶走,所以搞出了新的日志框架。Log4j2 和 Log4j1.x 并不兼容,設(shè)計(jì)上很大程度上模仿了 SLF4J/Logback,性能上也獲得了很大的提升。Log4j2 也做了 Facade/Implementation 分離的設(shè)計(jì),分成了 log4j-api 和 log4j-core。
至此我們已經(jīng)有了三個(gè)的 Log 接口和四個(gè) Log 實(shí)現(xiàn),果然程序員真的是愛造輪子。出現(xiàn)這么多框架之后,有人開始搞各個(gè)框架之間的橋接,你兼容我,我兼容你,如下圖所示。
因?yàn)楹芏?jar 使用的日志框架不同,所以經(jīng)常會(huì)出現(xiàn)引入 jar 包之后導(dǎo)致日志類沖突,前面我們排查的那個(gè)問題就是因?yàn)橐肓?jcl-over-slf4j 的橋接包。
動(dòng)態(tài)加載日志實(shí)現(xiàn)
前面我們提到日志框架分為日志接口和日志實(shí)現(xiàn),只要我們代碼中使用的是日志接口(JCL、SLF4J),我們可以隨時(shí)替換日志的實(shí)現(xiàn)。
SLF4J 加載日志實(shí)現(xiàn)的方式
SLF4J 加載日志實(shí)現(xiàn)分為兩個(gè)步驟:
獲取 ILoggerFactory 日志工廠
根據(jù) ILoggerFactory 獲取 Logger
SLF4J 要求日志實(shí)現(xiàn) jar 包都要實(shí)現(xiàn) StaticLoggerBinder 這個(gè)類,而且要放在指定目錄:org/slf4j/impl/StaticLoggerBinder.class,SLF4J 的LoggerFactory會(huì)去掃描所有 jar 包中的這個(gè)地址,參考下面的代碼。
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";static SetfindPossibleStaticLoggerBinderPathSet() { LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet(); //加載所有可能日志工廠類 try { ClassLoader ioe = LoggerFactory.class.getClassLoader(); Enumeration paths; if(ioe == null) { paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); } else { paths = ioe.getResources(STATIC_LOGGER_BINDER_PATH); } while(paths.hasMoreElements()) { URL path = (URL)paths.nextElement(); staticLoggerBinderPathSet.add(path); } } catch (IOException var4) { Util.report("Error getting resources from path", var4); } return staticLoggerBinderPathSet; }雖然它掃描了多個(gè)日志實(shí)現(xiàn),但實(shí)際上同名類 JVM 只能存在一個(gè),它這里掃描的目的是為了打印日志告訴用戶有多少個(gè)日志實(shí)現(xiàn)在依賴包中。下面的代碼返回的是最終使用的日志實(shí)現(xiàn)。
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: //這里通過靜態(tài)方法返回真正使用的日志工程類 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"); }你可能要問了,同時(shí)存在多個(gè)日志實(shí)現(xiàn)類的時(shí)候,到底是用的是哪個(gè)?答案很簡單,因?yàn)?SLF4J 利用了靜態(tài)類來加載日志工程,實(shí)際上就是讓 JVM 決定使用哪個(gè)類:哪個(gè)被先加載到 JVM 中就用哪個(gè)。為了搞清楚這個(gè)問題的答案,我特地去看了URLClassPath加載類的實(shí)現(xiàn),它就是按照 jar 加入到 URLClassPath的順序遍歷掃描,找到第一個(gè)符合條件的就返回。
JCL 加載日志實(shí)現(xiàn)的方式
相比 SLF4J 比較任性的加載方式(依賴 JVM 加載類的順序),JCL 提供了更多的配置能力,可以指定使用哪一個(gè)日志工程類。
類似的,JCL 也分為兩個(gè)步驟加載日志實(shí)現(xiàn):
獲取 LogFactory 日志工廠類
根據(jù) LogFactory 獲取 Logger
首先是獲取 LogFactory:
先從系統(tǒng)屬性中讀取系統(tǒng)屬性System.getProperty("org.apache.commons.logging.LogFactory")
使用 Java 的 SPI 機(jī)制,來搜尋對應(yīng)的實(shí)現(xiàn):META-INF/services/org.apache.commons.logging.LogFactory,這里就不對 SPI 進(jìn)行過多介紹了,簡單來說就是搜尋哪些 jar 包中含有搜尋含有上述文件,該文件中指明了對應(yīng)的 LogFactory 實(shí)現(xiàn)
從 commons-logging 的配置文件中 commons-logging.properties 尋找org.apache.commons.logging.LogFactory的值
最后還沒找到的話,使用默認(rèn)的org.apache.commons.logging.impl.LogFactoryImpl
找到 LogFactory 之后就根據(jù) LogFactory 獲取 Logger,這個(gè)根據(jù)不同的 LogFactory 實(shí)現(xiàn)有不同的方式。前面我遇到那個(gè)問題就是因?yàn)轭悰_突導(dǎo)致使用了 SLJ4J 的 LogFactory ,加載了錯(cuò)誤的 Logger。
# 總結(jié)
開發(fā)過程中總會(huì)遇到奇奇怪怪的問題,有無處下手的感覺時(shí)先穩(wěn)住心態(tài),按照大膽假設(shè),小心求證的方式進(jìn)行排查,實(shí)在沒有思路往往是因?yàn)榛A(chǔ)還不扎實(shí)。像這次日志打印不出來的問題,如果了解日志框架的加載實(shí)現(xiàn),很容易就能定位到問題;差一點(diǎn)的像我不了解日志框架的實(shí)現(xiàn),但是我可以根據(jù)之前對類加載機(jī)制的了解也能解決問題;如果對類加載機(jī)制不了解,那基本上是無解了。因此,要把問題當(dāng)做學(xué)習(xí)機(jī)會(huì),不光要解決問題,還要深挖背后的原理,做好總結(jié),這樣才能為解決更多的問題打下扎實(shí)基礎(chǔ)。
熱文推薦圖解:利用PostMan+Newman+Jenkins實(shí)現(xiàn)Web API持續(xù)集成...
大廠高頻面試題:給我說說高并發(fā)下接口冪等性的解決方案?本文匯總了所有解決方案。
牛逼,利用多路復(fù)用實(shí)現(xiàn)單服百萬級(jí)別RPS吞吐...
覺得不錯(cuò),請給個(gè)「在看」
分享給你的朋友!
點(diǎn)我,查看更多精彩文章。
總結(jié)
以上是生活随笔為你收集整理的java log4j logback jcl_内部分享:如何解决Java日志框架冲突问题。的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言选择排序_冒泡、插入、选择排序(C
- 下一篇: jvm 调优_Java架构—JVM调优