史上最全日志框架整理

最近準(zhǔn)備看一下各個日志框架能否以及如何實現(xiàn)多線程下寫入自定義文件。同時深入的整理并學(xué)習(xí)一下這些日志框架。

一、 目前主流日志框架主要包含以下幾種:

  1. JUL
  2. Log4j
  3. Log4j2
  4. Conmmons-logging
  5. Slf4j
  6. Logback

二、框架簡介

  1. JUL:
    JUL 全稱 java.util.logging.Logger,JDK 自帶的日志系統(tǒng),從 JDK1.4 就有了。因為 Log4j 的存在,這個 Logger 一直沉默著,其實在一些測試性的代碼中,JDK 自帶的 Logger 比 Log4j 更方便。JUL 是自帶具體實現(xiàn)的,與 Log4j、Logback 等類似,而不是像 Conmmons-logging、Slf4j 那樣的日志接口封裝。
  1. Log4j:
    <1> Log4j 是 Apache 的一個開放源代碼項目,通過使用 Log4j ,我們可以控制日志信息輸送的目的地是控制臺、文件、數(shù)據(jù)庫等;
    <2> 我們也可以控制每一條日志的輸出格式;通過定義每一條日志信息的級別,我們能夠更加細(xì)致地控制日志的生成過程。
    <3> Log4j 有 7 種不同的log 級別,按照等級從低到高依次為:TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF。如果配置為 OFF 級別,表示關(guān)閉 log。
    <4> Log4j 支持兩種格式的配置文件:properties 和 xml。
    <5> 包含三個主要的組件:Logger、appender、Layout。
    <6> Log4j 產(chǎn)生的原因:
    在 JDK 1.3 及以前,Java 打日志依賴 System.out.println(), System.err.println() 或者 e.printStackTrace(),Debug 日志被寫到 STDOUT 流,錯誤日志被寫到 STDERR 流。這樣打日志有一個非常大的缺陷,即無法定制化,且日志粒度不夠細(xì)。Log4j 是在這樣的環(huán)境下誕生的,它是一個里程碑式的框架,它定義的 Logger、Appender、Level 等概念如今已經(jīng)被廣泛使用。
    <7> 注意:2015年8月5日,項目管理委員會宣布 Log4j已達(dá)到使用壽命。建議用戶使用 Log4j 升級到 Apache Log4j2。
  1. Log4j2:
    <1> Log4j2 是 Log4j 和 Logback 的改進(jìn)版。據(jù)說采用了一些新技術(shù)(無鎖異步等等),使得日志的吞吐量、性能比 Log4j 提高10倍,并解決了一些死鎖的bug,而且配置更加簡單靈活。
    <2> Log4j2 支持插件式結(jié)構(gòu),可以根據(jù)需要自行擴(kuò)展 Log4j2,實現(xiàn)自己的 appender、logger、filter 等。
    <3> Log4j2 在配置文件中可以引用屬性,還可以直接替代或傳遞到組件,而且支持 json 格式的配置文件。不像其他的日志框架,它在重新配置的時候不會丟失之前的日志文件。
    <4> Log4j2 利用 Java5 中的并發(fā)特性支持,盡可能地執(zhí)行最低層次的加鎖。解決了在 Log4j 中存留的死鎖的問題。
    <5> Log4j2 是基于 LMAX Disruptor 庫的。在多線程的場景下,和已有的日志框架相比,異步 logger 擁有 10 倍左右的效率提升。

Log4j2 與其他框架共同使用的體系結(jié)構(gòu)

Log4j2 體系結(jié)構(gòu)

  1. Conmmons-logging:
    <1> 之前叫 Jakarta Commons Logging,簡稱 JCL,是 Apache 提供的一個通用日志門面接口 API,可以讓應(yīng)用程序不再依賴于具體的日志實現(xiàn)工具。
    <2> Commons-logging 包中對其它一些日志工具,包括 Log4J、Avalon LogKit、JUL 等,進(jìn)行了簡單的包裝,可以讓應(yīng)用程序在運行時,直接將 JCL API 打點的日志適配到對應(yīng)的日志實現(xiàn)工具中。
    <3> Commons-logging 通過動態(tài)查找的機(jī)制,在程序運行時自動找出真正使用的日志庫。這一點與 Slf4j 不同,Slf4j 是在編譯時靜態(tài)綁定真正的 Log 實現(xiàn)庫。
    <4> 如果只引入 Apache Commons Logging,也沒有通過配置文件 commons-logging.properties 進(jìn)行適配器綁定,也沒有通過系統(tǒng)屬性或者 SPI 重新定義 LogFactory 實現(xiàn),默認(rèn)使用的就是 JDK 自帶的 java.util.logging.Logger 來進(jìn)行日志輸出。
    <4> Commons-logging 提供簡單的日志實現(xiàn)以及日志解耦功能。
    <5> Commons-logging 是 Apache commons 類庫中的一員。Apache commons 類庫是一個通用的類庫,提供了基礎(chǔ)的功能,比如說 commons-fileupload,commons-httpclient,commons-io,commons-codes 等。
  1. Commons-logging包里的包裝類和簡單實現(xiàn)列舉如下:
    <1> org.apache.commons.logging.impl.Jdk14Logger,適配 JDK1.4 里的 JUL;
    <2> org.apache.commons.logging.impl.Log4JLogger,適配 Log4j;
    <3> org.apache.commons.logging.impl.LogKitLogger,適配 avalon-Logkit;
    <4> org.apache.commons.logging.impl.SimpleLog,Commons-logging 自帶日志實現(xiàn)類,它實現(xiàn)了 Log 接口,把日志消息都輸出到系統(tǒng)錯誤流 System.err 中;
    <5> org.apache.commons.logging.impl.NoOpLog,Commons-logging 自帶日志實現(xiàn)類,它實現(xiàn)了 Log 接口,其輸出日志的方法中不進(jìn)行任何操作;
  1. Slf4j:
    <1> SLF4J 全稱 The Simple Logging Facade for Java,簡單日志門面,這個不是具體的日志解決方案,而是通過門面模式提供一些 Java Logging API,類似于 JCL。
    <2> SLF4J 提供的核心 API 是一些接口以及一個 LoggerFactory 的工廠類。在使用 SLF4J 的時候,不需要在代碼中或配置文件中指定你打算使用哪個具體的日志系統(tǒng),可以在部署的時候不修改任何配置即可接入一種日志實現(xiàn)方案,在編譯時靜態(tài)綁定真正的 Log 庫。
    <3> 題外話,作者當(dāng)時創(chuàng)建 SLF4J 的目的就是為了替代 Jakarta Commons Logging(JCL)。
    <4> 使用 SLF4J 時,如果你需要使用某一種日志實現(xiàn),那么你必須選擇正確的 SLF4J 的 jar 包的集合(各種橋接包)。
    <5> SLF4J 提供了統(tǒng)一的記錄日志的接口,只要按照其提供的方法記錄即可,最終日志的格式、記錄級別、輸出方式等通過具體日志系統(tǒng)的配置來實現(xiàn),因此可以在應(yīng)用中靈活切換日志系統(tǒng)。
  1. Slf4j 的一些橋接包:
    <1> slf4j-log4j12:可以使用log4j進(jìn)行底層日志輸出。
    <2> slf4j-jdk14:可以使用JUL進(jìn)行日志輸出。
  1. Logback:
    <1> Logback,一個“ 可靠、通用、快速而又靈活的 Java 日志框架 ”。
    <2> Logback當(dāng)前分成三個模塊:logback-core,logback- classic和logback-access:
    logback-core 模塊為其他兩個模塊奠定了基礎(chǔ)。
    logback-classic 模塊可以被同化為 Log4j 的顯著改進(jìn)版本。logback-classic 本身實現(xiàn)了 slf4j-api,因此我們可以在 logback 和其他日志框架(如 Log4j 或java.util.logging(JUL))之間來回切換。
    logback-access 模塊??與 Servlet 容器(如 Tomcat 和 Jetty)集成,以提供 HTTP 訪問日志功能。可以在 logback-core 之上輕松構(gòu)建自己的模塊。
    <3> Logback依賴配置文件logback.xml,當(dāng)然也支持groovy方式。
    <4> Logback 的核心對象:Logger、Appender、Layout
    Logback 主要建立于 Logger、Appender 和 Layout 這三個類之上。
    Logger:日志的記錄器,把它關(guān)聯(lián)到應(yīng)用的對應(yīng)的 context 上后,主要用于存放日志對象,也可以定義日志類型、級別。Logger 對象一般多定義為靜態(tài)常量.
    Appender:用于指定日志輸出的目的地,目的地可以是控制臺、文件、遠(yuǎn)程套接字服務(wù)器、 MySQL、 PostreSQL、Oracle 和其他數(shù)據(jù)庫、 JMS 和遠(yuǎn)程 UNIX Syslog 守護(hù)進(jìn)程等。
    Layout:負(fù)責(zé)把事件轉(zhuǎn)換成字符串,格式化的日志信息的輸出。
  1. Logback相比log4j,有很多很多的優(yōu)點:
    <1> Logback的內(nèi)核重寫了,在一些關(guān)鍵執(zhí)行路徑上性能提升10倍以上。而且logback不僅性能提升了,初始化內(nèi)存加載也更小了。
    <2> Logback經(jīng)過了幾年,數(shù)不清小時的測試。Logback的測試完全不同級別的。在作者的觀點,這是簡單重要的原因選擇logback而不是log4j。
    <3> 當(dāng)配置文件修改了,Logback-classic能自動重新加載配置文件。掃描過程快且安全,它并不需要另外創(chuàng)建一個掃描線程。這個技術(shù)充分保證了應(yīng)用程序能跑得很歡在JEE環(huán)境里面。
    <4> RollingFileAppender在產(chǎn)生新文件的時候,會自動壓縮已經(jīng)打出來的日志文件。壓縮是個異步過程,所以甚至對于大的日志文件,在壓縮過程中應(yīng)用不會受任何影響等。
    ……

