天天看點

Java 日志架構解析:彙總及最佳實踐基本介紹使用場景沖突處理slf4j整合日志輸出log4j2整合日志輸出日志打點API綁定實作

Java 日志架構解析:彙總及最佳實踐基本介紹使用場景沖突處理slf4j整合日志輸出log4j2整合日志輸出日志打點API綁定實作

基本介紹

在java的世界裡有許多實作日志功能的工具,最早得到廣泛使用的是 log4j,現在比較流行的是slf4j+logback。作為開發人員,我們有時候需要封裝一些元件(二方包)提供給其他人員使用,但是那麼多的日志工具,根本沒法保證每個元件裡都能使用約定好的日志工具,況且還有很多第三方的包,鬼知道他會用什麼日志工具。假如一個應用程式用到了兩個元件,恰好兩個元件使用不同的日志工具,那麼應用程式就會有兩份日志輸出了,蛋疼吧。。

下面簡單介紹下常見的日志工具:

▐ JUL

JUL 全稱 java.util.logging.Logger,JDK 自帶的日志系統,從 JDK1.4 就有了。因為 log4j 的存在,這個 logger 一直沉默着,其實在一些測試性的代碼中,jdk自帶的 logger 比 log4j 更友善。JUL是自帶具體實作的,與 log4j、logback 等類似,而不是像 JCL、slf4j 那樣的日志接口封裝。

import java.util.logging.Level;
import java.util.logging.Logger;

private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());           
  • 相同名字的Logger對象全局隻有一個;
  • 一般使用圓點分隔的層次命名空間來命名 Logger;Logger 名稱可以是任意的字元串,但是它們一般應該基于被記錄元件的包名或類名,如 java.net 或 javax.swing;
  • 配置檔案預設使用jre/lib/logging.properties,日志級别預設為INFO;

    可以通過系統屬性java.util.logging.config.file指定路徑覆寫系統預設檔案;

  • 日志級别由高到低依次為:SEVERE(嚴重)、WARNING(警告)、INFO(資訊)、CONFIG(配置)、FINE(詳細)、FINER(較詳細)、FINEST(非常詳細)。另外還有兩個全局開關:OFF「關閉日志記錄」和ALL「啟用所有消息日志記錄」。
  • 《logging.properties》檔案中,預設日志級别可以通過.level= ALL來控制,也可以基于層次命名空間來控制,按照Logger名字進行字首比對,比對度最高的優先采用;日志級别隻認大寫;
  • JUL通過handler來完成實際的日志輸出,可以通過配置檔案指定一個或者多個hanlder,多個handler之間使用逗号分隔;handler上也有一個日志級别,作為該handler可以接收的日志最低級别,低于該級别的日志,将不進行實際的輸出;handler上可以綁定日志格式化器,比如java.util.logging.ConsoleHandler就是使用的String.format來支援的;

配置檔案示例:

handlers= java.util.logging.ConsoleHandler

.level= ALL
com.suian.logger.jul.xxx.level = CONFIG
com.suian.logger.jul.xxx.demo2.level = FINE
com.suian.logger.jul.xxx.demo3.level = FINER

java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%1$tF %1$tT [%4$s] %3$s -  %5$s %n           

▐ Apache Commons Logging

之前叫Jakarta Commons Logging,簡稱JCL,是Apache提供的一個通用日志API,可以讓應用程式不再依賴于具體的日志實作工具。

commons-logging包中對其它一些日志工具,包括Log4J、Avalon LogKit、JUL等,進行了簡單的包裝,可以讓應用程式在運作時,直接将JCL API打點的日志适配到對應的日志實作工具中。

common-logging通過動态查找的機制,在程式運作時自動找出真正使用的日志庫。這一點與slf4j不同,slf4j是在編譯時靜态綁定真正的Log實作庫。

