從SLF4J源碼角度分析阿里開發手冊日誌規約

歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我我的微信「java_front」一塊兒交流學習java

1 日誌規約

阿里巴巴開發手冊日誌規約章節有一條強制規定:應用中不可直接使用日誌系統(Log4j、Logback)API,而應依賴使用日誌框架SLF4J中的API。使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一:git

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

咱們在使用日誌框架過程當中會發現,日誌框架種類不少如slf4j、log4j、logback等等,在引入依賴時很容易混淆。那麼這些框架是什麼關係、應該如何使用就是本文須要回答的問題。github

2 實例分析

在編寫代碼以前咱們首先了解slf4j全稱,我認爲這會對理解這個框架有所幫助:apache

Simple Logging Facade for Java設計模式

全稱含義就是Java簡單日誌門面,咱們知道有一種設計模式稱爲門面模式,其本質是化零爲整,經過一個對象將散落在各處的功能整合在一塊兒,這樣外部只要經過與這個對象交互,由該對象選擇具體實現細節。slf4j就是這樣一個門面,應用程序只須要和slf4j進行交互,slf4j選擇使用哪個日誌框架的具體實現。api

從SLF4J源碼角度分析阿里開發手冊日誌規約

 

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

(2) 代碼實例架構

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) 輸出日誌app

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

嚴重: 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) 代碼實例

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

(2) 代碼實例

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) 代碼實例

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>
    <!--過濾器設置輸出級別 -->
    <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 源碼分析

咱們發現上述實例中Java代碼並無變化,只是將引用具體日誌框架實現進行了替換,例如依賴從simple替換爲log4j,具體日誌服務實現就替換成了log4j,這究竟是怎麼實現的?咱們經過閱讀源碼回答這個問題。

3.1 閱讀準備

(1) 源碼地址

目前最新版本2.0.0-alpha2-SNAPSHOT

https://github.com/qos-ch/slf4j

(2) 項目結構

咱們從項目結構能夠看出一些信息:門面是api模塊,具體實現包括jdk1四、log4j十二、simple模塊,須要注意logback是同一個做者的另外一個項目不在本項目。

從SLF4J源碼角度分析阿里開發手冊日誌規約

 

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

這是加載具體日誌實現的核心方法,使用SPI機制加載全部SLF4JServiceProvider實現類:

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)是一種服務發現機制,本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件加載實現類,這樣能夠在運行時動態爲接口替換實現類,經過SPI機制能夠爲程序提供拓展功能。本文以log4j爲例說明使用SPI功能的三個步驟:

(a) 實現接口

public class Log4j12ServiceProvider implements SLF4JServiceProvider

(b) 配置文件

文件位置:
src/main/resources/META-INF/services/

文件名稱:
org.slf4j.spi.SLF4JServiceProvider

文件內容:
org.slf4j.log4j12.Log4j12ServiceProvider

(c) 服務加載

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約定進行代碼編寫和配置文件聲明,便可以被LoggerFactory加載,slf4j會獲取第一個做爲實現。

public final class LoggerFactory {
    private final static void bind() {
        try {
            // 使用SPI機制加載具體日誌實現
            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);
        }
    }
}

分析到這裏咱們的問題應該能夠獲得解答:假設咱們項目只引入了slf4j和log4j,至關於只有log4j這一個具體實現,那麼本項目就會使用log4j框架。若是將log4j依賴換爲logback,那麼項目在不改動代碼的狀況下會使用logback框架。

4 文章總結

本文咱們從阿里開發手冊日誌規約出發,首先分析瞭如何使用不一樣的日誌框架,而後咱們從問題出發(不修改代碼便可替換具體日誌框架)進行slf4j源碼閱讀,從源碼中咱們知道實現核心是SPI機制,這個機制能夠動態加載具體日誌實現。關於SPI源碼分析請參看筆者文章DUBBO系列(1)什麼是SPI機制 ,但願本文對你們有所幫助。

歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我我的微信「java_front」一塊兒交流學習

相關文章
相關標籤/搜索