三、這些日志框架的歷史

  1. 1996 年早期,歐洲安全電子市場項目組決定編寫它自己的程序跟蹤 API( Tracing API )。經(jīng)過不斷的完善,這個 API 終于成為一個十分受歡迎的 Java 日志軟件包,即 Log4j。后來 Log4j 成為 Apache 基金會項目中的一員。
  2. 期間 Log4j 近乎成了 Java 社區(qū)的日志標(biāo)準(zhǔn)。據(jù)說 Apache基金會還曾經(jīng)建議 Sun 引入 Log4j 到 java 的標(biāo)準(zhǔn)庫中,但 Sun 拒絕了。
  3. 2002 年 Java1.4 發(fā)布,Sun 推出了自己的日志庫 JUL ( Java Util Logging ),其實現(xiàn)基本模仿了 Log4j 的實現(xiàn)。在 JUL 出來以前,Log4j 就已經(jīng)成為一項成熟的技術(shù),使得Log4j 在選擇上占據(jù)了一定的優(yōu)勢。
  4. 接著,Apache 推出了 Jakarta Commons Logging,JCL 只是定義了一套日志接口(其內(nèi)部也提供一個 Simple Log 的簡單實現(xiàn)),支持運行時動態(tài)加載日志組件的實現(xiàn),也就是說,在你應(yīng)用代碼里,只需調(diào)用 Commons Logging 的接口,底層實現(xiàn)可以是 Log4j,也可以是 Java Util Logging。
  5. 后來( 2006 年),Ceki Gülcü 不適應(yīng) Apache 的工作方式,離開了 Apache 。然后先后創(chuàng)建了 Slf4j (日志門面接口,類似于 Commons Logging )和 Logback (Slf4j的實現(xiàn))兩個項目,并回瑞典創(chuàng)建了 QOS 公司,QOS 官網(wǎng)上是這樣描述 Logback 的:The Generic,Reliable Fast&Flexible Logging Framework (一個通用,可靠,快速且靈活的日志框架)。
  6. 現(xiàn)今,Java 日志領(lǐng)域被劃分為兩大陣營:Commons Logging 陣營和 Slf4j 陣營。
    Commons Logging 在 Apache 大樹的籠罩下,有很大的用戶基數(shù)。但有證據(jù)表明,形式正在發(fā)生變化。2013 年底有人分析了 GitHub 上 30000 個項目,統(tǒng)計出了最流行的 100 個 Libraries,可以看出 Slf4j 的發(fā)展趨勢更好:
    (圖片來自https://www.cnblogs.com/chenhongliang/p/5312517.html
各個日志框架的使用熱度
  1. Apache 眼看有被 Logback 反超的勢頭,于 2012-07 重寫了 Log4j1.x,成立了新的項目 Log4j2 , Log4j2 具有 Logback 的所有特性。

四、Java 常用日志框架關(guān)系

(圖片來自 http://www.lxweimin.com/p/bbbdcb30bba8

常用日志框架關(guān)系
  1. Commons-logging、Slf4j 遵循面向接口編程的原則,這兩大框架是統(tǒng)一抽象出來的一些接口。
  2. JUL、Log4j、Log4j2、logback 等框架已定制了日志API以及自己的實現(xiàn)。
  3. 因此,基本上就是(Commons-logging/Slf4j)接口+(JUL/Log4j/Log4j2/logback 等)實現(xiàn)的方式來使用。
  4. Log4j2 與 Log4j1 發(fā)生了很大的變化,基本所有核心全都重構(gòu)了一遍,因此Log4j2 不兼容 Log4j1。
  5. Commons Logging 和 Slf4j 是日志門面(門面模式是軟件工程中常用的一種軟件設(shè)計模式,也被稱為正面模式、外觀模式。它為子系統(tǒng)中的一組接口提供一個統(tǒng)一的高層接口,使得子系統(tǒng)更容易使用)。Log4j 和 Logback 則是具體的日志實現(xiàn)方案。可以簡單的理解為接口與接口的實現(xiàn),調(diào)用者只需要關(guān)注接口而無需關(guān)注具體的實現(xiàn),做到解耦。
  6. 比較常用的組合使用方式是 Slf4j 與 Logback 組合使用,Commons Logging 與 Log4j 組合使用,但并不絕對,主要看自己選擇。
  7. Logback 必須配合Slf4j 使用。由于 Logback 和 Slf4j 同一個作者,其兼容性不言而喻。

五、各個框架所需要的 jar 包

寫在前面:
在使用各種日志框架的過程中,避免不了要引入各種依賴,而多種依賴之間很有可能會產(chǎn)生沖突,因此我們在引入一些日志的依賴庫后,需要將其他依賴庫中的一些依賴排除,而如果依賴較多時,一個個尋找比較麻煩,因此可以在項目根目錄下進(jìn)入CMD,用以下命令檢查:

mvn dependency:tree
  1. Log4j:只需要導(dǎo)入一個依賴即可。(已停止更新)
<dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.17</version>
</dependency>
  1. Log4j2:則需要導(dǎo)入兩個依賴。
<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-core</artifactId>
   <version>2.13.3</version>
</dependency>
<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-api</artifactId>
   <version>2.13.3</version>
</dependency>

不過如果我們使用 SpringBoot 開發(fā)的話,我們會看到在 SpringBoot 的 jar 包中已經(jīng)包含了 Log4j2 的 log4j-api.jar 包,但是,這里的 log4j-api 是為了將 Log4j2 的接口適配到 Slf4j 上而存在的,如果想單獨使用 Log4j2,則需要單獨引入 log4j-api.jar 并且將 log4j-to-slf4j 下的 log4j-api 排除掉。
log4j-api 包含的是 .class 一堆接口,實際使用需要 log4j-core,log4j-core 包含 .class 與 .java 也就是源碼。

  • SpringBoot 包含的 jar 包
  1. Logback:分為三個模塊,需要同時導(dǎo)入三個依賴。
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>1.2.3</version>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-core</artifactId>
   <version>1.2.3</version>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-access</artifactId>
   <version>1.2.3</version>
</dependency>

注:如果在 SpringBoot 下是不需要引入 Logback 的,因為在 spring-boot-starter-logging 中已經(jīng)內(nèi)置了該依賴。

  1. Slf4j:一般不會導(dǎo)入 slf4j-api jar 包,而是導(dǎo)入針對另一個具體實現(xiàn)的 jar 包,例如 slf4j-lo4j12,里面包含了 log4j:1.2.17。
<dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.7.25</version>
</dependency>

通常輸出日志開銷非常大,Slf4j 通過 {} 作為占位符的方式輸出字符串,相比字符串拼接的方式,效率有顯著的提升。

  1. Commons-logging:這個框架已經(jīng)停止更新了。
<dependency>
   <groupId>commons-logging</groupId>
   <artifactId>commons-logging</artifactId>
   <version>1.2</version>
</dependency>

Commons-logging 能夠選擇使用 Log4j 還是 JUL,但是不依賴 Log4j、JUL 的API。
如果項目的 classpath 中包含了 Log4j 的類庫,就會使用 Log4j,否則就使用
JUL。
使用 Commons-logging 能夠靈活的選擇使用那些日志方式,而且不需要修改源代碼。

  1. JUL 不需要導(dǎo)入任何依賴,JDK 中已包含 JUL 的接口和具體實現(xiàn)。

下面是幾種日志框架的詳細(xì)內(nèi)容以及使用場景。

六、JUL

由于小白人員對日志方面并不熟悉,因此先了解 JUL,并看一下底層源碼實現(xiàn),提升自己的基礎(chǔ)了解,后續(xù)再對其余幾個框架進(jìn)行分析整理。

1. 在.../jre/lib下找到logging.properties
############################################################
#   Default Logging Configuration File 
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.  
# For example java -Djava.util.logging.config.file=myfile
# 你可以通過 java.util.logging.config.file 屬性指定一個文件名作為日志配置文件。
# 例如:java -Djava.util.logging.config.file=myfile
############################################################

############################################################
#   Global properties
############################################################

# "handlers" specifies a comma separated list of log Handler 
# classes.  These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
# "handlers" :以逗號分隔,指定日志處理器。這些處理器將在VM啟動期間安裝。
# 注意:這些類必須位于系統(tǒng)的classpath路徑下。
# 默認(rèn)情況下,我們只配置一個ConsoleHandler(控制臺),并且只顯示INFO等級以上的信息。

handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler

# To also add the FileHandler, use the following line instead.
# 如果需要使用FileHandler,則使用下面這行。
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers.  For any given facility this global level 
# can be overriden by a facility specific level 
# Note that the ConsoleHandler also has a separate level 
# setting to limit messages printed to the console.
# 默認(rèn)全局日志級別
# 它指定所有的 logger 記錄哪些類型的事件。
# 它可以被任何設(shè)備中的指定級別覆蓋。
# 注意:ConsoleHandler具有單獨的級別設(shè)置,用來限制打印到控制臺的信息。

.level= INFO

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
# 處理器特定配置文件
# 描述處理器的特定配置信息
############################################################

# default file output is in user's home directory. 
# 日志文件默認(rèn)輸出到用戶主目錄。
java.util.logging.FileHandler.pattern = %h/java%u.log

# 指定要寫入到任意文件的近似最大量(以字節(jié)為單位)。如果該數(shù)為 0,則沒有限制(默認(rèn)為無限制)。
java.util.logging.FileHandler.limit = 50000

# 指定有多少輸出文件參與循環(huán)(默認(rèn)為 1)。
java.util.logging.FileHandler.count = 1

# 指定要使用的 Formatter(格式化) 類的名稱(默認(rèn)為 java.util.logging.XMLFormatter)。
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
# 限制打印在控制臺上的信息級別(默認(rèn)為INFO級別以上)。
java.util.logging.ConsoleHandler.level = INFO

# 指定控制臺上要使用的 Formatter(格式化) 類的名稱。
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# Example to customize the SimpleFormatter output format 
# to print one-line log message like this:
#     <level>: <log message> [<date/time>]
# 自定義SimpleFormatter輸出格式示例

# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

############################################################
# Facility specific properties.
# Provides extra control for each logger.
# 設(shè)備特定配置
# 為每個 logger 提供額外的配置
############################################################

# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
# 例如:讓 com.xyz.foo.level 下的 logger 只記錄SEVERE級別及以上

com.xyz.foo.level = SEVERE

按自己情況進(jìn)行修改后,將 logging.properties 放在 src/main/reources 目錄下,即項目 classpath 目錄下。

2. 使用 JUL

話不多說,先上代碼

package com.cmos.javalog.originalLog;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class JavaLog {
    public static void main(String[] args) {
        try {
            testLog();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void testLog() throws IOException {
        /*
         * 為指定子系統(tǒng)查找或創(chuàng)建一個logger
         * name: logger名稱
         * resourceBundleName: 子系統(tǒng)名稱
         */
        Logger logger = Logger.getLogger("test");
        /*
         * 創(chuàng)建一個FileHandler,并指定屬性
         * pattern: 日志文件存儲路徑
         * limit: 最大存儲字節(jié)數(shù)
         * count: 日志文件數(shù)量
         * append: 是否可以追究
         */
        FileHandler fileHandler = new FileHandler("E:/logs/test.log",102400,1,true);
        logger.addHandler(fileHandler);
        // 指定 logger 輸出級別
        logger.setLevel(Level.INFO);
        SimpleFormatter simpleFormatter = new SimpleFormatter();
        // 格式化log。
        // 這里放在 logger.addHandler() 之后并沒有問題,只要保證在 logger.log() 方法之前即可。
        fileHandler.setFormatter(simpleFormatter);

        logger.log(Level.INFO,"這是一個INFO消息");
        logger.log(Level.WARNING,"這是一個WARNING警告");
        logger.log(Level.SEVERE,"這是一個SEVERE服務(wù)器消息");
    }
}

然后,讓我們看一下 logger 具體是如何完成這些操作的。

1)首先我們調(diào)用 getLogger 方法去獲取一個 logger,而 getLogger 方法重載為了兩個方法:

// 方法1:
@CallerSensitive
public static Logger getLogger(String name) {
    return demandLogger(name, null, Reflection.getCallerClass());
}
// 方法2:
@CallerSensitive
public static Logger getLogger(String name, String resourceBundleName) {
    // 獲取調(diào)用者類
    Class<?> callerClass = Reflection.getCallerClass();
    Logger result = demandLogger(name, resourceBundleName, callerClass);
    
    // setupResourceInfo() 可以拋出 MissingResourceException 或 IllegaArgumentException,
    // 我們不得不在這里設(shè)置一個 callers 類加載器
    // 以防上面的 demandLogger 發(fā)現(xiàn)以前已經(jīng)創(chuàng)建好的 Logger。
    // 這是又可能發(fā)生的,例如,如果我們先調(diào)用了 Logger.getLogger(name) ,
    // 隨后調(diào)用 Logger.getLogger(name,resourceBundleName)
    // 在這種情況下我們無法保證我們存起來的是一個正確的類加載器,所以我們需要在這里設(shè)置它。
    result.setupResourceInfo(resourceBundleName, callerClass);
    return result;
}

2)但是殊途同歸,這兩個重載方法都是去調(diào)用了 demandLogger(name, resourceBundleName, callerClass) ,那讓我們看一下 Logger 下的 demandLogger 方法的內(nèi)容:

private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
    // 這里獲取的 manager 是單例的,如果其他線程已經(jīng)初始化了一個 LogManager ,
    // 那我們就直接獲取拿來使用
    LogManager manager = LogManager.getLogManager();
    SecurityManager sm = System.getSecurityManager();
    // 這里進(jìn)行安全檢查
    if (sm != null && !SystemLoggerHelper.disableCallerCheck) {
        // 若 getClassLoader() 為空,說明該 classLoader 是 bootstrap classLoader ,
        // 因此調(diào)用 demandSystemLogger(name,resourceBundleName)
        if (caller.getClassLoader() == null) {
            return manager.demandSystemLogger(name, resourceBundleName);
        }
    }
    // 上述不成立,就調(diào)用 LoggerManager 的 demandLogger(name,resourceBundleName,caller)
    return manager.demandLogger(name, resourceBundleName, caller);
}

3)同樣的殊途同歸,通過追溯源碼發(fā)現(xiàn) demandSystemLogger() 最后還是是調(diào)用了 demandLogger() ,因此我們只需要看一下 demandLogger() 是如何實現(xiàn)的即可:

