天天看點

SPI架構實作之旅一:背景介紹

SPI架構實作之旅一:背景介紹

SPI的全名為Service Provider Interface,簡單的總結下java spi機制的思想。我們系統裡抽象的各個子產品,往往有很多不同的實作方案,比如日志子產品的方案,xml解析子產品、jdbc子產品的方案等。面向的對象的設計裡,我們一般推薦子產品之間基于接口程式設計,子產品之間不對實作類進行寫死。一旦代碼裡涉及具體的實作類,就違反了可拔插的原則,如果需要替換一種實作,就需要修改代碼。為了實作在子產品裝配的時候能不在程式裡動态指明,這就需要一種服務發現機制。 java spi就是提供這樣的一個機制:為某個接口尋找服務實作的機制

1. 背景

上面摘抄了一下spi的概念,接着以個人的了解,簡單的談一下為什麼會用到SPI, 什麼場景下可以用到這個, 以及使用了SPI機制後有什麼優越性

什麼是SPI

雖然最開始就引用了spi的解釋,這裡淺談一下個人了解。​

​Service Provider Interface​

​ 以接口方式提供服務, 和API不同,spi的機制是定義一套标準規範的接口,實作交給其他人來做。

是以一個接口,可以有很多的實作,你完全可以根據自己的需要去選擇具體的實作方式,因為是面向接口的開發,是以你的業務代碼基本上就不用修改,就可以切到另一個實作了

什麼場景可以用

分别從架構層面和業務層面,給出一個我認為比較合适的場景

1. 日志輸出 ​

​SLF4j​

SLF4j:大名鼎鼎的日志輸出接口,這個jar包裡面提供的都隻是接口方式,具體的實作需要自己去實作,當然比較常用的 ​

​logback​

​​ 就是一個具體的實作包了, 在項目中使用 ​

​slf4j​

​​ 的api進行日志的輸出, 通過簡單的配置,引入logback, 就可以使用logback來實作具體的日志輸出; 也可以換一個日志實作 ​

​commons-logging​

​,業務上不需要任何的改動,就可以用不同的實作來輸出日志

2. 業務場景

假設你現在有個使用者注冊成功後的歡迎使用者的業務,不同管道(微信,qq,微網誌等)注冊的,顯示的歡迎不同,對此有兩種不同的實作方式

  • 如果每個不同的管道進來的,都有一個獨立的應用來響應 (因為絕大多數的業務都一樣,可能就歡迎詞不同,如果做到代碼最大程度的複用)
  • 隻有一個應用,來處理所有的這些場景

可以怎麼用

結合上面的業務場景,來描述下可以怎麼用

1. 代碼複用

為了實作代碼最大程度的複用,那麼可以将不同的地方,抽象成一個SPI接口,在業務層通過接口來代替具體的實作類實作業務邏輯;

每個管道,都有個獨立的應用,那麼在微信管道,建立一個 WeixinSpiImpl來實作接口

在qq管道,實作 QQSpiImpl;那麼在具體的接口調用處,實際上就是執行的spi實作類方法

2. 業務場景的選擇區分

這個與上面不同,同一個服務接口,根據不同的業務場景,選擇不同的實作來執行;當然你是完全可以使用 if, else來實作這種場景,唯一的問題就是擴充比較麻煩;

這種場景下,我們希望的就是這個接口,能自動的根據業務場景,來選擇最合适的實作類來執行

簡單來講,就是 spi接口執行之前,其實需要有一個自動選擇比對的實作類的前置過程;

通常這種業務場景下,具體的spi實作會有多個,但是需要有一個選擇的政策

2. 小目标

在具體的實作之前,先定義一個小目标,我們想要實作一個什麼樣子的東西出來

通過上面的背景描述,我們的小目标也就很明确了,我們的實作至少需要滿足兩個場景

  1. 靜态選擇SPI實作, 即在選擇完成之後,所有對這個spi接口的引用都是确定由這個實作來承包
  2. 動态選擇SPI實作, 不到運作之時,你都不知道會是哪個spi實作來幹這件事

3. 技術儲備

