天天看點

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

作者:AC程式設計

第一節 掃地僧并不簡單

SLF4J對一個Java開發者來說,他就是一個不起眼的掃地僧,我們每天都能看到他,但幾乎沒有人去關注他,去多看他一眼,因為在我們眼裡,他實在是太普通、太簡單了,毫無亮點。但當我們靜下心來,慢慢地靠近他、走進他之後,我們會被他那些所蘊含的豐富的程式設計思想所驚歎,原來是我們想太簡單了。

第二節 SLF4J包含的程式設計思想與原則

SLF4J,即Java的簡單日志門面( Simple Logging Facade for Java SLF4J),作為一個簡單的門面或抽象,用來服務于各種各樣的日志架構,比如java.util.logging、logback和log4j。SLF4J允許最終使用者在部署時內建自己想要的日志架構(SPI機制)。簡單來說,SLF4J是Java日志的一個标準或規範,logging、logback和log4j是對該規範的具體實作(日志架構)。

這不禁讓我想到了JAP,同理,JPA也是一個标準,Hibernate、Spring data jpa、MyBaatis 是對該标準的具體實作(ORM架構)。

這種設計就展現了依賴倒置原則、面向抽象程式設計的思想、開閉原則、SPI機制等等。然而,這些概念和思想比較抽象,大部分人都很難了解,之前我們在單獨學習這些概念時都是一知半解。今天,我将用一個具體的例子SLF4J日志架構,并盡可能詳盡地來闡述這些概念和思想,希望我的這些淺見對您會有一些啟發,如果有錯誤之處,望不吝指正。

第三節 重溫SLF4J

我們先動手搭一個簡單的項目,來重溫一下我們平時是怎麼用SLF4J的,等操作完後我們再來細講SLF4J包含的程式設計思想與原則。

NO.3.1 用SLF4J輸出日志資訊

A、建立一個Maven項目

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

new maven project

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

maven project

B、編寫并運作測試代碼

導入slf4j依賴包

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

建立一個LogClient用來測試Log,注意Logger類用的是剛導入的org.slf4j.Logger,而不是其它包下的Logger,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

org.slf4j.Logger

用LoggerFactory工廠獲得一個Logger執行個體,并調用trace、info等方法列印日志資訊,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

列印日志

運作main函數,發現我們列印的日志資訊并沒有顯示在控制台,但我們看到控制台提示了SLF4J的預設實作是不做任何操作的,no-operation (NOP) ,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

運作測試代碼

是以我們需要給SLF4J配置一個實作架構,我們先用Logback

NO.3.2 引入SLF4J的實作架構Logback輸出日志

A、加入Logback

導入依賴包

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>           
Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

導入Logback

再次運作main函數,可以發現我們的日志資訊已經在控制台列印出來了,如下圖;

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

控制台顯示日志資訊

細心的同學可能已經發現了一個“小BUG”,我們要列印的trace怎麼沒有在控制台列印出來?因為這和log輸出的level有關系。

B、添加配置檔案logback.xml

現在我們來對logback進行相關配置,在src/main/resources目錄下建立logback.xml,将root level設定為trace ,詳細配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 控制台輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <Encoding>UTF-8</Encoding>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </layout>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <Encoding>UTF-8</Encoding>
        <!-- 指定日志檔案的名稱 -->
        <file>/Users/alanchen/temp/log/test.log</file>

        <!--
           日志輸出格式:%d表示日期時間,%thread表示線程名,%-5level:級别從左顯示5個字元寬度
           %logger{50} 表示logger名字最長50個字元,否則按照句點分割。 %msg:日志消息,%n是換行符
        -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
        </layout>

    </appender>

    <!--
    root與logger是父子關系,沒有特别定義則預設為root,任何一個類隻會和一個logger對應,
    要麼是定義的logger,要麼是root,判斷的關鍵在于找到這個logger,然後判斷這個logger的appender和level。
    -->
    <root level="trace">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>           

我們再次運作main函數,我們發現控制台已經列印出trace資訊了,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

控制台顯示trace

另外,我們在配置檔案中配置了輸出檔案,我們打開該輸出檔案檢視一下輸出資訊

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

test.log

