1. 程式人生 > >最強最全常用開發庫 - 日誌類庫詳解

最強最全常用開發庫 - 日誌類庫詳解

Java日誌庫是最能體現Java庫在進化中的淵源關係的,在理解時重點理解日誌框架本身和日誌門面,以及比較好的實踐等。要關注其歷史淵源和設計(比如橋接),而具體在使用時查詢介面即可, 否則會陷入JUL(Java Util Log), JCL(Commons Logging), Log4j, SLF4J, Logback,Log4j2傻傻分不清楚的境地。@pdai

日誌庫簡介

我認為全面理解日誌庫需要從下面三個角度去理解:@pdai

  • 最重要的一點是 區分日誌系統和日誌門面;
  • 其次是日誌庫的使用, 包含配置與API使用;配置側重於日誌系統的配置,API使用側重於日誌門面;
  • 最後是選型,改造和最佳實踐等

日誌庫之日誌系統

java.util.logging (JUL)

JDK1.4 開始,通過 java.util.logging 提供日誌功能。雖然是官方自帶的log lib,JUL的使用確不廣泛。主要原因:

  • JUL從JDK1.4 才開始加入(2002年),當時各種第三方log lib已經被廣泛使用了
  • JUL早期存在效能問題,到JDK1.5上才有了不錯的進步,但現在和Logback/Log4j2相比還是有所不如
  • JUL的功能不如Logback/Log4j2等完善,比如Output Handler就沒有Logback/Log4j2的豐富,有時候需要自己來繼承定製,又比如預設沒有從ClassPath里加載配置檔案的功能

Log4j

Log4j 是 apache 的一個開源專案,創始人 Ceki Gulcu。Log4j 應該說是 Java 領域資格最老,應用最廣的日誌工具。Log4j 是高度可配置的,並可通過在執行時的外部檔案配置。它根據記錄的優先級別,並提供機制,以指示記錄資訊到許多的目的地,諸如:資料庫,檔案,控制檯,UNIX 系統日誌等。

Log4j 中有三個主要組成部分:

  • loggers - 負責捕獲記錄資訊。
  • appenders - 負責釋出日誌資訊,以不同的首選目的地。
  • layouts - 負責格式化不同風格的日誌資訊。

官網地址:http://logging.apache.org/log4j/2.x/

Log4j 的短板在於效能,在Logback 和 Log4j2 出來之後,Log4j的使用也減少了。

Logback

Logback 是由 log4j 創始人 Ceki Gulcu 設計的又一個開源日記元件,是作為 Log4j 的繼承者來開發的,提供了效能更好的實現,非同步 logger,Filter等更多的特性。

logback 當前分成三個模組:logback-core、logback-classic 和 logback-access。

  • logback-core - 是其它兩個模組的基礎模組。
  • logback-classic - 是 log4j 的一個 改良版本。此外 logback-classic 完整實現 SLF4J API 使你可以很方便地更換成其它日記系統如 log4j 或 JDK14 Logging。
  • logback-access - 訪問模組與 Servlet 容器整合提供通過 Http 來訪問日記的功能。

官網地址: http://logback.qos.ch/

Log4j2

維護 Log4j 的人為了效能又搞出了 Log4j2。

Log4j2 和 Log4j1.x 並不相容,設計上很大程度上模仿了 SLF4J/Logback,效能上也獲得了很大的提升。

Log4j2 也做了 Facade/Implementation 分離的設計,分成了 log4j-api 和 log4j-core。

官網地址: http://logging.apache.org/log4j/2.x/

Log4j vs Logback vs Log4j2

從效能上Log4J2要強,但從生態上Logback+SLF4J優先。@pdai

初步對比

logback和log4j2都宣稱自己是log4j的後代,一個是出於同一個作者,另一個則是在名字上根正苗紅。