// 如果已經(jīng)有了給定名稱的 logger ,則返回該 logger。
// 否則就在 LoggerManager 的全局命名空間注冊一個新的 logger 實例
// 并且這個方法永遠(yuǎn)不會返回空實例
Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
    // 這里根據(jù)給定 name 去獲取 logger
    Logger result = getLogger(name);
    if (result == null) {
        Logger newLogger = new Logger(name, resourceBundleName, caller, this);
        do {
            // 注冊一個新 logger 實例
            if (addLogger(newLogger)) {
                return newLogger;
            }
            
            result = getLogger(name);
        } while (result == null);
    }
    return result;
}

4)至此我們已經(jīng)了解到如何獲取的 logger 對象,接下來則是對 logger 的一些配置,用來覆蓋上面提到的 logging.properties 中的一些默認(rèn)配置。

5)接下來則是正式進(jìn)行日志信息輸出:

這里可以看到 logger.log() 方法有很多重載,當(dāng)然最常用的則是 log(level,msg) 以及 log(level,msg,params)
前者作為靜態(tài)日志信息輸出,后者則作為動態(tài)日志信息輸出

logger.log()

6)以 log(level,msg,params) 為例,看一下日志信息如何進(jìn)行輸出:

public void log(Level level, String msg, Object params[]) {
    if (!isLoggable(level)) {
        return;
    }
    // 創(chuàng)建一個 LogRecord 對象,并將 level\msg\params 封入 lr 
    LogRecord lr = new LogRecord(level, msg);
    lr.setParameters(params);
    // 調(diào)用 doLog(lr)
    doLog(lr);
}