其實,slf4j的dependency配置可以去掉,因為logback的dependency已經包含了slf4j,我們注釋slf4j後再次運作項目,項目依然能正常運作并輸出日志資訊,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

正常運作并輸出日志資訊

NO.3.3 引入SLF4J的實作架構Log4j輸出日志

我們将用Log4j來替代Logback,是以logback.xml配置檔案以及Logback的dependency可以去掉了,我們加入log4j的依賴并運作項目,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

Log4j

并沒有在控制台列印日志資訊,我們加上Log4j的配置檔案log4j.properties,配置如下:

log4j.rootLogger=trace,stdout,file

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c] [%p] - %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=/Users/alanchen/temp/log/test2.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c] [%p] - %m%n           

再運作項目,如下圖:

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

log4j運作結果

到目前為止,日志架構的操作我們已經示範完了,接下來我們進入依賴倒置原則、面向抽象程式設計思想闡述部分。

第四節 依賴倒置原則、面向抽象程式設計、開閉原則、SPI機制

NO.4.1 依賴倒置原則定義

  • 上層子產品不應該依賴底層子產品,它們都應該依賴于抽象。
  • 抽象不應該依賴于細節,細節應該依賴于抽象。

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

NO.4.2 面向抽象程式設計

面向抽象程式設計,或者叫面向接口程式設計,也或者叫針對接口程式設計,不針對實作程式設計。

“針對接口程式設計”真正的意思是“針對超類型程式設計”,這樣才能實作多态。超類型可以是父類、抽象類、接口。

針對接口程式設計,可以隔離掉以後系統可能發生的一大堆改變。如果代碼是針對接口而寫,那麼通過多态,它可以與任何新類實作該接口。但是,當代碼使用大量的具體類時,等于是自找麻煩,因為一旦加入新的具體類,就必須改變代碼,打破開閉原則。

NO.4.3 開閉原則定義

一個軟體實體,如類、子產品和函數應該對擴充開放,對修改關閉。

Software entities like classes, modules and functions should be open for extension but closed for modification

NO.4.4 SPI機制

A、什麼是SPI機制

SPI 全稱為 (Service Provider Interface) ,是JDK内置的一種服務提供發現機制,可以輕松實作面向服務的注冊與發現,完成服務提供與使用的解耦,并且可以實作動态加載。

引入服務提供者就是引入了SPI接口的實作者,通過本地的注冊發現擷取到具體的實作類,輕松可插拔,SPI實際上是“基于接口的程式設計+政策模式+配置檔案”組合實作的動态加載,為某個接口尋找服務實作的機制。

我的了解就是上層提供接口,我們需要去實作,并且上層隻需要根據我們的配置檔案即可拿到我們的實作類(反射擷取)。

B、Java SPI的具體約定為

當服務的提供者,提供了服務接口的一種實作之後,在jar包的META-INF/services/目錄裡同時建立一個以服務接口命名的檔案。該檔案裡就是實作該服務接口的具體實作類。而當外部程式裝配這個子產品的時候,就能通過該jar包META-INF/services/裡的配置檔案找到具體的實作類名,并裝載執行個體化,完成子產品的注入。 基于這樣一個約定就能很好的找到服務接口的實作類,而不需要再代碼裡制定。JDK提供服務實作查找的一個工具類:java.util.ServiceLoader。

第五節 SLF4J如何展現了依賴倒置原則、面向抽象程式設計、開閉原則、SPI機制?

NO5.1 依賴倒置原則

怎麼了解依賴倒置?倒置的是什麼?其實從依賴倒置這個名詞中就能找到答案,倒置的是依賴,具體來說倒置的是依賴關系。從依賴倒置這個名詞中,我們隐約能感受到這是非正常的逆向思維的,有依賴倒置,就有依賴正置。

  • 依賴正置:即正常思維,依賴的是具體/實作。
  • 依賴倒置:即逆向思維,依賴的是抽象。

其實依賴倒置核心也就是面向接口程式設計/面向抽象程式設計。

首先我們來談依賴倒置原則的上半句[上層子產品不應該依賴底層子產品,它們都應該依賴于抽象],上層子產品是日志架構的使用者,在上面例子中就是LogClient這個類,顯然,底層子產品(或叫下層子產品)就是日志架構了。在例子中LogClient用的是org.slf4j.Logger這個接口,是以上層子產品依賴的是抽象,同時,Logback、log4j都對該接口進行了實作,是以底層子產品依賴的也是抽象。

