java SPI 01-SPI 是什麼?spi 使用入門教程 ServiceLoader 使用簡介

系列目錄

spi 01-spi 是什麼?入門使用html

spi 02-spi 的實戰解決 slf4j 包衝突問題java

spi 03-spi jdk 實現源碼解析git

spi 04-spi dubbo 實現源碼解析github

spi 05-dubbo adaptive extension 自適應拓展sql

spi 06-本身從零手寫實現 SPI 框架apache

spi 07-自動生成 SPI 配置文件實現方式編程

問題引入

之前一直想指定一套標準,讓別人按照這個標準來實現,並編寫好對應的容器。oracle

而後我在代碼中動態獲取這些實現,讓代碼運行起來。框架

困難

如何獲取某個接口的實現?maven

初步方案

和同事討論,是經過掃描包的 class 的方式。而後判斷是否爲定製標準的子類。

  • 缺點

以爲很彆扭,須要限定死實現類的包名稱,並且性能也較差。

SPI 的解決方式

今天在閱讀 hibernate-validator 源碼時受到了啓發。

能夠經過 SPI 的方式,更加天然的解決這個問題。

SPI 是 Service Provider Interfaces 的縮寫。

本文簡單介紹下如何使用,具體原理,暫時不作深究。

SPI 是什麼

SPI 是 Java 提供的一種服務加載方式,全名爲 Service Provider Interface。

根據 Java 的 SPI 規範,咱們能夠定義一個服務接口,具體的實現由對應的實現者去提供,即服務提供者。

而後在使用的時候再根據 SPI 的規範去獲取對應的服務提供者的服務實現。

經過 SPI 服務加載機制進行服務的註冊和發現,能夠有效的避免在代碼中將具體的服務提供者寫死。從而能夠基於接口編程,實現模塊間的解耦。

SPI 機制的約定

  1. 在 META-INF/services/ 目錄中建立以接口全限定名命名的文件,該文件內容爲API具體實現類的全限定名

  2. 使用 ServiceLoader 類動態加載 META-INF 中的實現類

  3. 如 SPI 的實現類爲 Jar 則須要放在主程序 ClassPath 中

  4. API 具體實現類必須有一個不帶參數的構造方法

應用場景舉例

SPI 應用場景舉例

  • JDBC

jdbc4.0之前, 開發人員還須要基於Class.forName("xxx")的方式來裝載驅動,jdbc4也基於spi的機制來發現驅動提供商了,能夠經過METAINF/services/java.sql.Driver文件裏指定實現類的方式來暴露驅動提供者.

  • COMMON-LOGGING

apache最先提供的日誌的門面接口。只有接口,沒有實現。

具體方案由各提供商實現,發現日誌提供商是經過掃描METAINF/services/org.apache.commons.logging.LogFactory配置文件,經過讀取該文件的內容找到日誌提工商實現類。

只要咱們的日誌實現裏包含了這個文件,並在文件裏制定 LogFactory 工廠接口的實現類便可。

簡單實現

文件目錄

.
├── java
│   └── com
│       └── github
│           └── houbb
│               └── forname
│                   ├── Say.java
│                   ├── Sing.java
│                   └── impl
│                       ├── DefaultSay.java
│                       └── DefaultSing.java
└── resources
    └── META-INF
        └── services
            └── com.github.houbb.forname.Say

定義接口和實現

  • Say.java
public interface Say {

    /**
     * 說
     */
    void say();

}
  • DefaultSay.java
import com.github.houbb.forname.Say;

public class DefaultSay implements Say {

    @Override
    public void say() {
        System.out.println("Default say");
    }

}

編寫 services 實現指定

在 resources 目錄下,建立 META-INF/services 文件夾,以接口全路徑名 com.github.houbb.forname.Say 爲文件名稱,內容爲對應的實現類全路徑。

若是是多個,就直接換行隔開。

  • com.github.houbb.forname.Say
com.github.houbb.forname.impl.DefaultSay

測試

  • SayTest.java
public class SayTest {

    @Test
    public void spiTest() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ServiceLoader<Say> loader = ServiceLoader.load(Say.class, classLoader);

        for (Say say : loader) {
            say.say();
        }
    }

}
  • 測試結果
Default say

簡單總結

Java 中,能夠經過 ServiceLoader 類比較方便的找到該類的全部子類實現。

META-INF/services 下的實現指定和實現子類實現徹底能夠和接口定義徹底分開。

麻煩的地方

每次都要手動建立實現指定文件,比較繁瑣。

Auto 就爲解決這個問題而生。

Auto 版本演示

maven 引入

<dependencies>
    <dependency>
        <groupId>com.google.auto.service</groupId>
        <artifactId>auto-service</artifactId>
        <version>1.0-rc4</version>
        <optional>true</optional>
    </dependency>
</dependencies>

接口和定義

  • Sing.java
public interface Sing {

    /**
     * 唱歌
     */
    void sing();

}
  • DefaultSing.java
@AutoService(Sing.class)
public class DefaultSing implements Sing {

    @Override
    public void sing() {
        System.out.println("Sing a song...");
    }

}

測試

  • SingTest.java
public class SingTest {

    @Test
    public void spiTest() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ServiceLoader<Sing> loader = ServiceLoader.load(Sing.class, classLoader);

        for (Sing sing : loader) {
            sing.sing();
        }
    }

}
  • 結果
Sing a song...

簡單總結

經過 google 的 auto,能夠在編譯時自動爲咱們生成對應的接口實現指定文件。

在 target 對應的文件下能夠看到。

實現原理,也相對簡單。經過 java 的編譯時註解,生成對應的文件便可。

實際上諸如 dubbo 等框架,會利用 SPI 機制來提高項目總體的靈活性。

java 自帶的 SPI 有不少不足的地方,本系列就是要學習使用,而且實現本身加強的 SPI 框架。

源碼地址

SPI 源碼

參考資料

Oracle SPI-intro

google auto

詳解 JAVA SPI 機制和使用方法

相關文章
相關標籤/搜索