// 為 logging 日志構(gòu)建的私有化方法,用來向 lr 中填入 logger 的一些信息
private void doLog(LogRecord lr) {
    // 填入 loggerName
    lr.setLoggerName(name);
    // 獲取有效綁定對象
    final LoggerBundle lb = getEffectiveLoggerBundle();
    // 獲取綁定資源以及資源名稱
    final ResourceBundle  bundle = lb.userBundle;
    final String ebname = lb.resourceBundleName;
    if (ebname != null && bundle != null) {
        lr.setResourceBundleName(ebname);
        lr.setResourceBundle(bundle);
    }
    // 調(diào)用 log(lr)
    log(lr);
}

// 所有記錄日志的操作最終都會使用這個方法
public void log(LogRecord record) {
    if (!isLoggable(record.getLevel())) {
        return;
    }
    Filter theFilter = filter;
    if (theFilter != null && !theFilter.isLoggable(record)) {
        return;
    }

    // 將這個日志記錄發(fā)送給我們所有的 handler ,然后發(fā)送給父類的 handler ,直到樹的頂端。
    Logger logger = this;
    while (logger != null) {
        for (Handler handler : logger.getHandlers()) {
            handler.publish(record);
        }
        // 判斷是否有父類
        if (!logger.getUseParentHandlers()) {
            break;
        }
        // 獲取父類對象
        logger = logger.getParent();
    }
}