commons-logging包裡的包裝類和簡單實作列舉如下:

  • org.apache.commons.logging.impl.Jdk14Logger,适配JDK1.4裡的JUL;
  • org.apache.commons.logging.impl.Log4JLogger,适配Log4J;
  • org.apache.commons.logging.impl.LogKitLogger,适配avalon-Logkit;
  • org.apache.commons.logging.impl.SimpleLog,common-logging自帶日志實作類,它實作了Log接口,把日志消息都輸出到系統錯誤流System.err中;
  • org.apache.commons.logging.impl.NoOpLog,common-logging自帶日志實作類,它實作了Log接口,其輸出日志的方法中不進行任何操作;

如果隻引入Apache Commons Logging,也沒有通過配置檔案《commons-logging.properties》進行擴充卡綁定,也沒有通過系統屬性或者SPI重新定義LogFactory實作,預設使用的就是jdk自帶的java.util.logging.Logger來進行日志輸出。

JCL使用配置檔案commons-logging.properties,可以在該檔案中指定具體使用哪個日志工具。不配置的話,預設會使用JUL來輸出日志。配置示例:

▐ Avalon LogKit

Avalon LogKit是一個高速日志記錄工具集,Avalon裡的各個元件Framework、Excalibur、Cornerstone和Phoenix都用到它。它的模型與JDK 1.4 Logging package采用相同的原理,但與JDK 1.2+相容。使用LogKit的原因是:Context和LogTargets。

使用Log4j的時候,日志的内容隻能是一句話,而使用LogKit,你可以記錄很多項内容,甚至可以各項内容記錄到對應的資料庫字段中。如果使用Log4j存儲日志到不同的存儲媒體,如資料庫,需要使用Appender,而LogKit已經可以支援多種存儲目标。

▐ log4j

Log4j是Apache的一個開放源代碼項目,通過使用Log4j,我們可以控制日志資訊輸送的目的地是控制台、檔案、資料庫等;我們也可以控制每一條日志的輸出格式;通過定義每一條日志資訊的級别,我們能夠更加細緻地控制日志的生成過程。

Log4j有7種不同的log級别,按照等級從低到高依次為:TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF。如果配置為OFF級别,表示關閉log。Log4j支援兩種格式的配置檔案:properties和xml。包含三個主要的元件:Logger、appender、Layout。

▐ SLF4J

SLF4J全稱The Simple Logging Facade for Java,簡單日志門面,這個不是具體的日志解決方案,而是通過門面模式提供一些Java Logging API,類似于JCL。題外話,作者當時建立SLF4J的目的就是為了替代Jakarta Commons Logging(JCL)。

SLF4J提供的核心API是一些接口以及一個LoggerFactory的工廠類。在使用SLF4J的時候,不需要在代碼中或配置檔案中指定你打算使用哪個具體的日志系統,可以在部署的時候不修改任何配置即可接入一種日志實作方案,在編譯時靜态綁定真正的Log庫。

使用SLF4J時,如果你需要使用某一種日志實作,那麼你必須選擇正确的SLF4J的jar包的集合(各種橋接包)。SLF4J提供了統一的記錄日志的接口,隻要按照其提供的方法記錄即可,最終日志的格式、記錄級别、輸出方式等通過具體日志系統的配置來實作,是以可以在應用中靈活切換日志系統。

logback是slf4j-api的天然實作,不需要橋接包就可以使用。另外slf4j還封裝了很多其他的橋接包,可以使用到其他的日志實作中,比如slf4j-log4j12,就可以使用log4j進行底層日志輸出,再比如slf4j-jdk14,可以使用JUL進行日志輸出。

▐ Logback

Logback,一個“可靠、通用、快速而又靈活的Java日志架構”。Logback目前分成三個子產品:logback-core,logback- classic和logback-access。logback-core是其它兩個子產品的基礎子產品。logback-classic是log4j的一個改良版本,完整實作了SLF4J API。