撇開血統不談,比較一下log4j2和logback:

  • log4j2比logback更新:log4j2的GA版在2014年底才推出,比logback晚了好幾年,這期間log4j2確實吸收了slf4j和logback的一些優點(比如日誌模板),同時應用了不少的新技術
  • 由於採用了更先進的鎖機制和LMAX Disruptor庫,log4j2的效能優於logback,特別是在多執行緒環境下和使用非同步日誌的環境下
  • 二者都支援Filter(應該說是log4j2借鑑了logback的Filter),能夠實現靈活的日誌記錄規則(例如僅對一部分使用者記錄debug級別的日誌)
  • 二者都支援對配置檔案的動態更新
  • 二者都能夠適配slf4j,logback與slf4j的適配應該會更好一些,畢竟省掉了一層適配庫
  • logback能夠自動壓縮/刪除舊日誌
  • logback提供了對日誌的HTTP訪問功能
  • log4j2實現了“無垃圾”和“低垃圾”模式。簡單地說,log4j2在記錄日誌時,能夠重用物件(如String等),儘可能避免例項化新的臨時物件,減少因日誌記錄產生的垃圾物件,減少垃圾回收帶來的效能下降
  • log4j2和logback各有長處,總體來說,如果對效能要求比較高的話,log4j2相對還是較優的選擇。

效能對比

附上log4j2與logback效能對比的benchmark,這份benchmark是Apache Logging出的,有多大水分不知道,僅供參考

同步寫檔案日誌的benchmark:

非同步寫日誌的benchmark:

當然,這些benchmark都是在日誌Pattern中不包含Location資訊(如日誌程式碼行號 ,呼叫者資訊,Class名/原始碼檔名等)時測定的,如果輸出Location資訊的話,效能誰也拯救不了:

日誌庫之日誌門面

common-logging

common-logging 是 apache 的一個開源專案。也稱Jakarta Commons Logging,縮寫 JCL。

common-logging 的功能是提供日誌功能的 API 介面,本身並不提供日誌的具體實現(當然,common-logging 內部有一個 Simple logger 的簡單實現,但是功能很弱,直接忽略),而是在執行時動態的繫結日誌實現元件來工作(如 log4j、java.util.loggin)。

官網地址: http://commons.apache.org/proper/commons-logging/

slf4j

全稱為 Simple Logging Facade for Java,即 java 簡單日誌門面。

什麼,作者又是 Ceki Gulcu!這位大神寫了 Log4j、Logback 和 slf4j,專注日誌元件開發五百年,一直只能超越自己。

類似於 Common-Logging,slf4j 是對不同日誌框架提供的一個 API 封裝,可以在部署的時候不修改任何配置即可接入一種日誌實現方案。但是,slf4j 在編譯時靜態繫結真正的 Log 庫。使用 SLF4J 時,如果你需要使用某一種日誌實現,那麼你必須選擇正確的 SLF4J 的 jar 包的集合(各種橋接包)。

官網地址: http://www.slf4j.org/

common-logging vs slf4j

slf4j 庫類似於 Apache Common-Logging。但是,他在編譯時靜態繫結真正的日誌庫。這點似乎很麻煩,其實也不過是匯入橋接 jar 包而已。

slf4j 一大亮點是提供了更方便的日誌記錄方式:

不需要使用logger.isDebugEnabled()來解決日誌因為字元拼接產生的效能問題。slf4j 的方式是使用{}作為字串替換符,形式如下:

logger.debug("id: {}, name: {} ", id, name);

日誌庫使用方案

使用日誌解決方案基本可分為三步:

  • 引入 jar 包
  • 配置
  • 使用 API

常見的各種日誌解決方案的第 2 步和第 3 步基本一樣,實施上的差別主要在第 1 步,也就是使用不同的庫。

日誌庫jar包

這裡首選推薦使用 slf4j + logback 的組合。

如果你習慣了 common-logging,可以選擇 common-logging+log4j。

強烈建議不要直接使用日誌實現元件(logback、log4j、java.util.logging),理由前面也說過,就是無法靈活替換日誌庫。

還有一種情況:你的老專案使用了 common-logging,或是直接使用日誌實現元件。如果修改老的程式碼,工作量太大,需要相容處理。在下文,都將看到各種應對方法。

