Spring周邊:日誌——中
- 2019 年 11 月 26 日
- 筆記

有日誌么?
日誌有價值么?
日誌框架了解么?

「日誌」這個詞最早見於航海領域,是記錄航行主要情況的載體文件,內容包括操作指令、氣象、潮流、航向、航速、旅客、貨物等,是處理海事糾紛或者海難的原始依據之一。爾後延伸到航空領域,黑匣子就是一個重要的航空日誌載體,調查空難原因時第一反應是找到黑匣子,並通過解析其中的日誌資訊來還原空難的事實真相 碼出高效:Java開發手冊
門面設計模式是面面向對象設計模式中的一種,日誌框架採用的就是這種模式,類似JDBC的設計理念。它只提供一套介面規範,自身不負責日誌功能的實現,目的是讓使用者不需要關注底層具體是哪個日誌庫來負責日誌列印及具體的使用細節。目前用的最廣泛的日誌門面有兩種:SLF4J 和 Apache Commons Logging(JCL)。
本期內容提要
Apache Commons Logging(JCL)
與
JUL、Log4j1、Log4j2、Logback
的集成原理
1. 前置知識——SPI
SPI ,全稱為 Service Provider Interface,是一種服務發現機制。是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啟用框架擴展和替換組件。

1.1. 規則
- 當服務提供者提供了介面的一種具體實現後,在 jar 包 META-INF/services 目錄下創建一個以「介面全限定名」為命名的文件,內容為實現類的全限定名;
- 介面實現類所在的 jar 包放在主程式的 classpath 中;
- 主程式通過 java.util.ServiceLoder 動態裝載實現模組,它通過掃描 META-INF/services 目錄下的配置文件找到實現類的全限定名,把類載入到JVM;
- SPI 的實現類必須攜帶一個不帶參數的構造方法;

1.2. 示例
介面:
package webj2ee.spi; public interface SPIService { void execute(); }實現類:
package webj2ee.spi; public interface SPIService { void execute(); }package webj2ee.spi; public class SPIImpl2 implements SPIService { @Override public void execute() { System.out.println("SPIService Impl 2..."); } }SPI配置:webj2ee.spi.SPIService
webj2ee.spi.SPIImpl1 webj2ee.spi.SPIImpl2


測試類:
import webj2ee.spi.SPIService; import java.util.ServiceLoader; public class SPIServiceTest { public static void main(String[] args) { ServiceLoader<SPIService> services = ServiceLoader.load(SPIService.class); for (SPIService s : services) { s.execute(); } } }
1.3. 原理分析
a. ServiceLoader 核心成員變數:

b. ServiceLoader 構造器,內部構建懶載入迭代器;

c. LazyIterator 主體結構;

d. LazyIterator 的 hasNextService() 原理

e. LazyIterator 的 nextService() 原理

2. JCL 深度分析
Apache Commons Logging(JCL) 是 Apache 下的開源項目,是 Apache 提供的日誌的門面介面。提供簡單的日誌實現以及日誌解耦功能。
2.1. POM 依賴
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
2.2. 核心介面
JCL 日誌門面對外暴露兩個介面:
- org.apache.commons.logging.Log:日誌對象
- org.apache.commons.logging.LogFactory:日誌對象工廠
package webj2ee; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { private static Log logger = LogFactory.getLog(JCL.class); public static void main(String[] args) { logger.info("From jcl's info msg!"); logger.warn("From jcl's warn msg!"); logger.error("From jcl's error msg!"); } }JCL 日誌對象介面,定義了日誌操作的 5 個級別:
TRACE< DEBUG < INFO < WARN < ERROR

2.3. 源碼總體構成


2.4. 源碼分析——Log
- org.apache.commons.logging.Log 是日誌對象介面;
- 對日誌劃分 6 個等級:TRACE < DEBUG < INFO < WARN < ERROR < FATAL;
- 常用實現類:NoOpLog、SimpleLog、Jdk14Logger、Log4JLogger;

2.4.1. NoOpLog
NoOpLog 實現了 Log 介面,所有實現都是空的,沒什麼卵用,了解即可。

2.4.2. SimpleLog
JCL 內置的簡單日誌記錄器,實現了 Log 介面,日誌的輸出目標是 System.err;
- SimpleLog 本身提供了幾個簡單的控制參數;

- SimpleLog 構造器,主要負責解析日誌記錄器的級別;

- SimpleLog 的日誌格式控制;

- SimpleLog 默認輸出到 System.err;

commons-logging.properties:控制 JCL 使用 SimpleLog 作為日誌記錄器;
org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
simplelog.properties:SimpleLog 的參數配置;
## default level org.apache.commons.logging.simplelog.defaultlog=INFO ## webj2ee.JCL's log level org.apache.commons.logging.simplelog.log.webj2ee.JCL=WARN ## enable showdatetime org.apache.commons.logging.simplelog.showdatetime=true org.apache.commons.logging.simplelog.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS zzz ## enable showlogname; org.apache.commons.logging.simplelog.showShortLogname=false org.apache.commons.logging.simplelog.showlogname=true
測試程式:
package webj2ee; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { private static Log logger = LogFactory.getLog(JCL.class); public static void main(String[] args) { logger.info("From jcl's info msg!"); logger.warn("From jcl's warn msg!"); logger.error("From jcl's error msg!"); } }輸出結果:

2.4.3. Jdk14Logger
JCL 內置的 JUL(Java Util Logging)日誌記錄適配器,將日誌記錄的工作委託給 JUL 處理 ;
- Jdk14Logger 與 JUL 、JCL 的關係;


- Jdk14Logger 構造器,通過 getLogger() 方法獲取 JUL 的日誌記錄器;