7)之后則是我們最開始配置的 FileHandler 以及其他自定義 Handler 或默認(rèn) Handler 來進(jìn)行對日志記錄的格式化以及打印、輸出到文件等操作。

但是這里有個疑問,我們只是將這條記錄交給了 handlers ,但是這些 handler 又是何時進(jìn)行執(zhí)行的呢?

8)Handler 的執(zhí)行

// 在以上流程中唯一使用到 handler 的位置就是 handler.publish(record);
// 因此,我沿著 Handler 的子類向下尋找,發(fā)現(xiàn) Handler 類有一個 StreamHandler 子類
// 而恰巧 StreamHandler 下含有 FileHandler ConsoleHandler 等子類
// 因此我到 StreamHandler 下找到了 publish方法
@Override
    public synchronized void publish(LogRecord record) {
        if (!isLoggable(record)) {
            return;
        }
        String msg;
        try {
            msg = getFormatter().format(record);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.FORMAT_FAILURE);
            return;
        }

        try {
            // 如果這是向 OutputStream 中寫入的第一條記錄,
                // 則在寫入日志記錄之前,先將 formatter 的 head 寫入
            if (!doneHeader) {
                writer.write(getFormatter().getHead(this));
                doneHeader = true;
            }
            // 而后寫入 msg
            writer.write(msg);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.WRITE_FAILURE);
        }
    }

9)因此 Handler 的執(zhí)行就是在調(diào)用 publish 調(diào)用過程中進(jìn)行的,最后總結(jié)一下:

JUL:主要有以下特點
<1> 相同名字的 Logger 全局唯一;
<2> 配置文件默認(rèn)使用 jre/lib/logging.properties,日志級別默認(rèn)為 INFO;
<3> 可以通過系統(tǒng)屬性 java.util.logging.config.file 指定路徑覆蓋系統(tǒng)默認(rèn)文件;
<4> 日志級別由高到低依次為:SEVERE(嚴(yán)重)、WARNING(警告)、INFO(信息)、CONFIG(配置)、FINE(詳細(xì))、FINER(較詳細(xì))、FINEST(非常詳細(xì))。另外還有兩個全局開關(guān):OFF「關(guān)閉日志記錄」和ALL「啟用所有消息日志記錄」;
<5> logging.properties 文件中,默認(rèn)日志級別可以通過 .level = ALL 來控制,也可以基于層次命名空間來控制,按照 Logger 名字進(jìn)行前綴匹配,匹配度最高的優(yōu)先采用;日志級別只認(rèn)大寫;
<6> 原生 Logger 通過 handler 來完成實際的日志輸出,可以通過配置文件指定一個或者多個 hanlder,多個 handler之間使用逗號分隔;handler 上也有一個日志級別,作為該 handler 可以接收的日志最低級別,低于該級別的日志,將不進(jìn)行實際的輸出;handler 上可以綁定日志格式化器,比如 java.util.logging.ConsoleHandler 就是使用的 String.format 來支持的。

注意:一般使用圓點分隔的層次命名空間來命名 Logger;Logger 名稱可以是任意的字符串,但是它們一般應(yīng)該基于被記錄組件的包名或類名,如 java.net 或 javax.swing;

至此,我們對 log 就有了一定的認(rèn)識,了解了 log 的基本工作流程。所以接下來就開始著手分析各個日志框架以及它們?nèi)绾螌崿F(xiàn)多線程下寫入自定義文件。

3. JUL實現(xiàn)多線程寫入

1)自定義日志格式

在這里我只需要輸出我想輸出的數(shù)據(jù),不需要其他亂七八糟的內(nèi)容,因此我需要簡單的自定義一下輸出格式。

package com.cmos.javalog.jul;

import java.util.logging.Formatter;
import java.util.logging.LogRecord;

/**
 * 繼承 Formatter   實現(xiàn) format 方法
 */
public class MyFormatter extends Formatter {
    @Override
    public String format(LogRecord record) {
        return record.getMessage();
    }
}

2)修改配置文件

## 將 FileHandler.formatter 的值替換為自定義格式
java.util.logging.FileHandler.formatter = com.cmos.javalog.jul.MyFormatter

3)多線程環(huán)境下調(diào)用 log

public static void main(String[] args) throws IOException {
    // 設(shè)置配置文件路徑,使用自定義的配置文件
    String path = JavaLog.class.getClassLoader().getResource("logging.properties").getPath();
    System.setProperty("java.util.logging.config.file", path);
    // 多線程下使用同一個 logger 對象
    Logger logger = Logger.getLogger("moreThread2File");
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            // System.lineSeparator 系統(tǒng)換行符
            logger.log(Level.INFO, i + " thread-1" + System.lineSeparator());
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            logger.log(Level.INFO, i + " thread-2" + System.lineSeparator());
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            logger.log(Level.INFO, i + " thread-3" + System.lineSeparator());
        }
    }).start();
}

4)輸出結(jié)果

日志輸出結(jié)果
4. 總結(jié)

JUL屬于較為簡單的日志實現(xiàn),不需要單獨引用 jar 包,支持多線程下輸出數(shù)據(jù)到同一文件中,但配置較為麻煩,在平時可使用其基本功能進(jìn)行測試或簡單日志輸出,不推薦在對日志有進(jìn)一步需求時使用。

七、Log4j 和 Log4j2

1. Log4j 單獨使用

1) 首先引入 log4j 依賴

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2) 然后在 src/main/resources 下構(gòu)建 log4j.properties 配置文件

### 配置根 Logger ###
### debug 是日志輸出級別,stdout\D\E 是日志輸出位置的別名 ###
log4j.rootLogger = debug,stdout,D,E

### 輸出信息到控制抬 ###
### stdout 為 ConsoleAppender ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
### Target 為 System.out 指定控制臺輸出(out 為輸出白色字體,err 為輸出紅色字體) ###
log4j.appender.stdout.Target = System.out
### layout 為 PatternLayout 靈活指定布局 ###
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
### layout.ConversionPattern 為轉(zhuǎn)換格式,值為格式化信息 ### 
log4j.appender.stdout.layout.ConversionPattern = %m

### 輸出DEBUG 級別以上的日志到=E://logs/error.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
### File 為 日志文件位置 ###
log4j.appender.D.File = E://logs/log.log
### Append 為 是否可以追加 ###
log4j.appender.D.Append = true
### Threshold 為 輸出該級別以上的日志 ###
log4j.appender.D.Threshold = DEBUG 
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