想象一下,如果上層子產品依賴底層子產品,即LogClient直接用的是Logback的實作類,再想把Logback換成了Log4j,我們就需要去改LogClient,目前是隻有一個Client,真實項目中就是n多個Client。

再來,我們繼續來談談依賴倒置原則的下半句[抽象不應該依賴于細節,細節應該依賴于抽象],其實說的還是面向接口程式設計。我們再拆開來說:

【抽象不應該依賴于細節】,即我們的依賴要用接口或抽象類(抽象),而不是具體實作類(細節)。【細節應該依賴于抽象】,說的是我們的類要實作接口,或繼承抽象類,如:

public abstract class Pizza {
}

public class CheesePizza extends Pizza{
}

public interface IPizzaStroeService {

    // 抽象依賴抽象,沒有依賴細節
    void order(Pizza pizza);

    // 抽象依賴了細節
    void order(CheesePizza pizza);
}           

NO5.2 面向抽象程式設計

上層和底層都依賴抽象有什麼好處?好處顯然易見,LogClient我一行代碼沒改,就把Logback換成了Log4j,這就是面向抽象程式設計的好處,面向抽象程式設計使我們的程式更具擴充性。我們常說的面向抽象程式設計也叫面向接口程式設計。

NO5.3 開閉原則

我們再來看,這有沒有展現開閉原則?當然有展現,沒有改LogClient一行代碼,就把Logback換成了Log4j,對修改進行了關閉。如果我們對Logback、Log4j都不滿意,自己寫一個日志架構LogAC也去實作SLF4J,然後項目中換成自己的日志架構LogAC,這就是對擴充開放。

NO5.4 SPI機制

SLF4J是Java日志的一個标準或規範,logging、logback和log4j是對該規範的具體實作,并可以無縫切換,通過檢視源碼我們可以發現SPI的身影。

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

logback的jar包

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

LogbackServletContainerInitializer

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

ServletContainerInitializer

第六節 SPI機制擴充說明

NO.6.1 SPI的另一應用場景 JDBC驅動

通常各大廠商(如MySQL、Oracle)會根據一個統一的規範(java.sql.Driver)開發各自的驅動實作邏輯。用戶端使用JDBC時不需要去改變代碼,直接引入不同的SPI接口服務即可。Mysql的則是com.mysql.jdbc.Drive,Oracle則是oracle.jdbc.driver.OracleDriver

Java掃地僧SLF4J(依賴倒置原則、面向抽象程式設計、開閉原則、SPI)

MySql的jar包

NO.6.2 SpringBoot中的類SPI擴充機制

在SpringBoot的自動裝配過程中,最終會加載META-INF/spring.factories檔案,而加載的過程是由SpringFactoriesLoader加載的。從CLASSPATH下的每個Jar包中搜尋所有META-INF/spring.factories配置檔案,然後将解析properties檔案,找到指定名稱的配置後傳回。需要注意的是,其實這裡不僅僅是會去ClassPath路徑下查找,會掃描所有路徑下的Jar包,隻不過這個檔案隻會在Classpath下的jar包中。(例如:資料庫的自動配置功能)。

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories檔案的格式為:key=value1,value2,value3
// 從所有的jar包中找到META-INF/spring.factories檔案
// 然後從檔案中解析出key=factoryClass類名稱的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    // 取得資源檔案的URL
    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    List<String> result = new ArrayList<String>();
    // 周遊所有的URL
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 根據資源檔案URL解析properties檔案,得到對應的一組@Configuration類
        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
        String factoryClassNames = properties.getProperty(factoryClassName);
        // 組裝資料,并傳回
        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
    }
    return result;
}           

可以看到,它并沒有采用JDK中的SPI機制來加載這些類,不過原理差不多。都是通過一個配置檔案,加載并解析檔案内容,然後通過反射建立執行個體。

假如你希望參與到SpringBoot初始化的過程中,現在我們又多了一種方式。我們也建立一個spring.factories檔案,自定義一個初始化器。

繼續閱讀