- Jdk14Logger 最終使用 JUL 的 logp 方法記錄日誌;

- JUL 與 JCL 的日誌級別映射關係;

2.4.4. Log4JLogger
JCL 內置的 Log4j1 日誌記錄適配器,將日誌記錄的工作委託給 Log4j1 處理 ;
- Log4JLogger 與 Log4j、JCL 的關係;


- Log4JLogger 構造器,通過 getLogger() 方法獲取 Log4j 的日誌記錄器;


2.5. 源碼分析——LogFactory
2.5.1. LogFactory 基本關係回顧

2.5.2. LogFactory 最常用方法

上述獲取Log的過程大致分成2個階段
- 獲取 LogFactory;
- 通過 LogFactory 的 getInstance 介面獲取 Log;
2.5.3. 獲取 LogFactory 的過程解析
1. LogFactory 的關鍵常量

2. LogFactory實現類查找過程:
- 嘗試通過系統屬性【org.apache.commons.logging.LogFactory】值獲取;
- 嘗試通過 SPI 模式【META-INF/services/org.apache.commons.logging.LogFactory】獲取;
- 嘗試通過JCL配置文件【commons-logging.properties】中配置的【org.apache.commons.logging.LogFactory】屬性值獲取;
- 如果還找不到,就使用 JCL 默認的 LogFactory 實現類 【org.apache.commons.logging.impl.LogFactoryImpl】;

其實大多數情況下,使用的都是 JCL 默認提供的 LogFactory 實現類 LogFactoryImpl。
2.5.4. LogFactoryImpl 獲取 Log 的過程解析
1. LogFactoryImpl 的關鍵常量


2. LogFactoryImpl.getInstance 的關鍵路徑
- 從 commons-logging.properties 配置文件中尋找屬性為 org.apache.commons.logging.Log 對應的 Log 實現類;
- 從系統屬性中尋找屬性為 org.apache.commons.logging.Log 對應的 Log 實現類;
- 如果還沒招到,則按照 classesToDiscover 中定義的順序尋找;





3. JCL 與 JUL 集成
從前面分析可知, JCL 通過自動發現,即可完成與 JUL 的集成;
POM 配置:只需要引入JCL;
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
測試程式:
package webj2ee; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { private static Log logger = LogFactory.getLog(JCL.class); public static void main(String[] args) { logger.info("From jcl's info msg!"); logger.warn("From jcl's warn msg!"); logger.error("From jcl's error msg!"); } }JVM 啟動參數:使用自定義的 JUL 配置;
-Djava.util.logging.config.file=D:/jul.properties

D:/jul.properties:
handlers= java.util.logging.ConsoleHandler .level= INFO java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format=[JUL] %4$s: %5$s [%1$tc]%n
運行效果:

4. JCL 與 Log4j1 集成
從前面分析可知, JCL 通過自動發現,即可完成與 Log4j1 的集成,且自動發現的優先順序最高;
POM配置:引入 JCL 和 Log4j1.x;
<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>
測試程式:
package webj2ee; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { private static Log logger = LogFactory.getLog(JCL.class); public static void main(String[] args) { logger.info("From jcl's info msg!"); logger.warn("From jcl's warn msg!"); logger.error("From jcl's error msg!"); } }log4j.properties:使用自定義的 Log4j 配置;
log4j.rootLogger=INFO, Console #Console log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=[LOG4J1] %d [%t] %-5p [%c] - %m%n

5. JCL 與 Log4j2 集成
從前面分析可知, JCL 自身沒有 Log4j2 的適配器,所以引入 Log4j2 提供的 JCL 適配器。
POM配置:引入JCL、Log4j2、log4j-jcl;
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-jcl</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency>
測試程式:
package webj2ee; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { private static Log logger = LogFactory.getLog(JCL.class); public static void main(String[] args) { logger.info("From jcl's info msg!"); logger.warn("From jcl's warn msg!"); logger.error("From jcl's error msg!"); } }log4j2.xml:使用自定義的 Log4j2 配置文件;
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="[LOG4J2] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Root level="debug"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>運行結果:

集成原理分析
log4j-jcl 幹了什麼?


- log4j-jcl 通過 SPI 形式,控制 JCL 使用 log4j-jcl 中的 LogFactory 的實現類 org.apache.logging.log4j.jcl.LogFactoryImpl
- org.apache.logging.log4j.jcl.LogFactoryImpl 則再通過 適配器 模式,最終返回的是 org.apache.logging.log4j.jcl.Log4jLog 日誌記錄器,它會將日誌記錄工作委託給 Log4j2 實現。
6. JCL 與 Logback 集成
- JCL 自身沒有到 Logback 的適配器;
- Logback 默認實現的是 SLF4J 介面,通過 SLF4J-API 訪問;
- 所以 JCL 需要將日誌記錄請求中轉給 SLF4J,然後 SLF4J 再中轉給 Logback;
POM配置:引入JCL、SLF4J-API、Logback、jcl-over-slf4j;
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.29</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.29</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>
測試程式:
package webj2ee; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class JCL { private static Log logger = LogFactory.getLog(JCL.class); public static void main(String[] args) { logger.info("From jcl's info msg!"); logger.warn("From jcl's warn msg!"); logger.error("From jcl's error msg!"); } }logback.xml:使用自定義的 Logback 配置;
<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[LOGBACK] %d{HH:mm:ss.SSS} [[%thread]] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>運行結果:

集成原理分析
jcl-over-slf4j 幹了什麼?


顯然,還是老路子,通過 SPI 機制使用自定義的 LogFactory 實現類,然後實例化中轉到 SLF4J 的 Log 實現類;
參考:
《碼出高效:Java 開發手冊》 《Java 開發手冊 1.5.0》