### 輸出ERROR 級別以上的日志到=E://logs/error.log ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File =E://logs/error.log 
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR 
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

3) 最后在多線程情況下輸出

package com.cmos.javalog.log4j;

import org.apache.log4j.Logger;

public class Log4JTest {
    private static Logger logger = Logger.getLogger(Log4JTest.class);
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.debug(i + " thread-1" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.debug(i + " thread-2" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.debug(i + " thread-3" + System.lineSeparator());
            }
        }).start();
    }
}

4) 輸出結(jié)果

輸出結(jié)果

5) 但在這種情況下,如果我需要自定義文件名稱的話,則需要加上以下代碼

FileAppender fileAppender = new FileAppender(new PatternLayout("%m"), "E:/logs/newLog.log");
logger.addAppender(fileAppender);

6) 結(jié)果:

結(jié)果
2. Log4j2 單獨使用

1) 引入依賴

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

2) 然后在 src/main/resources 下構(gòu)建 log4j2.xml 配置文件
<1> Log4j2 中不再支持像 Log4j 中的 .properties 后綴的文件配置方式,Log4j2 配置文件后綴名只能為".xml",".json"或者".jsn"。
<2> 系統(tǒng)選擇配置文件的優(yōu)先級(從先到后)如下:
a. classpath下的名為log4j2-test.json 或者log4j2-test.jsn的文件.
b. classpath下的名為log4j2-test.xml的文件.
c. classpath下名為log4j2.json 或者log4j2.jsn的文件.
d. classpath下名為log4j2.xml的文件.

<?xml version="1.0" encoding="UTF-8"?>
<!--configuration:根節(jié)點  status:定義log4j2本身打印日志的級別  
monitorinterval:指定log4j2自動重新配置的監(jiān)測間隔時間,單位是s,最小是5s -->
<Configuration status="INFO" monitorinterval="5" strict="true" name="LogConfig">
    <!-- configuration有兩個子節(jié)點:appenders loggers -->
    <Appenders>
        <!-- Console:定義輸出到控制臺的Appender  name:Console的屬性,指定Appender的名字.
        target:Console的屬性,SYSTEM_OUT 或 SYSTEM_ERR,一般只設(shè)置默認(rèn):SYSTEM_OUT.-->
        <Console name="Console" target="SYSTEM_OUT">
            <!--只接受程序中INFO級別及以上的日志進(jìn)行處理-->
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--PatternLayout:Console的子節(jié)點,輸出格式,不設(shè)置默認(rèn)為:%m%n.-->
            <PatternLayout pattern="%m"/>
        </Console>
        <!--RollingFile:定義超過指定大小自動刪除舊的創(chuàng)建新的的Appender.  
        name:RollingFile的屬性,指定Appender的名字.
        fileName:RollingFile的屬性,指定輸出日志的目的文件帶全路徑的文件名.  
        filePattern:RollingFile的屬性,指定新建日志文件的名稱格式.-->
        <RollingFile name="RollingFileINFO" fileName="./logs/INFO.log"
                     filePattern="logs/$${date:yyyy-MM}/INFO-%d{yyyy-MM-dd}-%i.log.gz">
            <!--Filters:RollingFile的子節(jié)點,決定日志事件能否被輸出。過濾條件有三個值:
            ACCEPT(接受), DENY(拒絕) or NEUTRAL(中立)-->
            <Filters>
                <!--只接受INFO級別的日志,其余的全部拒絕處理 onMatch:符合  onMismatch:不符合-->
                <ThresholdFilter level="INFO"/>
                <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!--PatternLayout:RollingFile的子節(jié)點,輸出格式,不設(shè)置默認(rèn)為:%m%n.-->
            <PatternLayout
                    pattern="[%d{yyyy-MM-dd HH:mm:ss}] %-5level %class{36} %L %M - %msg%xEx"/>
            <!--Policies:RollingFile的子節(jié)點,指定滾動日志的策略,
            就是什么時候進(jìn)行新建日志文件輸出日志.-->
            <Policies>
                <!--SizeBasedTriggeringPolicy:Policies子節(jié)點,基于指定文件大小的滾動策略,
                size屬性用來定義每個日志文件的大小.-->
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <!--TimeBasedTriggeringPolicy:Policies子節(jié)點,基于時間的滾動策略,
                interval屬性用來指定多久滾動一次,默認(rèn)是1hour。
                modulate=true用來調(diào)整時間:比如現(xiàn)在是早上3am,interval是4,
                那么第一次滾動是在4am,接著是8am,12am...而不是7am.-->
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
        <!--Routing:將日志事件分類,按條件分配給子appender  name:RoutingAppender名稱-->
        <Routing name="Routing">
            <!--Routes:包含一個或多個路由聲明來標(biāo)識選擇Appenders的標(biāo)準(zhǔn)。 pattern:上下文模板名稱-->
            <Routes pattern="$${ctx:log4jFile}">
                <Route>
                    <!--File:用于保存文件。 name:當(dāng)前Appender的命名  fileName:包含路徑的文件名-->
                    <File name="file" fileName="E:/logs/${ctx:log4jFile:-other.log}">
                        <PatternLayout pattern="%m"></PatternLayout>
                    </File>
                </Route>
            </Routes>
        </Routing>

    </Appenders>
    <!-- 然后定義logger,只有定義了logger并引入的appender,appender才會生效 -->
    <!--Loggers節(jié)點,常見的有兩種:Root和Logger.-->
    <Loggers>
        <!--root:指定項目的根日志,如果沒有單獨指定Logger,那么就會默認(rèn)使用該Root日志輸出
        level:日志輸出級別,共有8個級別,按照從低到高為:
        All < Trace < Debug < Info < Warn < Error < Fatal < OFF.-->
        <Root level="INFO">
            <!--appender-ref:用來指定該日志輸出到哪個Appender,通過ref指定.-->
            <Appender-ref ref="Console"/>
            <Appender-ref ref="RollingFileINFO"/>
        </Root>
        <!--logger:用來單獨指定日志的形式,比如要為指定包下的class指定不同的日志級別等。
        name:用來指定該Logger所適用的類或者類所在的包全路徑,繼承自Root節(jié)點.  additivity:是否疊加-->
        <Logger name="com.cmos.javalog" level="INFO"></Logger>
        <!--這個name要與java中LogManager.getLogger("log4jFile")相匹配-->
        <Logger name="log4jFile" level="INFO" >
            <AppenderRef ref="Routing"/>
        </Logger>
    </Loggers>
</Configuration>

多線程下測試輸出