注:據我所知,當前仍沒有方法可以將 slf4j 橋接到 common-logging。如果我孤陋寡聞了,請不吝賜教。

slf4j 直接繫結日誌元件

  • slf4j + logback

新增依賴到 pom.xml 中即可。

logback-classic-1.0.13.jar 會自動將 slf4j-api-1.7.21.jar 和 logback-core-1.0.13.jar 也新增到你的專案中。

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.0.13</version>
</dependency>
  • slf4j + log4j

新增依賴到 pom.xml 中即可。

slf4j-log4j12-1.7.21.jar 會自動將 slf4j-api-1.7.21.jar 和 log4j-1.2.17.jar 也新增到你的專案中。

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.21</version>
</dependency>
  • slf4j + java.util.logging

新增依賴到 pom.xml 中即可。

slf4j-jdk14-1.7.21.jar 會自動將 slf4j-api-1.7.21.jar 也新增到你的專案中。

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-jdk14</artifactId>
  <version>1.7.21</version>
</dependency>

slf4j 相容非 slf4j 日誌元件

在介紹解決方案前,先提一個概念——橋接

  • 什麼是橋接呢

假如你正在開發應用程式所呼叫的元件當中已經使用了 common-logging,這時你需要 jcl-over-slf4j.jar 把日誌資訊輸出重定向到 slf4j-api,slf4j-api 再去呼叫 slf4j 實際依賴的日誌元件。這個過程稱為橋接。下圖是官方的 slf4j 橋接策略圖:

從圖中應該可以看出,無論你的老專案中使用的是 common-logging 或是直接使用 log4j、java.util.logging,都可以使用對應的橋接 jar 包來解決相容問題。

  • slf4j 相容 common-logging
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.7.12</version>
</dependency>
  • slf4j 相容 log4j
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.12</version>
</dependency>
  • slf4j 相容 java.util.logging
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.12</version>
</dependency>
  • spring 整合 slf4j

做 java web 開發,基本離不開 spring 框架。很遺憾,spring 使用的日誌解決方案是 common-logging + log4j。

所以,你需要一個橋接 jar 包:logback-ext-spring。

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.1.3</version>
</dependency>
<dependency>
  <groupId>org.logback-extensions</groupId>
  <artifactId>logback-ext-spring</artifactId>
  <version>0.1.2</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.7.12</version>
</dependency>

common-logging 繫結日誌元件

  • common-logging + log4j

新增依賴到 pom.xml 中即可。

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

日誌庫配置 - 針對於日誌框架

log4j2 配置

log4j2 基本配置形式如下:

<?xml version="1.0" encoding="UTF-8"?>;
<Configuration>
  <Properties>
    <Property name="name1">value</property>
    <Property name="name2" value="value2"/>
  </Properties>
  <Filter type="type" ... />
  <Appenders>
    <Appender type="type" name="name">
      <Filter type="type" ... />
    </Appender>
    ...
  </Appenders>
  <Loggers>
    <Logger name="name1">
      <Filter type="type" ... />
    </Logger>
    ...
    <Root level="level">
      <AppenderRef ref="name"/>
    </Root>
  </Loggers>
</Configuration>

配置示例:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" strict="true" name="XMLConfigTest"
               packages="org.apache.logging.log4j.test">
  <Properties>
    <Property name="filename">target/test.log</Property>
  </Properties>
  <Filter type="ThresholdFilter" level="trace"/>
 
  <Appenders>
    <Appender type="Console" name="STDOUT">
      <Layout type="PatternLayout" pattern="%m MDC%X%n"/>
      <Filters>
        <Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismatch="NEUTRAL"/>
        <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismatch="ACCEPT"/>
      </Filters>
    </Appender>
    <Appender type="Console" name="FLOW">
      <Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/><!-- class and line number -->
      <Filters>
        <Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
        <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismatch="DENY"/>
      </Filters>
    </Appender>
    <Appender type="File" name="File" fileName="${filename}">
      <Layout type="PatternLayout">
        <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
      </Layout>
    </Appender>
  </Appenders>
 
  <Loggers>
    <Logger name="org.apache.logging.log4j.test1" level="debug" additivity="false">
      <Filter type="ThreadContextMapFilter">
        <KeyValuePair key="test" value="123"/>
      </Filter>
      <AppenderRef ref="STDOUT"/>
    </Logger>
 
    <Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
      <AppenderRef ref="File"/>
    </Logger>
 
    <Root level="trace">
      <AppenderRef ref="STDOUT"/>
    </Root>
  </Loggers>
 