logback-access子產品與Servlet容器內建提供通過Http來通路日志的功能。Logback依賴配置檔案logback.xml,當然也支援groovy方式。Logback相比log4j,有很多很多的優點,網上一搜一大片,此處就不再贅述了。

▐ Log4j2

Log4j 2是log4j 1.x和logback的改進版,據說采用了一些新技術(無鎖異步等等),使得日志的吞吐量、性能比log4j 1.x提高10倍,并解決了一些死鎖的bug,而且配置更加簡單靈活。

Log4j2支援插件式結構,可以根據需要自行擴充Log4j2,實作自己的appender、logger、filter等。在配置檔案中可以引用屬性,還可以直接替代或傳遞到元件,而且支援json格式的配置檔案。不像其他的日志架構,它在重新配置的時候不會丢失之前的日志檔案。

Log4j2利用Java5中的并發特性支援,盡可能地執行最低層次的加鎖。解決了在log4j 1.x中存留的死鎖的問題。Log4j 2是基于LMAX Disruptor庫的。在多線程的場景下,和已有的日志架構相比,異步logger擁有10倍左右的效率提升。

Log4j2體系結構:

Java 日志架構解析:彙總及最佳實踐基本介紹使用場景沖突處理slf4j整合日志輸出log4j2整合日志輸出日志打點API綁定實作

使用場景

▐ 隻使用java.util.logging.Logger

最簡單的場景,正式系統一般不會這麼用,自己寫點小demo、測試用例啥的是可以這麼用。不要任何第三方依賴,jdk原生支援。

▐ 隻使用Apache Commons Logging

需要引入commons-logging包,示例如下:

<dependency>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
          <version>1.2</version>
      </dependency>           

▐ Apache Commons Logging和log4j結合使用

需要引入commons-logging包和log4j包,示例如下:

<dependency>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
          <version>1.2</version>
      </dependency>
      <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
      </dependency>           

該模式下可以使用的打點api:

  • org.apache.commons.logging.Log,commons-logging裡的api;
  • org.apache.log4j.Logger,log4j裡的api;

無論使用哪種api打點,最終日志都會通過log4j進行實際的日志記錄。推薦用commons-logging裡的api,如果直接用log4j裡的api,就跟單用log4j沒差別,就沒有再引入commons-logging包的必要了。

既然最終是通過log4j實作日志記錄,那麼日志輸出的level、target等也就是通過log4j的配置檔案進行控制了。下面是一個log4j配置檔案《log4j.properties》的簡單示例:

#log4j.rootLogger = error,console
log4j.logger.com.suian.logtest = trace,console

#輸出源console輸出到控制台
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c - [log4j]%m%n           

既然是推薦使用commons-logging裡的api打點,為了能找到log4j的日志實作,必須通過《commons-logging.properties》配置檔案顯式的确定關聯,示例如下:

org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger           

代碼中使用JCL api進行日志打點,底層使用log4j進行日志輸出。日志輸出控制依托于log4j的配置檔案,另外需要在commons-logging.properties配置檔案中顯式指定與log4j的綁定關系。

▐ 單獨使用log4j

這個是早幾年最最流行的用法了,現在因為log4j本身的問題以及新的日志架構的湧現,已經逐漸退出曆史舞台了。具體怎麼用自己去百度吧。

▐ SLF4J結合Logback

當下最流行的用法,SLF4J為使用場景最廣泛的日志門面,加上Logback的天然實作,簡單、統一、快速。

需要引入第三方依賴:

<dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>${slf4j.version}</version>
      </dependency>
      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-core</artifactId>
          <version>${logback.version}</version>
      </dependency>
      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>${logback.version}</version>
      </dependency>           

▐ 單獨使用Log4j2

Log4j2感覺就是SLF4J+Logback。log4j-api等價于SLF4J,log4j-core等價于Logback。

<dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-api</artifactId>
          <version>2.6.2</version>
      </dependency>
      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-core</artifactId>
          <version>2.6.2</version>
      </dependency>           