package com.cmos.javalog.log4j2;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2Test {
    private static Logger logger = LogManager.getLogger("log4jFile");
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                logger.warn(i + " thread-1" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                logger.warn(i + " thread-2" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                logger.warn(i + " thread-3" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
    }
}

輸出結(jié)果

Log4j2輸出結(jié)果

總結(jié):
Log4j2 相比于 Log4j 提升的不止一星半點,首先Log4j2舍棄properties而只使用xml配置文件,大大的提高了可讀性。而且 Log4j2 的效率也遠(yuǎn)超 Log4j 和 Logback,個人感覺是目前最好用的日志實現(xiàn)。

八、Commons-logging

1. Commons-logging + JUL

寫在前面
目前 Commons-logging + JUL 在使用時只能通過修改 logging.properties 配置文件進(jìn)行控制日志信息輸出級別及路徑。如果大家知道更好的方法,請留言或私信,多謝!

引入依賴
上面說過,Commons-logging 會自動查找 classpath 下是否包含了 Log4j 的類庫,若沒有,則使用 JUL,因此我們只需要引入 commons-logging.jar 即可,同時需要排除 SpringBoot 啟動依賴中的日志依賴。

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

在 src/main/resources 下創(chuàng)建 commons-logging.properties 與 logging.properties
注: logging.properties 可依舊使用上面的配置

## commons-logging.properties

## 指定 Commons-logging 使用 JUL 實現(xiàn)
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger 

Commons-logging 支持很多日志實現(xiàn),針對 JUL 我們需要使用 Jdk14Logger。

-
Commons-logging 支持的日志實現(xiàn)

單線程下測試輸出
這里我們先不考慮多線程情況,只做最簡單的使用。

package com.cmos.javalog.jcl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;

public class CommonsLoggingJUL {
    public static void main(String[] args) throws IOException {
        Log log = LogFactory.getLog("CommonsJUL");
        log.trace("This is TRACE COMMONS-JUL");
        log.debug("This is DEBUG COMMONS-JUL");
        log.info("This is INFO COMMONS-JUL");
        log.warn("This is WARN COMMONS-JUL");
        log.error("This is ERROR COMMONS-JUL");
        log.fatal("This is FATAL COMMONS-JUL");
    }
}

日志結(jié)果

十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
信息: This is INFO COMMONS-JUL
十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
警告: This is WARN COMMONS-JUL
十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
嚴(yán)重: This is ERROR COMMONS-JUL
十一月 16, 2020 3:38:11 下午 com.cmos.javalog.jcl.CommonsLoggingJUL main
嚴(yán)重: This is FATAL COMMONS-JUL

可以看出,只能輸出 INFO 級別及以上的日志信息,而我們?nèi)绻胄薷娜罩据敵黾墑e,目前我已知的可以通過修改 logging.properties 的值來實現(xiàn),但每次運行前都需要修改。

多線程下測試輸出

## logging.properties

handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
.level= INFO
java.util.logging.FileHandler.pattern = E:/logs/CommonsJUL.log
java.util.logging.FileHandler.limit = 102400000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = com.cmos.javalog.jul.MyFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%4$s: %5$s
package com.cmos.javalog.jcl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;

public class CommonsLoggingJUL {
    public static void main(String[] args) throws IOException {
        // 設(shè)置JUL配置文件路徑,使用自定義的配置文件
        String path = CommonsLoggingJUL.class.getClassLoader()
                             .getResource("logging.properties").getPath();
        System.setProperty("java.util.logging.config.file", path);
        Log log = LogFactory.getLog("CommonsJUL");
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                log.info(i + " thread-1" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                log.info(i + " thread-2" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                log.info(i + " thread-3" + System.lineSeparator());
            }
        }).start();
    }
}

輸出結(jié)果

輸出結(jié)果

同樣的,對于寫入自定義文件同樣需要通過 logging.properties 的配置,若要修改日志文件名,則每次運行前都需要修改。

總結(jié):
使用 JCL + JUL 極其不方便,JCL 并未提供 JUL 中對 FileHandler 等屬性的配置方法,因此每次使用都需要修改配置文件,在多線程運行情況下不建議使用。

2. Commons-logging + Log4j2

引入依賴

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.3</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>

在 src/main/resources 下創(chuàng)建 commons-logging.properties 以及 log4j2.xml
注:其中 log4j2.xml 繼續(xù)使用上面的配置。

#################### commons-logging.properties ####################
## 指定 Commons-logging 使用 Log4j 實現(xiàn)
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

多線程下測試輸出
我們只需要用 Commons-logging 的 Log 來替換 Log4j2 的 Logger 即可。

package com.cmos.javalog.jcl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class CommonsLoggingLog4j2 {
    private static Log log = LogFactory.getLog("log4jFile");

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                log.warn(i + " thread-1" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                log.warn(i + " thread-2" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "log4j2.log");
                log.warn(i + " thread-3" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
    }
}

輸出結(jié)果

輸出結(jié)果

總結(jié):
使用 Commons-logging 其實就是使用它的 log 對象,在這個業(yè)務(wù)里,說實話沒有感覺到比單獨使用 Log4j2 來的方便。

九、Slf4j

1. Slf4j + JUL

引入依賴

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

多線程下運行

package com.cmos.javalog.slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4j2JUL {
    public static void main(String[] args) {
        // 設(shè)置配置文件路徑,使用自定義的配置文件
        System.setProperty("java.util.logging.config.file", Slf4j2JUL.class.getClassLoader()
                    .getResource("logging.properties").getPath());

        Logger logger = LoggerFactory.getLogger("Slf4j2JUL");
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.info("{}", i + " thread-1" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.info("{}", i + " thread-2" + System.lineSeparator());
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                logger.info("{}", i + " thread-3" + System.lineSeparator());
            }
        }).start();
    }
}

輸出結(jié)果

輸出結(jié)果

總結(jié):
Slf4j + JUL 與 Commons-logging + JUL 使用幾乎相同,優(yōu)缺點也差不多,畢竟 Slf4j 與 Commons-logging 都只是一個門面,具體的實現(xiàn)都是使用的 JUL。不同之處則是相比于 Commons-logging ,Slf4j 不需要通過配置文件來選擇具體實現(xiàn)框架,而是通過添加不同的依賴,在這一點上,二者也是各有千秋,但是 Slf4j 采用的 {} 作為占位符則更節(jié)省系統(tǒng)資源。

2. Slf4j + Log4j2

引入依賴

<!-- slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!-- log4j2 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.13.3</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.13.3</version>
</dependency>
<!-- slf4j與log4j2的橋梁 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.13.3</version>
</dependency>

在 src/main/resources 下添加 log4j2.xml
注: 這里繼續(xù)使用上面的 log4j2.xml

多線程下運行測試