</Configuration>

logback 配置

<?xml version="1.0" encoding="UTF-8" ?>
 
<!-- logback中一共有5種有效級別,分別是TRACE、DEBUG、INFO、WARN、ERROR,優先順序依次從低到高 -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
 
  <property name="DIR_NAME" value="spring-helloworld"/>
 
  <!-- 將記錄日誌列印到控制檯 -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <!-- RollingFileAppender begin -->
  <appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/all.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>30MB</maxFileSize>
    </triggeringPolicy>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
 
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>ERROR</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
 
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>WARN</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
 
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
 
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>DEBUG</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/trace.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
 
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>TRACE</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
 
  <appender name="SPRING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 根據時間來制定滾動策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${user.dir}/logs/${DIR_NAME}/springframework.%d{yyyy-MM-dd}.log
      </fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
 
    <!-- 根據檔案大小來制定滾動策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>10MB</maxFileSize>
    </triggeringPolicy>
 
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
    </encoder>
  </appender>
  <!-- RollingFileAppender end -->
 
  <!-- logger begin -->
  <!-- 本專案的日誌記錄,分級列印 -->
  <logger name="org.zp.notes.spring" level="TRACE" additivity="false">
    <appender-ref ref="STDOUT"/>
    <appender-ref ref="ERROR"/>
    <appender-ref ref="WARN"/>
    <appender-ref ref="INFO"/>
    <appender-ref ref="DEBUG"/>
    <appender-ref ref="TRACE"/>
  </logger>
 
  <!-- SPRING框架日誌 -->
  <logger name="org.springframework" level="WARN" additivity="false">
    <appender-ref ref="SPRING"/>
  </logger>
 
  <root level="TRACE">
    <appender-ref ref="ALL"/>
  </root>
  <!-- logger end -->
 
</configuration>

log4j 配置

完整的 log4j.xml 參考示例

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
 
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
 
  <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern"
             value="%d{yyyy-MM-dd HH:mm:ss,SSS\} [%-5p] [%t] %c{36\}.%M - %m%n"/>
    </layout>
 
    <!--過濾器設定輸出的級別-->
    <filter class="org.apache.log4j.varia.LevelRangeFilter">
      <param name="levelMin" value="debug"/>
      <param name="levelMax" value="fatal"/>
      <param name="AcceptOnMatch" value="true"/>
    </filter>
  </appender>
 
 
  <appender name="ALL" class="org.apache.log4j.DailyRollingFileAppender">
    <param name="File" value="${user.dir}/logs/spring-common/jcl/all"/>
    <param name="Append" value="true"/>
    <!-- 每天重新生成日誌檔案 -->
    <param name="DatePattern" value="'-'yyyy-MM-dd'.log'"/>
    <!-- 每小時重新生成日誌檔案 -->
    <!--<param name="DatePattern" value="'-'yyyy-MM-dd-HH'.log'"/>-->
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern"
             value="%d{yyyy-MM-dd HH:mm:ss,SSS\} [%-5p] [%t] %c{36\}.%M - %m%n"/>
    </layout>
  </appender>
 
  <!-- 指定logger的設定,additivity指示是否遵循預設的繼承機制-->
  <logger name="org.zp.notes.spring" additivity="false">
    <level value="error"/>
    <appender-ref ref="STDOUT"/>
    <appender-ref ref="ALL"/>
  </logger>
 
  <!-- 根logger的設定-->
  <root>
    <level value="warn"/>
    <appender-ref ref="STDOUT"/>
  </root>
</log4j:configuration>