沖突處理

理論上各種日志輸出方式是可以共存的,比如log4j和log4j2以及logback等,但是麻煩的是我們得維護多個配置檔案,必須充分了解每個元件使用的是那種日志元件,然後進行對應的配置檔案配置。

如何解決呢?每一個想做通用日志解決方案的,都對相容性問題進行了特殊處理。目前隻有slf4j和log4j2提供了這樣的整合機制,其他的基本都很弱。

代碼中可能使用的日志打點Api列舉:

  • java.util.logging.Logger,jdk自帶的;
  • org.apache.commons.logging.Log,commons-logging包裡的api;
  • org.apache.log4j.Logger,log4j包裡的api;
  • org.apache.logging.log4j.Logger,log4j2提供的api,在log4j-api包裡;
  • org.slf4j.Logger,slf4j提供的api,在slf4j-api包裡;

上述打點方式,在一個應用中是有可能共存的,即使自己寫的代碼可以確定都使用同一類api,但是引入的第三方依賴裡就可能各式各樣了。該怎麼處理呢?

前面已經提過了,現在能夠對各類沖突支援比較到位的就是slf4j和log4j2,他們都提供了很多的綁定器和橋接器。

所謂的綁定器,也可以稱之為擴充卡或者包裝類,就是将特定api打點的日志綁定到具體日志實作元件來輸出。比如JCL可以綁定到log4j輸出,也可以綁定到JUL輸出;再比如slf4j,可以通過logback輸出,也可以綁定到log4j、log4j2、JUL等;

所謂的橋接器就是一個假的日志實作工具,比如當你把 jcl-over-slf4j.jar 放到 CLASS_PATH 時,即使某個元件原本是通過 JCL 輸出日志的,現在卻會被 jcl-over-slf4j “騙到”SLF4J 裡,然後 SLF4J 又會根據綁定器把日志交給具體的日志實作工具。

slf4j整合日志輸出

▐ java.util.logging.Logger

将JUL日志整合到slf4j統一輸出,需要引入slf4j提供的依賴包:

<dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jul-to-slf4j</artifactId>
          <version>1.7.22</version>
      </dependency>           

隻引入依賴并不能整合JUL日志,該包裡隻是提供了一個JUL的handler,仍舊需要通過JUL的配置檔案進行配置,slf4j綁定器(如logback)上設定的日志級别等價于JUL handler上的日志級别,是以控制JUL的日志輸出,日志級别仍舊分兩個地方控制:JUL配置檔案《logging.properties》和slf4j綁定器的配置檔案,比如《logback.xml》、《log4j2.xml》等。

  • 建立jdk14-logger的配置檔案《logger.properties》,加入handler配置以及日志級别配置;
handlers= org.slf4j.bridge.SLF4JBridgeHandler
.level= ALL           
  • 在啟動程式或容器的時候加入JVM參數配置-Djava.util.logging.config.file = /path/logger.properties;當然也可以使用程式設計方式進行處理,可以在main方法或者擴充容器的listener來作為系統初始化完成;此種方式有些場景下不如配置JVM參數來的徹底,比如想代理tomcat的系統輸出日志,程式設計方式就搞不定了。

▐ org.apache.commons.logging.Log

将JCL日志整合到slf4j統一輸出,需要引入slf4j提供的依賴包:

<dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>1.7.22</version>
      </dependency>           

jcl-over-slf4j包裡所有類的根路徑為org.apache.commons.logging,也有Log和LogFactory類,相當于以重寫commons-logging包的代價來實作對JCL的橋接。Log與commons-logging包裡的一模一樣,LogFactory的實作,代碼寫死使用的是org.apache.commons.logging.impl.SLF4JLogFactory。