package com.cmos.javalog.slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4j2Log4j2 {

    private static Logger logger = LoggerFactory.getLogger("log4jFile");

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "Slf4j2Log4j2.log");
                logger.warn("{}", i + " thread-1" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "Slf4j2Log4j2.log");
                logger.warn("{}", i + " thread-2" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ThreadContext.put("log4jFile", "Slf4j2Log4j2.log");
                logger.warn("{}", i + " thread-3" + System.lineSeparator());
                ThreadContext.clearAll();
            }
        }).start();
    }
}

輸出結(jié)果

輸出結(jié)果

總結(jié):
這里也明顯看的出來,與上面的 Commons-logging + Log4j2 幾乎沒有任何區(qū)別。在使用日志時,絕大多數(shù)的差異是由日志具體實現(xiàn)框架來決定的,不同的實現(xiàn)也會有不同的效率與結(jié)果。

3. Slf4j + Logback

引入依賴

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>

在 src/main/resources 下添加 logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--configuration 子節(jié)點為 appender、logger、root
scan:當(dāng)此屬性設(shè)置為true時,配置文件如果發(fā)生改變,將會被重新加載,默認(rèn)值為true。
scanPeriod:設(shè)置監(jiān)測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認(rèn)單位是毫秒。
           當(dāng)scan為true時,此屬性生效。默認(rèn)的時間間隔為1分鐘。
debug:當(dāng)此屬性設(shè)置為true時,將打印出logback內(nèi)部日志信息,實時查看logback運行狀態(tài)。默認(rèn)值為false。-->
<configuration scan="true" scanPeriod="5 seconds" debug="false">
    <!--用于區(qū)分不同應(yīng)用程序的記錄-->
    <contextName>edu-cloud</contextName>
    <!--日志文件所在目錄-->
    <property name="LOG_HOME" value="E:/logs"/>

    <!--控制臺-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度 
            %logger輸出日志的logger名 %msg:日志消息,%n是換行符 -->
            <pattern>
                [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg
            </pattern>
            <!--解決亂碼問題-->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--滾動文件-->
    <appender name="infoFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- ThresholdFilter:臨界值過濾器,過濾掉 TRACE 和 DEBUG 級別的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/log.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory><!--保存最近30天的日志-->
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>
                [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg
            </pattern>
        </encoder>
    </appender>

    <!--SiftingAppender:篩選并分配  根據(jù)discriminator 值篩選<sift>中的appender-->
    <appender name="logbackFile" class="ch.qos.logback.classic.sift.SiftingAppender">
        <!--discriminator不指定具體class時,默認(rèn)使用MDCBasedDiscriminator,
        因此才能在下方Java代碼中使用MDC進(jìn)行傳值-->
        <discriminator>
            <key>logFileName</key>
            <defaultValue>default</defaultValue>
        </discriminator>
        <sift>
            <appender name="File-${logFileName}" class="ch.qos.logback.core.FileAppender">
                <file>E:/logs/${logFileName}.log</file>
                <append>true</append>
                <encoder charset="UTF-8">
                    <Pattern>%msg</Pattern>
                </encoder>
            </appender>
        </sift>
    </appender>

    <!--這里如果是info,spring、mybatis等框架則不會輸出:TRACE < DEBUG < INFO <  WARN < ERROR-->
    <!--root是所有l(wèi)ogger的祖先,均繼承root,如果某一個自定義的logger沒有指定level,就會尋找
    父logger看有沒有指定級別,直到找到root。-->
    <root level="debug">
        <appender-ref ref="console"/>
        <appender-ref ref="infoFile"/>

    </root>

    <logger name="com.cmos.javalog" level="INFO">
        <appender-ref ref="logbackFile"/>
    </logger>

    <!--為某個包單獨配置logger

    比如定時任務(wù),寫代碼的包名為:com.seentao.task
    步驟如下:
    1、定義一個appender,取名為task(隨意,只要下面logger引用就行了)
    appender的配置按照需要即可

    2、定義一個logger:
    <logger name="com.seentao.task" level="DEBUG" additivity="false">
      <appender-ref ref="task" />
    </logger>
    注意:additivity必須設(shè)置為false,這樣只會交給task這個appender,
    否則其他appender也會打印com.seentao.task里的log信息。

    3、這樣,在com.seentao.task的logger就會是上面定義的logger了。
    private static Logger logger = LoggerFactory.getLogger(Class1.class);
    -->

</configuration>

多線程下運行測試

package com.cmos.javalog.slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class Slf4j2Logback {
    private static Logger logger = LoggerFactory.getLogger(Slf4j2Logback.class);

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                MDC.put("logFileName","Slf4j2Logback");
                logger.info("{}", i + " thread-1" + System.lineSeparator());
                MDC.remove("logFileName");
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                MDC.put("logFileName","Slf4j2Logback");
                logger.info("{}", i + " thread-2" + System.lineSeparator());
                MDC.remove("logFileName");
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                MDC.put("logFileName","Slf4j2Logback");
                logger.info("{}", i + " thread-3" + System.lineSeparator());
                MDC.remove("logFileName");
            }
        }).start();
    }
}

輸出結(jié)果

輸出結(jié)果

總結(jié):
Logback 中使用 SiftingAppender ,與 Log4j2 中的 RoutingAppender功能相同 ,Logback并為之匹配了一個 MDC 類用于傳遞篩選參數(shù),在這個業(yè)務(wù)中,相比 Log4j 來說,Logback 更加解耦,但效率比不上 Log4j2。

本文如有不足或有誤之處,歡迎大家指正,私信或留言均可。

本文參考:
http://www.lxweimin.com/p/bbbdcb30bba8
https://blog.csdn.net/waitgod/article/details/78750184
https://www.cnblogs.com/chenhongliang/p/5312517.html
http://logging.apache.org/log4j/2.x/manual/appenders.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一、聊聊java中混亂的日志體系 ???♀?波妞:先提個問題,你知道哪些日志框架? ???♀?波妞:呃。。。我說說搜我...
    c夢2019閱讀 234評論 0 2
  • 對于Java的日志框架,你也許會經(jīng)常看到這些名詞: Log4j、Log4j2 Logback Slf4j JCL ...
    NoahU閱讀 3,972評論 0 15
  • 作為Java開發(fā)人員,對于日志記錄框架一定非常熟悉。而且?guī)缀踉谒袘?yīng)用里面,一定會用到各種各樣的日志框架用來記錄程...
    意識流丶閱讀 14,035評論 0 13
  • 在項目開發(fā)過程中,我們可以通過 debug 查找問題。而在線上環(huán)境我們查找問題只能通過打印日志的方式查找問題。因此...
    Java架構(gòu)閱讀 3,499評論 2 41
  • [TOC] Java 日志框架解析:設(shè)計模式、性能 在平常的系統(tǒng)開發(fā)中,日志起到了重要的作用,日志寫得好對于線上問...
    albon閱讀 4,187評論 1 8