日誌庫API - 針對於日誌門面

slf4j 用法

使用 slf4j 的 API 很簡單。使用LoggerFactory初始化一個Logger例項,然後呼叫 Logger 對應的列印等級函式就行了。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class App {
    private static final Logger log = LoggerFactory.getLogger(App.class);
    public static void main(String[] args) {
        String msg = "print log, current level: {}";
        log.trace(msg, "trace");
        log.debug(msg, "debug");
        log.info(msg, "info");
        log.warn(msg, "warn");
        log.error(msg, "error");
    }
}

common-logging 用法

common-logging 用法和 slf4j 幾乎一樣,但是支援的列印等級多了一個更高級別的:fatal。

此外,common-logging 不支援{}替換引數,你只能選擇拼接字串這種方式了。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
public class JclTest {
    private static final Log log = LogFactory.getLog(JclTest.class);
 
    public static void main(String[] args) {
        String msg = "print log, current level: ";
        log.trace(msg + "trace");
        log.debug(msg + "debug");
        log.info(msg + "info");
        log.warn(msg + "warn");
        log.error(msg + "error");
        log.fatal(msg + "fatal");
    }
}

日誌庫選型與改造

對Java日誌元件選型的建議

slf4j已經成為了Java日誌元件的明星選手,可以完美替代JCL,使用JCL橋接庫也能完美相容一切使用JCL作為日誌門面的類庫,現在的新系統已經沒有不使用slf4j作為日誌API的理由了
日誌記錄服務方面,log4j在功能上輸於logback和log4j2,在效能方面log4j2則全面超越log4j和logback。所以新系統應該在logback和log4j2中做出選擇,對於效能有很高要求的系統,應優先考慮log4j2

對日誌架構使用比較好的實踐

總是使用Log Facade,而不是具體Log Implementation

正如之前所說的,使用 Log Facade 可以方便的切換具體的日誌實現。而且,如果依賴多個專案,使用了不同的Log Facade,還可以方便的通過 Adapter 轉接到同一個實現上。如果依賴專案使用了多個不同的日誌實現,就麻煩的多了。

具體來說,現在推薦使用 Log4j-API 或者 SLF4j,不推薦繼續使用 JCL。

只新增一個 Log Implementation依賴

毫無疑問,專案中應該只使用一個具體的 Log Implementation,建議使用 Logback 或者Log4j2。如果有依賴的專案中,使用的 Log Facade不支援直接使用當前的 Log Implementation,就新增合適的橋接器依賴。具體的橋接關係可以看上一篇文章的圖。

具體的日誌實現依賴應該設定為optional和使用runtime scope

在專案中,Log Implementation的依賴強烈建議設定為runtime scope,並且設定為optional。例如專案中使用了 SLF4J 作為 Log Facade,然後想使用 Log4j2 作為 Implementation,那麼使用 maven 新增依賴的時候這樣設定:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

設為optional,依賴不會傳遞,這樣如果你是個lib專案,然後別的專案使用了你這個lib,不會被引入不想要的Log Implementation 依賴;

Scope設定為runtime,是為了防止開發人員在專案中直接使用Log Implementation中的類,而不適用Log Facade中的類。

如果有必要, 排除依賴的第三方庫中的Log Impementation依賴

這是很常見的一個問題,第三方庫的開發者未必會把具體的日誌實現或者橋接器的依賴設定為optional,然後你的專案繼承了這些依賴——具體的日誌實現未必是你想使用的,比如他依賴了Log4j,你想使用Logback,這時就很尷尬。另外,如果不同的第三方依賴使用了不同的橋接器和Log實現,也極容易形成環。

這種情況下,推薦的處理方法,是使用exclude來排除所有的這些Log實現和橋接器的依賴,只保留第三方庫裡面對Log Facade的依賴。

比如阿里的JStorm就沒有很好的處理這個問題,依賴jstorm會引入對Logback和log4j-over-slf4j的依賴,如果你想在自己的專案中使用Log4j或其他Log實現的話,就需要加上excludes:

<dependency>
    <groupId>com.alibaba.jstorm</groupId>
    <artifactId>jstorm-core</artifactId>
    <version>2.1.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
        </exclusion>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>

避免為不會輸出的log付出代價

Log庫都可以靈活的設定輸出界別,所以每一條程式中的log,都是有可能不會被輸出的。這時候要注意不要額外的付出代價。

先看兩個有問題的寫法:

logger.debug("start process request, url: " + url);
logger.debug("receive request: {}", toJson(request));

第一條是直接做了字串拼接,所以即使日誌級別高於debug也會做一個字串連線操作;第二條雖然用了SLF4J/Log4j2 中的懶求值方式來避免不必要的字串拼接開銷,但是toJson()這個函式卻是都會被呼叫並且開銷更大。

推薦的寫法如下:

logger.debug("start process request, url:{}", url); // SLF4J/LOG4J2
logger.debug("receive request: {}", () -> toJson(request)); // LOG4J2
logger.debug(() -> "receive request: " + toJson(request)); // LOG4J2
if (logger.isDebugEnabled()) { // SLF4J/LOG4J2
    logger.debug("receive request: " + toJson(request)); 
}

日誌格式中最好不要使用行號,函式名等欄位

原因是,為了獲取語句所在的函式名,或者行號,log庫的實現都是獲取當前的stacktrace,然後分析取出這些資訊,而獲取stacktrace的代價是很昂貴的。如果有很多的日誌輸出,就會佔用大量的CPU。在沒有特殊需要的情況下,建議不要在日誌中輸出這些這些欄位。

最後, log中不要輸出稀奇古怪的字元!

部分開發人員為了方便看到自己的log,會在log語句中加上醒目的字首,比如:

logger.debug("========================start process request=============");

雖然對於自己來說是方便了,但是如果所有人都這樣來做的話,那log輸出就沒法看了!正確的做法是使用grep 來看只自己關心的日誌。

對現有系統日誌架構的改造建議

如果現有系統使用JCL作為日誌門面,又確實面臨著JCL的ClassLoader機制帶來的問題,完全可以引入slf4j並通過橋接庫將JCL api輸出的日誌橋接至slf4j,再通過適配庫適配至現有的日誌輸出服務(如log4j),如下圖:

這樣做不需要任何程式碼級的改造,就可以解決JCL的ClassLoader帶來的問題,但沒有辦法享受日誌模板等slf4j的api帶來的優點。不過之後在現系統上開發的新功能就可以使用slf4j的api了,老程式碼也可以分批進行改造。

如果現有系統使用JCL作為日誌門面,又頭疼JCL不支援logback和log4j2等新的日誌服務,也可以通過橋接庫以slf4j替代JCL,但同樣無法直接享受slf4j api的優點。

如果想要使用slf4j的api,那麼就不得不進行程式碼改造了,當然改造也可以參考1中提到的方式逐步進行。

如果現系統面臨著log4j的效能問題,可以使用Apache Logging提供的log4j到log4j2的橋接庫log4j-1.2-api,把通過log4j api輸出的日誌橋接至log4j2。這樣可以最快地使用上log4j2的先進效能,但元件中缺失了slf4j,對後續進行日誌架構改造的靈活性有影響。另一種辦法是先把log4j橋接至slf4j,再使用slf4j到log4j2的適配庫。這樣做稍微麻煩了一點,但可以逐步將系統中的日誌輸出標準化為使用slf4j的api,為後面的工作打好基礎。

參考文件

主要參考整理自:

  • https://www.jianshu.com/p/85d141365d39
  • https://blog.csdn.net/Dome_/article/details/98489727
  • https://zhuanlan.zhihu.com/p/24272450

此外還參考了:

  • http://www.slf4j.org/manual.html
  • http://logback.qos.ch/
  • http://logging.apache.org/log4j/1.2/
  • http://commons.apache.org/proper/commons-logging/
  • http://blog.csdn.net/yycdaizi/article/details/8276265

更多內容

最全的Java後端知識體系 https://www.pdai.tech, 每天更新中...。