commons-logging包裡預設使用的是org.apache.commons.logging.impl.LogFactoryImpl。以這樣的代價來實作橋接,可以實作無縫對接,不像JUL那樣還得添加額外配置,但是有一個壞處就是需要處理類庫沖突了。commons-logging包和jcl-over-slf4j包肯定是不能共存的,需要将commons-logging包在classpath裡排掉。

題外話,因為JCL本身就支援通過配置檔案《commons-logging.properties》綁定擴充卡,是以個人感覺更傾向于封裝一個擴充卡的方式來支援,就像commons-logging包裡的org.apache.commons.logging.impl.Log4JLogger,這樣更符合程式員的思維,明明白白。

橋接包的命名也是很講究的,覆寫的這種,命名為xxx-over-slf4j,如本例的jcl-over-slf4j;純橋接的,命名為xxx-to-slf4j,如文章前面提到的jul-to-slf4j。

▐ org.apache.log4j.Logger

将log4j日志整合到slf4j統一輸出,需要引入slf4j提供的依賴包:

<dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>log4j-over-slf4j</artifactId>
          <version>1.7.22</version>
      </dependency>           

看橋接包的名字就知道了,log4j-over-slf4j肯定是覆寫了log4j:log4j包,是以使用起來隻需要引入依賴即可,不需要其他額外的配置。但是仍舊是要處理沖突的,log4j包和log4j-over-slf4j是不能共存的哦。

▐ org.apache.logging.log4j.Logger

将log4j2日志整合到slf4j統一輸出,slf4j沒有提供橋接包,但是log4j2提供了,原理是一樣的,首先引入log4j2的橋接包:

<dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-to-slf4j</artifactId>
          <version>2.6.2</version>
      </dependency>           

log4j2提供的依賴包有org.apache.logging.log4j:log4j-api和org.apache.logging.log4j:log4j-core,其作用看包名就清楚了。log4j-core是log4j-api的标準實作,同樣log4j-to-slf4j也是log4j-api的一個實作。

log4j-to-slf4j用于将log4j2輸出的日志橋接到slf4j進行實際的輸出,作用上來講,log4j-core和log4j-to-slf4j是不能共存的,因為會存在兩個log4j2的實作。

經測試,就測試結果分析,共存也是木有問題的,何解?log4j2加載provider的時候采用了優先級政策,即使找到多個也能決策出一個可用的provider來。在所有提供log4j2實作的依賴包中,都有一個META-INF/log4j-provider.properties配置檔案,裡面的FactoryPriority屬性就是用來配置provider優先級的,幸運的是log4j-to-slf4j(15)的優先級是高于log4j-core(10)的,是以測試結果符合預期,log4j2的日志橋接到了slf4j中進行輸出。

同樣,為確定系統的确定性,不會因為log4j2的provider決策政策變更導緻問題,建議還是要在classpath裡排掉log4j-core,log4j2也是推薦這麼做的。

log4j2整合日志輸出

将JUL日志整合到log4j2統一輸出,需要引入log4j2提供的依賴包:

<dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-jul</artifactId>
          <version>2.6.2</version>
      </dependency>           

log4j2整合JUL日志的方式與slf4j不同,slf4j隻是定義了一個handler,仍舊依賴JUL的配置檔案;log4j2則直接繼承重寫了java.util.logging.LogManager。

使用時,隻需要通過系統屬性java.util.logging.manager綁定重寫後的LogManager(org.apache.logging.log4j.jul.LogManager)即可,感覺比slf4j的方式要簡單不少。

将JCL日志整合到log4j2統一輸出,需要引入log4j2提供的依賴包:

<dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-jcl</artifactId>
          <version>2.6.2</version>
      </dependency>           

