優雅的使用slf4j

前言

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in ...

這段提示是不是很眼熟?好像每次啟動項目都會報一下,但似乎又沒啥影響。
但是,某天多引一個庫后,項目就真的再也起不來了......

好吧,是時候正面Java中混亂的日志系統了。

JVM的是一個開放包容的平臺,正因如此,才造就了今日繁榮的JVM生態,但凡事有利有弊,比如這百花齊放的日志系統,相處的似乎就不那么愉快。

提到日志系統,我們先來羅列一下幾個有名的Java日志框架:

  • log4j
  • commons-logging
  • jdk-logging
  • slf4j
  • logback
  • log4j2

上面這幾個是在眾多日志框架中脫穎而出的,還有一些比較小眾的日志框架,比如jboss-logging,gwt-log先暫時忽略。

這些日志框架可以說是在Java不同階段的代表,比如log4j和log4j2,光從名字看就有千絲萬縷的聯系,明顯后者是一個進化版。

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

  1. 日志系統幾乎是所有庫都會用到的一個功能,每個庫由于早期的技術選型和開發者喜好等原因,可能使用了不同的日志框架。我們平時需要什么庫,Maven倉庫搜一波,貼過來就用,那叫一個爽啊,殊不知,這樣間接的引入了多少種不同的日志框架。

  2. 日志系統往往會盡可能早的進行初始化,并且由于日志橋接器和日志門面系統的存在,會嘗試做一些綁定和劫持工作(后文會提到),一旦引入多個日志框架,輕則會導致程序中有好幾套日志系統同時工作,日志輸出混亂,重則會導致項目日志系統初始化死鎖,項目無法啟動。

嗯,那么咋辦呢?首先簡單分析一下上面幾個框架,先確定最終要使用的框架。

上面幾個日志框架簡單分為兩類:

  • 日志門面 commons-logging,slf4j
  • 日志實現 log4j,jdk-logging,logback,log4j2

這也符合Java的面向對象設計理念,將接口與實現相分離。

日志門面系統的出現其實已經很大程度上緩解了日志系統的混亂,很多庫的作者也已經意識到了日志門面系統的重要性,不在庫中直接使用具體的日志實現框架。
PS:其實很多庫都會自己造一個類似slf4j的日志門面系統,并且綁定實現的優先級不一樣。

其實說是在做選擇,但事實上沒得選擇,slf4j作為現代的日志門面系統,已經成為事實的標準,并且為其他日志系統做了十足的兼容工作。

我們能做的就是選一個日志實現框架。
logback,log4j2是現代的高性能日志實現框架,兩者都很給力,看喜好了。

分析

我們這里以統一使用slf4j & logback為例分析。

如果我們直接暴力的排除其他日志框架,可能導致第三方庫在調用日志接口時拋出ClassNotFound異常,這里就需要用到日志系統橋接器

日志系統橋接器說白了就是一種偷天換日的解決方案。
比如log4j-over-slf4j,即log4j -> slf4j的橋接器,這個庫定義了與log4j一致的接口(包名、類名、方法簽名均一致),但是接口的實現卻是對slf4j日志接口的包裝,即間接調用了slf4j日志接口,實現了對日志的轉發。
但是,jul-to-slf4j是個意外例外,畢竟JDK自帶的logging包排除不掉啊,其實是利用jdk-logging的Handler機制,在root logger上install一個handler,將所有日志劫持到slf4j上。要使得jul-to-slf4j生效,需要執行

 SLF4JBridgeHandler.removeHandlersForRootLogger();
 SLF4JBridgeHandler.install();

spring boot中的日志初始化模塊已經包括了該邏輯,故無需手動調用。在使用其他框架時,建議在入口類處的static{ }區執行,確保盡早初始化。

日志系統橋接器是個巧妙的解決方案,有些庫的作者在引用第三方庫的時候,也碰到了日志系統混亂的問題,并順手用橋接器解決了,只不過碰巧跟你橋接的目標不一樣,橋接到了log4j。想想一下:

  • log4j -> slf4j,slf4j -> log4j兩個橋接器同時存在會出現什么情況?
    互相委托,無限循環,堆棧溢出。
  • slf4j -> logback,slf4j -> log4j兩個橋接器同時存在會如何?
    兩個橋接器都會被slf4j發現,在slf4j中定義了優先順序,優先使用logback,僅會報警,發現多個日志框架綁定實現;
    但有一些框架中封裝了自己的日志facade,如果其對綁定日志實現定義的優先級順序與slf4j不一致,優先使用log4j,那整個程序中就有兩套日志系統在工作。

上面一波分析之后,我們得出結論,為達到統一使用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提供。
如果再嚴謹一點,還要排除掉slf4j-simple、slf4j-nop兩個框架,不過這兩個一般沒人用。

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

slf4j.png

來自開源中國的一篇博文,也比較詳細的分析了各個橋接器的工作原理,奉上傳送門:https://my.oschina.net/pingpangkuangmo/blog/410224

上述提到了這么多日志系統的橋接器,但似乎沒有提到logback -> slf4j的橋接器,如果我們日志實現系統選擇log4j2,怎么處理logback?

其實logback在設計上,天生綁定sfl4j,可以認為從根源上避免了直接被使用,自然也不需要logbak -> slf4j的橋接器。

Gradle實戰

Gradle作為更現代的項目管理工具,實現上述步驟只需:

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作為日志實現框架

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實戰

在步驟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在解析依賴的時候,有兩個仲裁原則:

  • 路徑最短優先原則
  • 優先聲明原則

首先遵循路徑最短優先原則,即直接引入最優先,傳遞依賴層級越淺,越優先。若依然無法仲裁,則遵循優先聲明原則,在pom中聲明靠前的優先。

既然了解了這個規則,那就可以巧妙的利用一下,如果我們在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提供的一個靜態Maven倉庫,里面只有這3個虛包,是不能滿足其他要求的,我們可以照葫蘆畫瓢,制作其他虛包上傳到Nexus。
當然,發揮一下腦洞,可以分析一下Maven下載依賴的機制,編程實現一個動態的Maven倉庫,請求任何empty版本的依賴包都返回一個虛包。

這里奉上一個傳送門:
https://github.com/erikvanoosten/version99

嗯,還是Gradle更優雅!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容