Java 日誌框架適配、衝突解決方案
前言
你是否遇到過配置了日誌,但打印不出來的情況?
你是否遇到過配置了 logback,啓動時卻提示 log4j 錯誤的情況?像下面這樣:
log4j:WARN No appenders could be found for logger (org.example.App).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
你是否遇到過 SLF4J 的這種報錯?
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/org/slf4j/slf4j-log4j12/1.7.30/slf4j-log4j12-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
日誌框架的衝突
上面的這些問題,基本都是由於多套日誌框架共存或配置錯誤導致的。那麼爲什麼會出現共存或者衝突呢?
-
項目手動引用了各種日誌框架的包 - 比如同時引用了 log4j/log4j2/logback/jboss-logging/jcl 等
-
包管理工具的傳遞依賴(Transitive Dependencies)導致,比如依賴了 dubbo,但是 dubbo 依賴了 zkclient,可 zkclient 又依賴了 log4j,此時如果你的項目中還有其他日誌框架存在並有使用,那麼就會導致多套共存
-
同一個日誌框架多版本共存
JAVA 裏的各種日誌框架
Java 中的日誌框架分爲兩種,分別爲日誌抽象 / 門面,日誌實現
日誌抽象 / 門面
日誌抽象 / 門面,他們不負責具體的日誌打印,如輸出到文件、配置日誌內容格式等。他們只是一套日誌抽象,定義了一套統一的日誌打印標準,如 Logger 對象,Level 對象。
slf4j(Simple Logging Facade for Java)和 jcl(Apache Commons Logging)這兩個日誌框架就是 JAVA 中最主流的日誌抽象了。還有一個 jboss-logging,主要用於 jboss 系列軟件,比如 hibernate 之類。像 jcl 已經多年不更新了(上一次更新時間還是 14 年),目前最推薦的是使用 slf4j
日誌實現
Java 中的日誌實現框架,主流的有以下幾種:
-
log4j - Apache(老牌日誌框架,不過多年不更新了,新版本爲 log4j2)
-
log4j2 - Apache(log4j 的新版本,目前異步 IO 性能最強,配置也較簡單)
-
logback - QOS(slf4j 就是這家公司的產品)
-
jul(java.util.logging) - jdk 內置
在程序中,可以直接使用日誌框架,也可以使用日誌抽象 + 日誌實現搭配的方案。不過一般都是用日誌抽象 + 日誌實現,這樣更靈活,適配起來更簡單。
目前最主流的方案是 slf4j+logback/log4j2,不過如果是 jboss 系列的產品,可能用的更多的還是 jboss-logging,畢竟親兒子嘛。像 JPA/Hibernate 這種框架裏,內置的就是 jboss-logging
SpringBoot + Dubbo 日誌框架衝突的例子
舉個例子來說個最常見的傳遞依賴導致的共存衝突:
比如我有一個 “乾淨的”spring-boot 項目,乾淨到只有一個 spring-boot-starter 依賴,此時我想集成 dubbo,使用 zookeeper 作爲註冊中心,此時我的依賴配置是這樣:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.9</version>
</dependency>
</dependencies>
現在啓動這個 spring-boot 項目,會發現一堆紅色錯誤:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/org/slf4j/slf4j-log4j12/1.7.30/slf4j-log4j12-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
----------------------------------人肉分割線----------------------------------------
log4j:WARN No appenders could be found for logger (org.apache.dubbo.common.logger.LoggerFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
從錯誤提示上看,錯誤內容分爲兩個部分:
-
slf4j 報錯,提示找到多個 slf4j 的日誌綁定
-
log4j 報錯,提示 log4j 沒有 appender 配置
出現這個錯誤,就是因爲 dubbo 的傳遞依賴中含有 log4j,但是 spring-boot 的默認配置是 slf4j+logback。在依賴了 dubbo 相關包之後,現在項目中同時存在logback/jcl(apache commons-logging)/log4j/jul-to-slf4j/slf4j-log4j/log4j-to-slf4j
(搜索公衆號 Java 知音,回覆 “2021”,送你一份 Java 面試題寶典)
來看一下依賴圖:
這個時候就亂套了,slf4j-log4j 是 log4j 的 slf4j 實現,作用是調用 slf4j api 的時候使用 log4j 輸出;而 log4j-to-slf4j 的作用是將 log4j 的實現替換爲 log4j,這樣一來不是死循環了
而且還有 logback 的存在,logback 默認實現了 slf4j 的抽象,而 slf4j-log4j 也是一樣實現了 slf4j 的抽象,logback,項目裏共存了兩套 slf4j 的實現,那麼在使用 slf4j 接口打印的時候會使用哪個實現呢?
答案是 “第一個”,也就是第一個被加載的 Slf4j 的實現類,但這種依靠 ClassLoader 加載順序來保證的日誌配置順序是非常不靠譜的
如果想正常使用日誌,讓這個項目裏所有的框架都正常打印日誌,必須將日誌框架統一。不過這裏的統一併不是至強行修改,而是用 “適配 / 中轉” 的方式。
現在項目裏雖然有 slf4j-log4j 的配置,但這個配置是適配 log4j2 用的,而我們的依賴了只有 log4j1,實際上這個中轉是無效的。但 logback 是有效的,而且是 spring-boot 項目的默認配置,這次就選擇 logback 作爲項目的統一日誌框架吧。
現在項目裏存在 log4j(1) 的包,而且啓動時又報 log4j 的錯誤,說明某些代碼調用了 log4j 的 api。但我們又不想用 log4j,所以需要先解決 log4j 的問題。
由於有 log4j 代碼的引用,所以直接刪除 log4j 一定是不可行的。slf4j 提供了一個 log4j-over-slf4j 的包,這個包複製了一份 log4j1 的接口類(Logger 等),同時將實現類修改爲 slf4j 了。
所以將 log4j 的(傳遞)依賴排除,同時引用 log4j-over-slf4j,就解決了這個 log4j 的問題。現在來修改下 pom 中的依賴(查看依賴圖可以使用 maven 的命令,或者是 IDEA 自帶的 Maven Dependencies Diagram,再或者 Maven Helper 之類的插件)
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.9</version>
<scope>compile</scope>
<!--排除log4j-->
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--增加log4j-slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.30</version>
</dependency>
解決了 log4j 的問題之後,現在還有 slf4j 有兩個實現的問題,這個問題處理就更簡單了。由於我們計劃使用 logback,那麼只需要排除 / 刪除 slf4j-log4j 這個實現的依賴即可
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.9</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
修改完成,再次啓動就沒有錯誤了,輕鬆解決問題
日誌適配大全
上面只是介紹了一種轉換的方式,但這麼多日誌框架,他們之間是可以互相轉換的。不過最終目的都是統一一套日誌框架,讓最終的日誌實現只有一套
這麼多的日誌適配 / 轉換方式,全記住肯定是有點難。爲此我畫了一張可能是全網最全的日誌框架適配圖(原圖尺寸較大,請點擊放大查看),如果再遇到衝突,需要將一個日誌框架轉換到另一款的時候,只需要按照圖上的路徑,引入相關的依賴包即可。
比如想把 slf4j,適配 / 轉換到 log4j2。按照圖上的路徑,只需要引用 log4j-slf4j-impl 即可。
如果想把 jcl,適配 / 轉換到 slf4j,只需要刪除 jcl 包,然後引用 jcl-over-slf4j 即可。
圖上的箭頭,有些標了文字的,是需要額外包進行轉換的,有些沒有標文字的,是內置了適配的實現。其實內置實現的這種會更麻煩,因爲如果遇到共存基本都需要通過配置環境變量 / 配置額外屬性的方式來指定一款日誌實現。
目前 slf4j 是適配方案中,最核心的那個框架,算是這個圖的中心樞紐。只要圍繞 slf4j 做適配 / 轉化,就沒有處理不了的衝突
總結
解決日誌框架共存 / 衝突問題其實很簡單,只要遵循幾個原則:
-
統一使用一套日誌實現
-
刪除多餘的無用日誌依賴
-
如果有引用必須共存的話,那麼就移除原始包,使用 “over” 類型的包(over 類型的包複製了一份原始接口,重新實現)
-
不能 over 的,使用日誌抽象提供的指定方式,例如 jboss-logging 中,可以通過 org.jboss.logging.provider 環境變量指定一個具體的日誌框架實現
項目裏統一了日誌框架之後,無論用那種日誌框架打印,最終還是走向我們中轉 / 適配後的唯一一個日誌框架。
解決了共存 / 衝突之後,項目裏就只剩一款日誌框架。再也不會出現 “日誌打不出”,“日誌配置不生效” 之類的各種噁心問題,下班都能早點了!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ey25epbwzQKE1FHahN3o9A