作者:空無
連結:
https://juejin.cn/post/6945220055399399455前言
你是否遇到過配置了日志,但列印不出來的情況?
你是否遇到過配置了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]
複制代碼
你是否遇到過DUBBO日志列印不正常的情況?
你是否遇到過Mybatis SQL日志列印不出來的情況?
你是否遇到過JPA/Hibernate SQL日志無法列印的情況?
你是否遇到過複雜項目中,很多架構内部日志無法列印的情況?
你是否遇到過Tomcat工程,日志檔案列印了多份,catalina.out和其他檔案?
你是否遇到過SpringBoot項目,日志檔案列印了多份的問題?
你是否遇到過各種日志配置問題……
日志架構的沖突
上面的這些問題,基本都是由于多套日志架構共存或配置錯誤導緻的。
那麼為什麼會出現共存或者沖突呢?
一般是以下幾種原因:
項目手動引用了各種日志架構的包 - 比如同時引用了log4j/log4j2/logback/jboss-logging/jcl等
包管理工具的傳遞依賴(Transitive Dependencies)導緻,比如依賴了dubbo,但是dubbo依賴了zkclient,可zkclient又依賴了log4j,此時如果你的項目中還有其他日志架構存在并有使用,那麼就會導緻多套共存
同一個日志架構多版本共存
JAVA裡的各種日志架構
在正式介紹沖突和解決之前,需要先簡單的說一下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 最新教程推薦看下這個:
https://github.com/javastacks/spring-boot-best-practice現在啟動這個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
來看一下依賴圖:
這個時候就亂套了,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環境變量指定一個具體的日志架構實作
項目裡統一了日志架構之後,無論用那種日志架構列印,最終還是走向我們中轉/适配後的唯一一個日志架構。
解決了共存/沖突之後,項目裡就隻剩一款日志架構。再也不會出現“日志打不出”,“日志配置不生效”之類的各種惡心問題,下班都能早點了!