從Slf4j源碼角度分析阿里開發(fā)手冊日志規(guī)約
本文轉(zhuǎn)載自微信公眾號(hào)「JAVA前線」,作者IT徐胖子 。轉(zhuǎn)載本文請聯(lián)系JAVA前線公眾號(hào)。
1 日志規(guī)約
《阿里巴巴開發(fā)手冊》日志規(guī)約章節(jié)有一條強(qiáng)制規(guī)定:應(yīng)用中不可直接使用日志系統(tǒng)(Log4j、Logback)API,而應(yīng)依賴使用日志框架SLF4J中的API。使用門面模式的日志框架,有利于維護(hù)和各個(gè)類的日志處理方式統(tǒng)一:
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- private static final Logger logger = LoggerFactory.getLogger(Abc.class);
我們在使用日志框架過程中會(huì)發(fā)現(xiàn),日志框架種類很多如slf4j、log4j、logback等等,在引入依賴時(shí)很容易混淆。那么這些框架是什么關(guān)系、應(yīng)該如何使用就是本文需要回答的問題。
2 實(shí)例分析
在編寫代碼之前我們首先了解slf4j全稱,我認(rèn)為這會(huì)對理解這個(gè)框架有所幫助:
- Simple Logging Facade for Java
全稱的含義就是Java簡單日志門面,我們知道有一種設(shè)計(jì)模式被稱為門面模式,其本質(zhì)是化零為整,通過一個(gè)對象將散落在各處的功能整合在一起,這樣外部只要通過與這個(gè)對象交互,由該對象選擇具體實(shí)現(xiàn)細(xì)節(jié)。slf4j就是這樣一個(gè)門面,應(yīng)用程序只需要和slf4j進(jìn)行交互,slf4j選擇使用哪一個(gè)日志框架的具體實(shí)現(xiàn)。
2.1 slf4j-jdk14
(1) 引入依賴
- <dependencies>
- <!-- slf4j -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- <version>1.7.30</version>
- </dependency>
- <!-- jdk14 -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-jdk14</artifactId>
- <version>1.7.30</version>
- </dependency>
- </dependencies>
(2) 代碼實(shí)例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 輸出日志
- LogTest
- 三月 14, 2021 11:39:14 上午 com.my.log.test.jdk14.LogTest main
- 信息: info message
- 三月 14, 2021 11:39:14 上午 com.my.log.test.jdk14.LogTest main
- 嚴(yán)重: error message
2.2 slf4j-simple
(1) 引入依賴
- <dependencies>
- <!-- slf4j -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- <version>1.7.30</version>
- </dependency>
- <!-- simple -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-simple</artifactId>
- <version>1.7.30</version>
- </dependency>
- </dependencies>
(2) 代碼實(shí)例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 輸出日志
- [main] INFO com.my.log.test.simple.LogTest - info message
- LogTest
- [main] ERROR com.my.log.test.simple.LogTest - error message
2.3 logback
(1) 引入依賴
- <dependencies>
- <!-- slf4j -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- <version>1.7.30</version>
- </dependency>
- <!-- logback -->
- <dependency>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-core</artifactId>
- <version>1.2.3</version>
- </dependency>
- <dependency>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-classic</artifactId>
- <version>1.2.3</version>
- </dependency>
- </dependencies>
(2) 代碼實(shí)例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 輸出日志
- 11:40:53.406 [main] INFO com.my.log.test.logbck.LogTest - info message
- LogTest
- 11:40:53.410 [main] ERROR com.my.log.test.logbck.LogTest - error message
2.4 slf4j-log4j12
(1) 引入依賴
- <dependencies>
- <!-- slf4j -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- <version>1.7.30</version>
- </dependency>
- <!-- log4j12 -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- <version>1.7.30</version>
- </dependency>
- </dependencies>
(2) 代碼實(shí)例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 日志配置
- <log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
- <appender name="myConsoleAppender" class="org.apache.log4j.ConsoleAppender">
- <layout class="org.apache.log4j.PatternLayout">
- <param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
- </layout>
- <!--過濾器設(shè)置輸出級別 -->
- <filter class="org.apache.log4j.varia.LevelRangeFilter">
- <param name="levelMin" value="debug" />
- <param name="levelMax" value="error" />
- <param name="AcceptOnMatch" value="true" />
- </filter>
- </appender>
- <root>
- <priority value="debug" />
- <appender-ref ref="myConsoleAppender" />
- </root>
- </log4j:configuration>
(4) 輸出日志
- [14 11:41:39,198 INFO ] [main] log4j.LogTest - info message
- LogTest
- [14 11:41:39,201 ERROR] [main] log4j.LogTest - error message
3 源碼分析
我們發(fā)現(xiàn)上述實(shí)例中Java代碼并沒有變化,只是將引用具體日志框架實(shí)現(xiàn)進(jìn)行了替換,例如依賴從simple替換為log4j,具體日志服務(wù)實(shí)現(xiàn)就替換成了log4j,這到底是怎么實(shí)現(xiàn)的?我們通過閱讀源碼回答這個(gè)問題。
3.1 閱讀準(zhǔn)備
(1) 源碼地址
目前最新版本2.0.0-alpha2-SNAPSHOT
- https://github.com/qos-ch/slf4j
(2) 項(xiàng)目結(jié)構(gòu)
我們從項(xiàng)目結(jié)構(gòu)可以看出一些信息:門面是api模塊,具體實(shí)現(xiàn)包括jdk14、log4j12、simple模塊,需要注意logback是同一個(gè)作者的另一個(gè)項(xiàng)目不在本項(xiàng)目。
(3) 閱讀入口
- package org.slf4j;
- public class NoBindingTest {
- public void testLogger() {
- Logger logger = LoggerFactory.getLogger(NoBindingTest.class);
- logger.debug("hello" + diff);
- assertTrue(logger instanceof NOPLogger);
- }
- }
3.2 源碼分析
LoggerFactory.getLogger
- public final class LoggerFactory {
- public static Logger getLogger(Class<?> clazz) {
- Logger logger = getLogger(clazz.getName());
- if (DETECT_LOGGER_NAME_MISMATCH) {
- Class<?> autoComputedCallingClass = Util.getCallingClass();
- if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
- Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
- autoComputedCallingClass.getName()));
- Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
- }
- }
- return logger;
- }
- }
getLogger(clazz.getName())
- public final class LoggerFactory {
- public static Logger getLogger(String name) {
- ILoggerFactory iLoggerFactory = getILoggerFactory();
- return iLoggerFactory.getLogger(name);
- }
- }
getILoggerFactory()
- public final class LoggerFactory {
- public static ILoggerFactory getILoggerFactory() {
- return getProvider().getLoggerFactory();
- }
- }
getProvider()
- public final class LoggerFactory {
- static SLF4JServiceProvider getProvider() {
- if (INITIALIZATION_STATE == UNINITIALIZED) {
- synchronized (LoggerFactory.class) {
- if (INITIALIZATION_STATE == UNINITIALIZED) {
- INITIALIZATION_STATE = ONGOING_INITIALIZATION;
- performInitialization();
- }
- }
- }
- switch (INITIALIZATION_STATE) {
- case SUCCESSFUL_INITIALIZATION:
- return PROVIDER;
- case NOP_FALLBACK_INITIALIZATION:
- return NOP_FALLBACK_FACTORY;
- case FAILED_INITIALIZATION:
- throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
- case ONGOING_INITIALIZATION:
- return SUBST_PROVIDER;
- }
- throw new IllegalStateException("Unreachable code");
- }
- }
performInitialization()
- public final class LoggerFactory {
- private final static void performInitialization() {
- bind();
- if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
- versionSanityCheck();
- }
- }
- }
bind()
- public final class LoggerFactory {
- private final static void bind() {
- try {
- // 核心代碼
- List<SLF4JServiceProvider> providersList = findServiceProviders();
- reportMultipleBindingAmbiguity(providersList);
- if (providersList != null && !providersList.isEmpty()) {
- PROVIDER = providersList.get(0);
- PROVIDER.initialize();
- INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
- reportActualBinding(providersList);
- }
- // 省略代碼
- } catch (Exception e) {
- failedBinding(e);
- throw new IllegalStateException("Unexpected initialization failure", e);
- }
- }
- }
findServiceProviders()
這是加載具體日志實(shí)現(xiàn)的核心方法,使用SPI機(jī)制加載所有SLF4JServiceProvider實(shí)現(xiàn)類:
- public final class LoggerFactory {
- private static List<SLF4JServiceProvider> findServiceProviders() {
- ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
- List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
- for (SLF4JServiceProvider provider : serviceLoader) {
- providerList.add(provider);
- }
- return providerList;
- }
- }
SPI(Service Provider Interface)是一種服務(wù)發(fā)現(xiàn)機(jī)制,本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件加載實(shí)現(xiàn)類,這樣可以在運(yùn)行時(shí)動(dòng)態(tài)為接口替換實(shí)現(xiàn)類,通過SPI機(jī)制可以為程序提供拓展功能。本文以log4j為例說明使用SPI功能的三個(gè)步驟:
(a) 實(shí)現(xiàn)接口
- public class Log4j12ServiceProvider implements SLF4JServiceProvider
(b) 配置文件
- 文件位置:src/main/resources/META-INF/services/
- 文件名稱:org.slf4j.spi.SLF4JServiceProvider
- 文件內(nèi)容:org.slf4j.log4j12.Log4j12ServiceProvider
(c) 服務(wù)加載
- public final class LoggerFactory {
- private static List<SLF4JServiceProvider> findServiceProviders() {
- ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
- List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
- for (SLF4JServiceProvider provider : serviceLoader) {
- providerList.add(provider);
- }
- return providerList;
- }
- }
只要各種日志實(shí)現(xiàn)框架按照SPI約定進(jìn)行代碼編寫和配置文件聲明,即可以被LoggerFactory加載,slf4j會(huì)獲取第一個(gè)作為實(shí)現(xiàn)。
- public final class LoggerFactory {
- private final static void bind() {
- try {
- // 使用SPI機(jī)制加載具體日志實(shí)現(xiàn)
- List<SLF4JServiceProvider> providersList = findServiceProviders();
- reportMultipleBindingAmbiguity(providersList);
- if (providersList != null && !providersList.isEmpty()) {
- // 獲取第一個(gè)實(shí)現(xiàn)
- PROVIDER = providersList.get(0);
- PROVIDER.initialize();
- INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
- reportActualBinding(providersList);
- }
- // 省略代碼
- } catch (Exception e) {
- failedBinding(e);
- throw new IllegalStateException("Unexpected initialization failure", e);
- }
- }
- }
分析到這里我們的問題應(yīng)該可以得到解答:假設(shè)我們項(xiàng)目只引入了slf4j和log4j,相當(dāng)于只有l(wèi)og4j這一個(gè)具體實(shí)現(xiàn),那么本項(xiàng)目就會(huì)使用log4j框架。如果將log4j依賴換為logback,那么項(xiàng)目在不改動(dòng)代碼的情況下會(huì)使用logback框架。
4 文章總結(jié)
本文我們從阿里開發(fā)手冊日志規(guī)約出發(fā),首先分析了如何使用不同的日志框架,然后我們從問題出發(fā)(不修改代碼即可替換具體日志框架)進(jìn)行slf4j源碼閱讀,從源碼中我們知道實(shí)現(xiàn)核心是SPI機(jī)制,這個(gè)機(jī)制可以動(dòng)態(tài)加載具體日志實(shí)現(xiàn)。關(guān)于SPI源碼分析請參看筆者文章JDK SPI機(jī)制,希望本文對大家有所幫助。