天天看點

Java中日志元件詳解

Java中日志元件詳解

作為開發人員,我相信您對日志記錄工具并不陌生。 Java還具有功能強大且功能強大的日志記錄庫。 但是擁有如此衆多的日志記錄工具和第三方程式包,如何確定每個元件都可以使用約定日志工具?

本文将和大家介紹一下 Java 主流的日志工具,以及相對應的使用場景。

基本介紹

在 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中日志元件詳解

使用場景

隻使用 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.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 的具體實作,是分了幾種場景的,簡單描述如下:

首先根據系統屬性 org.apache.commons.logging.LogFactory 查找 LogFactory 實作類;

如果找不到,則以 SPI 方式查找實作類,META-INF/services/org.apache.commons.logging.LogFactory;log4j-jcl 就是以這種方式支撐的;此種方式必須確定整個應用中,包括應用依賴的第三方 jar 包中,org.apache.commons.logging.LogFactory 檔案隻有一個,如果存在多個的話,哪個先被加載則以哪個為準。萬一存在沖突的話,排查起來也挺麻煩的。

還找不到,則讀取《commons-logging.properties》配置檔案,使用 org.apache.commons.logging.LogFactory 屬性指定的 LogFactory 實作類;

最後再找不到,就使用預設的實作 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