優(yōu)雅的使用slf4j

前言

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)系,明顯后者是一個進化版汗销。

話說回來犹褒,框架多那是好事兒啊,我還能比較比較弛针,看看哪個和我口味叠骑,選一個用不就得了。但是削茁,相比于其他庫宙枷,日志框架是比較特殊的:

  1. 日志系統(tǒng)幾乎是所有庫都會用到的一個功能,每個庫由于早期的技術(shù)選型和開發(fā)者喜好等原因茧跋,可能使用了不同的日志框架慰丛。我們平時需要什么庫,Maven倉庫搜一波瘾杭,貼過來就用诅病,那叫一個爽啊,殊不知粥烁,這樣間接的引入了多少種不同的日志框架贤笆。

  2. 日志系統(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件事:

  1. 引入slf4j & logback日志包和slf4j -> logback橋接器票从;
  2. 排除common-logging漫雕、log4j、log4j2日志包峰鄙;
  3. 引入jdk-logging -> slf4j浸间、common-logging -> slf4j、log4j -> slf4j吟榴、log4j2 -> slf4j橋接器发框;
  4. 排除slf4j -> jdk-logging、slf4j -> common-logging、slf4j -> log4j梅惯、slf4j -> log4j2橋接器宪拥。

ps:log4j2橋接器由log4j2提供,其他橋接器由slf4j提供铣减。
如果再嚴(yán)謹一點她君,還要排除掉slf4j-simple、slf4j-nop兩個框架葫哗,不過這兩個一般沒人用缔刹。

下面這幅圖來自slf4j官方文檔,描述了橋接器的工作原理劣针。

slf4j.png

來自開源中國的一篇博文校镐,也比較詳細的分析了各個橋接器的工作原理,奉上傳送門: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)雅声诸!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末酱讶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子彼乌,更是在濱河造成了極大的恐慌泻肯,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慰照,死亡現(xiàn)場離奇詭異灶挟,居然都是意外死亡,警方通過查閱死者的電腦和手機毒租,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門稚铣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事惕医「” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵抬伺,是天一觀的道長孤个。 經(jīng)常有香客問我,道長沛简,這世上最難降的妖魔是什么齐鲤? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮椒楣,結(jié)果婚禮上给郊,老公的妹妹穿的比我還像新娘。我一直安慰自己捧灰,他們只是感情好淆九,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毛俏,像睡著了一般炭庙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上煌寇,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天焕蹄,我揣著相機與錄音,去河邊找鬼阀溶。 笑死腻脏,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的银锻。 我是一名探鬼主播永品,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼击纬!你這毒婦竟也來了鼎姐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤更振,失蹤者是張志新(化名)和其女友劉穎炕桨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體殃饿,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡谋作,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年芋肠,在試婚紗的時候發(fā)現(xiàn)自己被綠了乎芳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖奈惑,靈堂內(nèi)的尸體忽然破棺而出吭净,到底是詐尸還是另有隱情,我是刑警寧澤肴甸,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布寂殉,位于F島的核電站,受9級特大地震影響原在,放射性物質(zhì)發(fā)生泄漏友扰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一庶柿、第九天 我趴在偏房一處隱蔽的房頂上張望村怪。 院中可真熱鬧,春花似錦浮庐、人聲如沸甚负。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽梭域。三九已至,卻和暖如春搅轿,著一層夾襖步出監(jiān)牢的瞬間病涨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工璧坟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留没宾,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓沸柔,卻偏偏與公主長得像循衰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子褐澎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容