java本身就提供了一套spi的支援方式: ​

​ServiceLoader​

​,我們後續的開發,也會在這個基礎之上進行

利用java的 ​

​ServiceLoader​

​ 找到服務接口的實作類,有一些約定,下面給出要求說明和一個測試case

一般實作流程

  • 定義spi接口 : ​

    ​IXxx​

  • 具體的實作類: ​

    ​AXxx​

    ​, ​

    ​BXxx​

  • 在jar包的​

    ​META-INF/services/​

    ​目錄下建立一個檔案,命名為 spi接口的完整類名,内容為spi接口實作的完整類名,一個實作類占一行

測試case如下

spi接口 ​

​com.hust.hui.quicksilver.commons.spi.HelloInterface​

package com.hust.hui.quicksilver.commons.spi;/**
 * Created by yihui on 2017/3/17.
 */public interface HelloInterface {    void sayHello();

}
package com.hust.hui.quicksilver.commons.spi;/**
 * Created by yihui on 2017/3/17.
 */public interface HelloInterface {    void sayHello();

}      

spi接口的兩個實作類

​com.hust.hui.quicksilver.commons.spi.impl.ImageHello.java​

package com.hust.hui.quicksilver.commons.spi.impl;import com.hust.hui.quicksilver.commons.spi.HelloInterface;/**
 * Created by yihui on 2017/3/17.
 */public class ImageHello implements HelloInterface {    @Override
    public void sayHello() {
        System.out.println("image hello!");
    }
}
package com.hust.hui.quicksilver.commons.spi.impl;import com.hust.hui.quicksilver.commons.spi.HelloInterface;/**
 * Created by yihui on 2017/3/17.
 */public class ImageHello implements HelloInterface {    @Override
    public void sayHello() {
        System.out.println("image hello!");
    }
}      

​com.hust.hui.quicksilver.commons.spi.impl.TextHello.java​

package com.hust.hui.quicksilver.commons.spi.impl;import com.hust.hui.quicksilver.commons.spi.HelloInterface;/**
 * Created by yihui on 2017/3/17.
 */public class TextHello implements HelloInterface {    @Override
    public void sayHello() {
        System.out.println("text hello");
    }
}
package com.hust.hui.quicksilver.commons.spi.impl;import com.hust.hui.quicksilver.commons.spi.HelloInterface;/**
 * Created by yihui on 2017/3/17.
 */public class TextHello implements HelloInterface {    @Override
    public void sayHello() {
        System.out.println("text hello");
    }
}      

配置檔案 ​

​com.hust.hui.quicksilver.commons.spi.HelloInterface​

com.hust.hui.quicksilver.commons.spi.impl.ImageHello
com.hust.hui.quicksilver.commons.spi.impl.TextHello
com.hust.hui.quicksilver.commons.spi.impl.ImageHello
com.hust.hui.quicksilver.commons.spi.impl.TextHello      

測試類

public class HelloSpiTest {

    @Test
    public void testSPI() {
        ServiceLoader<HelloInterface> serviceLoader = ServiceLoader.load(HelloInterface.class);        for (HelloInterface hello: serviceLoader) {
            hello.sayHello();
        }
    }
}
public class HelloSpiTest {

    @Test
    public void testSPI() {
        ServiceLoader<HelloInterface> serviceLoader = ServiceLoader.load(HelloInterface.class);        for (HelloInterface hello: serviceLoader) {
            hello.sayHello();
        }
    }
}      

輸出如下:

image hello!
text hello
image hello!
text hello      

測試類示範如下圖:

SPI架構實作之旅一:背景介紹

4. 設計思路

畫了一下結構圖,友善了解, 下面的核心是 ​

​SpiLoader​

​ 類, 負責加載spi接口的所有實作類, 初始化所有定義的選擇器, 傳回一個spi接口的實作類初始化使用者自定義的spi對象,然後使用者持有此對象調用spi接口中提供的方法即可

其他

  • SPI架構實作之旅一:背景介紹
  • SPI架構實作之旅二:整體設計
  • SPI架構實作之旅三:實作說明
  • SPI架構實作之旅四:使用測試