基于log4j-jcl包整合JCL比較簡單,隻要把log4j-jcl包扔到classpath就可以了。看起來slf4j的整合方式優雅多了,底層原理是這樣的:JCL的LogFactory在初始化的時候,查找LogFactory的具體實作,是分了幾種場景的,簡單描述如下:

  1. 首先根據系統屬性org.apache.commons.logging.LogFactory查找LogFactory實作類;
  2. 如果找不到,則以SPI方式查找實作類,META-INF/services/org.apache.commons.logging.LogFactory;log4j-jcl就是以這種方式支撐的;此種方式必須確定整個應用中,包括應用依賴的第三方jar包中,org.apache.commons.logging.LogFactory檔案隻有一個,如果存在多個的話,哪個先被加載則以哪個為準。萬一存在沖突的話,排查起來也挺麻煩的。
  3. 還找不到,則讀取《commons-logging.properties》配置檔案,使用org.apache.commons.logging.LogFactory屬性指定的LogFactory實作類;
  4. 最後再找不到,就使用預設的實作org.apache.commons.logging.impl.LogFactoryImpl。

将log4j 1.x日志整合到log4j2統一輸出,需要引入log4j2提供的依賴包:

<dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-1.2-api</artifactId>
          <version>2.6.2</version>
      </dependency>           

log4j2裡整合log4j 1.x日志,也是通過覆寫log4j 1.x api的方式來實作的,跟slf4j的實作原理一緻。是以也就存在類庫沖突的問題,使用log4j-1.2-api的話,必須把classpath下所有log4j 1.x的包清理掉。

▐ org.slf4j.Logger

将slf4j日志整合到log4j2統一輸出,需要引入log4j2提供的依賴包:

<dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-slf4j-impl</artifactId>
          <version>2.6.2</version>
      </dependency>           

log4j-slf4j-impl基于log4j2實作了slf4j的接口,其就是slf4j-api和log4j2-core之間的一個橋梁。這裡有個問題需要注意下,務必確定classpath下log4j-slf4j-impl和log4j-to-slf4j不要共存,否則會導緻事件無止盡地在SLF4J和Log4j2之間路由。

日志打點API綁定實作

slf4j-api和log4j-api都是接口,不提供具體實作,理論上基于這兩種api輸出的日志可以綁定到很多的日志實作上。slf4j和log4j2也确實提供了很多的綁定器。簡單列舉幾種可能的綁定鍊:

  • slf4j → logback
  • slf4j → slf4j-log4j12 → log4j
  • slf4j → log4j-slf4j-impl → log4j2
  • slf4j → slf4j-jdk14 → jul
  • slf4j → slf4j-jcl → jcl
  • jcl → jul
  • jcl → log4j
  • log4j2-api → log4j2-cor
  • log4j2-api → log4j-to-slf4j → slf4j

來個環圖:

Java 日志架構解析:彙總及最佳實踐基本介紹使用場景沖突處理slf4j整合日志輸出log4j2整合日志輸出日志打點API綁定實作

手淘行業與智能營運團隊

在阿裡,如果不經曆電商,那麼你可能就失去了一半的工作樂趣;做電商,如果不搞商業智能,那麼你可能就失去了連結人、貨、場、商,給幾億使用者創造更美好生活的機會!但是現在,一個充滿樂趣和機會的崗位就擺在你的面前 —— 它就是,行業與智能營運團隊,電商中最智能的技術團隊!大家走過路過,不要錯過!我們要從上到下打造一支幸福感極強的團隊 —— 如果你在追求幸福感,找我們,沒毛病!未來已來,淘系技術部行業與智能營運團隊,這支即将成為阿裡最具幸福感的技術團隊,期待具有好奇心和思考力的你的加入!

就是現在,阿裡巴巴淘系技術部行業與智能營運團隊,Java工程師、技術專家、架構師面向社會+校園招聘,base杭州阿裡巴巴西溪園區!

投喂履歷給我們:[email protected]

關注「淘系技術」微信公衆号,一個有溫度有内容的技術社群~

Java 日志架構解析:彙總及最佳實踐基本介紹使用場景沖突處理slf4j整合日志輸出log4j2整合日志輸出日志打點API綁定實作