前言
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in ...
這段提示是不是很眼熟?好像每次啟動項目都會報一下青自,但似乎又沒啥影響葫隙。
但是栽烂,某天多引一個庫后,項目就真的再也起不來了......
好吧恋脚,是時候正面Java中混亂的日志系統(tǒng)了腺办。
JVM的是一個開放包容的平臺,正因如此糟描,才造就了今日繁榮的JVM生態(tài)怀喉,但凡事有利有弊,比如這百花齊放的日志系統(tǒng)船响,相處的似乎就不那么愉快躬拢。
提到日志系統(tǒng)躲履,我們先來羅列一下幾個有名的Java日志框架:
- log4j
- commons-logging
- jdk-logging
- slf4j
- logback
- log4j2
上面這幾個是在眾多日志框架中脫穎而出的,還有一些比較小眾的日志框架估灿,比如jboss-logging崇呵,gwt-log先暫時忽略。
這些日志框架可以說是在Java不同階段的代表馅袁,比如log4j和log4j2域慷,光從名字看就有千絲萬縷的聯(lián)系,明顯后者是一個進化版汗销。
話說回來犹褒,框架多那是好事兒啊,我還能比較比較弛针,看看哪個和我口味叠骑,選一個用不就得了。但是削茁,相比于其他庫宙枷,日志框架是比較特殊的:
日志系統(tǒng)幾乎是所有庫都會用到的一個功能,每個庫由于早期的技術(shù)選型和開發(fā)者喜好等原因茧跋,可能使用了不同的日志框架慰丛。我們平時需要什么庫,Maven倉庫搜一波瘾杭,貼過來就用诅病,那叫一個爽啊,殊不知粥烁,這樣間接的引入了多少種不同的日志框架贤笆。
日志系統(tǒng)往往會盡可能早的進行初始化,并且由于日志橋接器和日志門面系統(tǒng)的存在讨阻,會嘗試做一些綁定和劫持工作(后文會提到)芥永,一旦引入多個日志框架,輕則會導(dǎo)致程序中有好幾套日志系統(tǒng)同時工作钝吮,日志輸出混亂恤左,重則會導(dǎo)致項目日志系統(tǒng)初始化死鎖,項目無法啟動搀绣。
嗯飞袋,那么咋辦呢?首先簡單分析一下上面幾個框架链患,先確定最終要使用的框架巧鸭。
上面幾個日志框架簡單分為兩類:
- 日志門面 commons-logging,slf4j
- 日志實現(xiàn) log4j麻捻,jdk-logging纲仍,logback呀袱,log4j2
這也符合Java的面向?qū)ο笤O(shè)計理念,將接口與實現(xiàn)相分離郑叠。
日志門面系統(tǒng)的出現(xiàn)其實已經(jīng)很大程度上緩解了日志系統(tǒng)的混亂夜赵,很多庫的作者也已經(jīng)意識到了日志門面系統(tǒng)的重要性,不在庫中直接使用具體的日志實現(xiàn)框架乡革。
PS:其實很多庫都會自己造一個類似slf4j的日志門面系統(tǒng)寇僧,并且綁定實現(xiàn)的優(yōu)先級不一樣。
其實說是在做選擇沸版,但事實上沒得選擇嘁傀,slf4j作為現(xiàn)代的日志門面系統(tǒng),已經(jīng)成為事實的標(biāo)準(zhǔn)视粮,并且為其他日志系統(tǒng)做了十足的兼容工作细办。
我們能做的就是選一個日志實現(xiàn)框架。
logback蕾殴,log4j2是現(xiàn)代的高性能日志實現(xiàn)框架笑撞,兩者都很給力,看喜好了钓觉。
分析
我們這里以統(tǒng)一使用slf4j & logback為例分析茴肥。
如果我們直接暴力的排除其他日志框架,可能導(dǎo)致第三方庫在調(diào)用日志接口時拋出ClassNotFound異常议谷,這里就需要用到日志系統(tǒng)橋接器。
日志系統(tǒng)橋接器說白了就是一種偷天換日的解決方案堕虹。
比如log4j-over-slf4j卧晓,即log4j -> slf4j的橋接器,這個庫定義了與log4j一致的接口(包名赴捞、類名逼裆、方法簽名均一致),但是接口的實現(xiàn)卻是對slf4j日志接口的包裝赦政,即間接調(diào)用了slf4j日志接口胜宇,實現(xiàn)了對日志的轉(zhuǎn)發(fā)。
但是恢着,jul-to-slf4j是個意外例外桐愉,畢竟JDK自帶的logging包排除不掉啊,其實是利用jdk-logging的Handler機制掰派,在root logger上install一個handler从诲,將所有日志劫持到slf4j上。要使得jul-to-slf4j生效靡羡,需要執(zhí)行
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
spring boot中的日志初始化模塊已經(jīng)包括了該邏輯系洛,故無需手動調(diào)用俊性。在使用其他框架時,建議在入口類處的static{ }
區(qū)執(zhí)行描扯,確保盡早初始化定页。
日志系統(tǒng)橋接器是個巧妙的解決方案,有些庫的作者在引用第三方庫的時候绽诚,也碰到了日志系統(tǒng)混亂的問題典徊,并順手用橋接器解決了,只不過碰巧跟你橋接的目標(biāo)不一樣憔购,橋接到了log4j宫峦。想想一下:
- log4j -> slf4j,slf4j -> log4j兩個橋接器同時存在會出現(xiàn)什么情況玫鸟?
互相委托导绷,無限循環(huán),堆棧溢出屎飘。 - slf4j -> logback妥曲,slf4j -> log4j兩個橋接器同時存在會如何?
兩個橋接器都會被slf4j發(fā)現(xiàn)钦购,在slf4j中定義了優(yōu)先順序檐盟,優(yōu)先使用logback,僅會報警押桃,發(fā)現(xiàn)多個日志框架綁定實現(xiàn)葵萎;
但有一些框架中封裝了自己的日志facade,如果其對綁定日志實現(xiàn)定義的優(yōu)先級順序與slf4j不一致唱凯,優(yōu)先使用log4j羡忘,那整個程序中就有兩套日志系統(tǒng)在工作。
上面一波分析之后磕昼,我們得出結(jié)論卷雕,為達到統(tǒng)一使用slf4j & logback的目的,必須要做4件事:
- 引入slf4j & logback日志包和slf4j -> logback橋接器票从;
- 排除common-logging漫雕、log4j、log4j2日志包峰鄙;
- 引入jdk-logging -> slf4j浸间、common-logging -> slf4j、log4j -> slf4j吟榴、log4j2 -> slf4j橋接器发框;
- 排除slf4j -> jdk-logging、slf4j -> common-logging、slf4j -> log4j梅惯、slf4j -> log4j2橋接器宪拥。
ps:log4j2橋接器由log4j2提供,其他橋接器由slf4j提供铣减。
如果再嚴(yán)謹一點她君,還要排除掉slf4j-simple、slf4j-nop兩個框架葫哗,不過這兩個一般沒人用缔刹。
下面這幅圖來自slf4j官方文檔,描述了橋接器的工作原理劣针。
來自開源中國的一篇博文校镐,也比較詳細的分析了各個橋接器的工作原理,奉上傳送門:https://my.oschina.net/pingpangkuangmo/blog/410224
上述提到了這么多日志系統(tǒng)的橋接器捺典,但似乎沒有提到logback -> slf4j的橋接器鸟廓,如果我們?nèi)罩緦崿F(xiàn)系統(tǒng)選擇log4j2,怎么處理logback襟己?
其實logback在設(shè)計上引谜,天生綁定sfl4j,可以認為從根源上避免了直接被使用擎浴,自然也不需要logbak -> slf4j的橋接器员咽。
Gradle實戰(zhàn)
Gradle作為更現(xiàn)代的項目管理工具,實現(xiàn)上述步驟只需:
buildscript {
// 定義全局變量
ext {
slf4j_version = '1.7.25'
log4j2_version = '2.11.1'
logback_version = '1.2.3'
}
}
// 全局排除依賴
configurations {
// 支持通過group贮预、module排除贝室,可以同時使用
all*.exclude group: 'commons-logging', module: 'commons-logging' // common-logging
all*.exclude group: 'log4j', module: 'log4j' // log4j
all*.exclude group: 'org.apache.logging.log4j', module: 'log4j-core' // slf4j -> log4j2
all*.exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' // log4j2
all*.exclude group: 'org.slf4j', module: 'slf4j-jdk14' // slf4j -> jdk-logging
all*.exclude group: 'org.slf4j', module: 'slf4j-jcl' // slf4j -> common-logging
all*.exclude group: 'org.slf4j', module: 'slf4j-log4j12' // slf4j -> log4j
}
// 引入依賴
dependencies {
// log
compile "org.slf4j:slf4j-api:$slf4j_version"
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
compile "org.apache.logging.log4j:log4j-api:$log4j2_version"
compile "org.apache.logging.log4j:log4j-to-slf4j:$log4j2_version"
compile "ch.qos.logback:logback-classic:$logback_version"
}
如果選擇log4j2作為日志實現(xiàn)框架
buildscript {
// 定義全局變量
ext {
slf4j_version = '1.7.25'
log4j2_version = '2.11.1'
logback_version = '1.2.3'
}
}
// 全局排除依賴
configurations {
// 支持通過group、module排除仿吞,可以同時使用
all*.exclude group: 'commons-logging', module: 'commons-logging' // common-logging
all*.exclude group: 'log4j', module: 'log4j' // log4j
all*.exclude group: 'ch.qos.logback', module: 'logback-core' // logback
all*.exclude group: 'ch.qos.logback', module: 'logback-classic' // slf4j -> logback
all*.exclude group: 'org.slf4j', module: 'slf4j-jdk14' // slf4j -> jdk-logging
all*.exclude group: 'org.slf4j', module: 'slf4j-jcl' // slf4j -> common-logging
all*.exclude group: 'org.slf4j', module: 'slf4j-log4j12' // slf4j -> log4j
}
// 引入依賴
dependencies {
// log
compile "org.slf4j:slf4j-api:$slf4j_version"
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
compile "org.apache.logging.log4j:log4j-core:$log4j2_version"
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j2_version"
}
Gradle的依賴管理十分靈活滑频,有篇博客介紹了其依賴管理的更多特性,傳送門:
http://www.zhaiqianfeng.com/2017/03/love-of-gradle-dependencies-1.html
Maven實戰(zhàn)
在步驟1茫藏、3依賴引入方面Maven沒有什么問題误趴,但是在步驟2霹琼、4依賴排除方面务傲,相比Gradle,Maven沒有直接提供全局依賴排除機制枣申,我們需要借助一些方法間接達到目的售葡。
Provided Scope
<project>
[...]
<properties>
<slf4j.version>1.7.25</slf4j.version>
<commons-logging.version>1.2</commons-logging.version>
<log4j.version>1.2.17</log4j.version>
<log4j2.version>2.11.1</log4j2.version>
<logback.version>1.2.3</logback.version>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jcl</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback_version}</version>
</dependency>
</dependencies>
[...]
</project>
version99倉庫
我們來分析一下Maven依賴的工作原理,在一個依賴庫被直接或間接引入多次時忠藤,并且版本不一致挟伙,maven在解析依賴的時候,有兩個仲裁原則:
- 路徑最短優(yōu)先原則
- 優(yōu)先聲明原則
首先遵循路徑最短優(yōu)先原則模孩,即直接引入最優(yōu)先尖阔,傳遞依賴層級越淺贮缅,越優(yōu)先。若依然無法仲裁介却,則遵循優(yōu)先聲明原則谴供,在pom中聲明靠前的優(yōu)先。
既然了解了這個規(guī)則齿坷,那就可以巧妙的利用一下桂肌,如果我們在pom的最開始,引入了一個虛包永淌,則該包其他的依賴全部失效崎场,也就達到了全局排除依賴的目的。
slf4j的文檔中也提到了該方案遂蛀,并且提供了一個version99倉庫谭跨,里面有幾個用于排除其他日志框架的虛包。
<project>
[...]
<repositories>
<--! 首先添加version99倉庫 -->
<repository>
<id>version99</id>
<url>http://version99.qos.ch/</url>
</repository>
</repositories>
<--! 直接引入依賴答恶,放置在最前 -->
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>99-empty</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging-api</artifactId>
<version>99-empty</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>99-empty</version>
</dependency>
</dependencies>
<--! 通過dependencyManagement強制指定依賴版本也可達到同樣效果 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>99-empty</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging-api</artifactId>
<version>99-empty</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>99-empty</version>
</dependency>
</dependencies>
</dependencyManagement>
[...]
</project>
這個version99倉庫是slf4j提供的一個靜態(tài)Maven倉庫饺蚊,里面只有這3個虛包,是不能滿足其他要求的悬嗓,我們可以照葫蘆畫瓢污呼,制作其他虛包上傳到Nexus。
當(dāng)然包竹,發(fā)揮一下腦洞燕酷,可以分析一下Maven下載依賴的機制,編程實現(xiàn)一個動態(tài)的Maven倉庫周瞎,請求任何empty版本的依賴包都返回一個虛包苗缩。
這里奉上一個傳送門:
https://github.com/erikvanoosten/version99
嗯,還是Gradle更優(yōu)雅声诸!