Java SPI機(jī)制詳解

什么是SPI舟铜?

SPI 全稱為 (Service Provider Interface) 寸齐,是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制弛随。SPI是一種動態(tài)替換發(fā)現(xiàn)的機(jī)制屯曹, 比如有個(gè)接口狱庇,想運(yùn)行時(shí)動態(tài)地給它添加實(shí)現(xiàn),你只需要添加一個(gè)實(shí)現(xiàn)恶耽。我們經(jīng)常遇到的就是java.sql.Driver接口密任,其他不同廠商可以針對同一接口做出不同的實(shí)現(xiàn),mysql和postgresql都有不同的實(shí)現(xiàn)提供給用戶偷俭,而Java的SPI機(jī)制可以為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)浪讳。

Java SPI機(jī)制詳解

類圖中,接口對應(yīng)定義的抽象SPI接口涌萤;實(shí)現(xiàn)方實(shí)現(xiàn)SPI接口淹遵;調(diào)用方依賴SPI接口口猜。

SPI接口的定義在調(diào)用方,在概念上更依賴調(diào)用方透揣;組織上位于調(diào)用方所在的包中济炎;實(shí)現(xiàn)位于獨(dú)立的包中。

當(dāng)接口屬于實(shí)現(xiàn)方的情況辐真,實(shí)現(xiàn)方提供了接口和實(shí)現(xiàn)须尚,這個(gè)用法很常見,屬于API調(diào)用侍咱。我們可以引用接口來達(dá)到調(diào)用某實(shí)現(xiàn)類的功能恨闪。

Java SPI 應(yīng)用實(shí)例

當(dāng)服務(wù)的提供者提供了一種接口的實(shí)現(xiàn)之后,需要在classpath下的META-INF/services/目錄里創(chuàng)建一個(gè)以服務(wù)接口命名的文件放坏,這個(gè)文件里的內(nèi)容就是這個(gè)接口的具體的實(shí)現(xiàn)類咙咽。當(dāng)其他的程序需要這個(gè)服務(wù)的時(shí)候,就可以通過查找這個(gè)jar包(一般都是以jar包做依賴)的META-INF/services/中的配置文件淤年,配置文件中有接口的具體實(shí)現(xiàn)類名钧敞,可以根據(jù)這個(gè)類名進(jìn)行加載實(shí)例化,就可以使用該服務(wù)了麸粮。JDK中查找服務(wù)實(shí)現(xiàn)的工具類是:java.util.ServiceLoader溉苛。

SPI接口

public interface ObjectSerializer {

    byte[] serialize(Object obj) throws ObjectSerializerException;

    <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;

    String getSchemeName();
}

定義了一個(gè)對象序列化接口,內(nèi)有三個(gè)方法:序列化方法弄诲、反序列化方法和序列化名稱愚战。

SPI具體實(shí)現(xiàn)

public class KryoSerializer implements ObjectSerializer {

    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        byte[] bytes;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            //獲取kryo對象
            Kryo kryo = new Kryo();
            Output output = new Output(outputStream);
            kryo.writeObject(output, obj);
            bytes = output.toBytes();
            output.flush();
        } catch (Exception ex) {
            throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {

            }
        }
        return bytes;
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        T object;
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
            Kryo kryo = new Kryo();
            Input input = new Input(inputStream);
            object = kryo.readObject(input, clazz);
            input.close();
        } catch (Exception e) {
            throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
        }
        return object;
    }

    @Override
    public String getSchemeName() {
        return "kryoSerializer";
    }

}

使用Kryo的序列化方式。Kryo 是一個(gè)快速高效的Java對象圖形序列化框架齐遵,它原生支持java寂玲,且在java的序列化上甚至優(yōu)于google著名的序列化框架protobuf。

public class JavaSerializer implements ObjectSerializer {
    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        ByteArrayOutputStream arrayOutputStream;
        try {
            arrayOutputStream = new ByteArrayOutputStream();
            ObjectOutput objectOutput = new ObjectOutputStream(arrayOutputStream);
            objectOutput.writeObject(obj);
            objectOutput.flush();
            objectOutput.close();
        } catch (IOException e) {
            throw new ObjectSerializerException("JAVA serialize error " + e.getMessage());
        }
        return arrayOutputStream.toByteArray();
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(param);
        try {
            ObjectInput input = new ObjectInputStream(arrayInputStream);
            return (T) input.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new ObjectSerializerException("JAVA deSerialize error " + e.getMessage());
        }
    }

    @Override
    public String getSchemeName() {
        return "javaSerializer";
    }

}

Java原生的序列化方式梗摇。

增加META-INF目錄文件

Resource下面創(chuàng)建META-INF/services 目錄里創(chuàng)建一個(gè)以服務(wù)接口命名的文件

Java SPI機(jī)制詳解
com.blueskykong.javaspi.serializer.KryoSerializer
com.blueskykong.javaspi.serializer.JavaSerializer

Service類

@Service
public class SerializerService {

    public ObjectSerializer getObjectSerializer() {
        ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);

        final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
                .findFirst();

        return serializer.orElse(new JavaSerializer());
    }
}

獲取定義的序列化方式拓哟,且只取第一個(gè)(我們在配置中寫了兩個(gè)),如果找不到則返回Java原生序列化方式伶授。

測試類

    @Autowired
    private SerializerService serializerService;

    @Test
    public void serializerTest() throws ObjectSerializerException {
        ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
        System.out.println(objectSerializer.getSchemeName());
        byte[] arrays = objectSerializer.serialize(Arrays.asList("1", "2", "3"));
        ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
        Assert.assertArrayEquals(Arrays.asList("1", "2", "3").toArray(), list.toArray());
    }

測試用例通過断序,且輸出kryoSerializer。

SPI的用途

數(shù)據(jù)庫DriverManager糜烹、Spring违诗、ConfigurableBeanFactory等都用到了SPI機(jī)制,這里以數(shù)據(jù)庫DriverManager為例疮蹦,看一下其實(shí)現(xiàn)的內(nèi)幕诸迟。

DriverManager是jdbc里管理和注冊不同數(shù)據(jù)庫driver的工具類。針對一個(gè)數(shù)據(jù)庫,可能會存在著不同的數(shù)據(jù)庫驅(qū)動實(shí)現(xiàn)亮蒋。我們在使用特定的驅(qū)動實(shí)現(xiàn)時(shí),不希望修改現(xiàn)有的代碼妆毕,而希望通過一個(gè)簡單的配置就可以達(dá)到效果慎玖。 在使用mysql驅(qū)動的時(shí)候,會有一個(gè)疑問笛粘,DriverManager是怎么獲得某確定驅(qū)動類的趁怔?我們在運(yùn)用Class.forName("com.mysql.jdbc.Driver")加載mysql驅(qū)動后,就會執(zhí)行其中的靜態(tài)代碼把driver注冊到DriverManager中薪前,以便后續(xù)的使用润努。

在JDBC4.0之前,連接數(shù)據(jù)庫的時(shí)候示括,通常會用Class.forName("com.mysql.jdbc.Driver")這句先加載數(shù)據(jù)庫相關(guān)的驅(qū)動铺浇,然后再進(jìn)行獲取連接等的操作。而JDBC4.0之后不需要Class.forName來加載驅(qū)動垛膝,直接獲取連接即可鳍侣,這里使用了Java的SPI擴(kuò)展機(jī)制來實(shí)現(xiàn)。

在java中定義了接口java.sql.Driver吼拥,并沒有具體的實(shí)現(xiàn)倚聚,具體的實(shí)現(xiàn)都是由不同廠商來提供的。

mysql


mysql-connector-java-5.1.45.jar中凿可,META-INF/services目錄下會有一個(gè)名字為java.sql.Driver的文件:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

pg

而在postgresql-42.2.2.jar中惑折,META-INF/services目錄下會有一個(gè)名字為java.sql.Driver的文件:

org.postgresql.Driver

用法

String url = "jdbc:mysql://localhost:3306/test";
Connection conn = DriverManager.getConnection(url,username,password);

上面展示的是mysql的用法,pg用法也是類似枯跑。不需要使用Class.forName("com.mysql.jdbc.Driver")來加載驅(qū)動惨驶。

Mysql DriverManager實(shí)現(xiàn)

上面代碼沒有了加載驅(qū)動的代碼,我們怎么去確定使用哪個(gè)數(shù)據(jù)庫連接的驅(qū)動呢敛助?這里就涉及到使用Java的SPI擴(kuò)展機(jī)制來查找相關(guān)驅(qū)動的東西了敞咧,關(guān)于驅(qū)動的查找其實(shí)都在DriverManager中,DriverManager是Java中的實(shí)現(xiàn)辜腺,用來獲取數(shù)據(jù)庫連接休建,在DriverManager中有一個(gè)靜態(tài)代碼塊如下:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

可以看到其內(nèi)部的靜態(tài)代碼塊中有一個(gè)loadInitialDrivers方法,loadInitialDrivers用法用到了上文提到的spi工具類ServiceLoader:

    public Void run() {

        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();

        /* Load these drivers, so that they can be instantiated.
         * It may be the case that the driver class may not be there
         * i.e. there may be a packaged driver with the service class
         * as implementation of java.sql.Driver but the actual class
         * may be missing. In that case a java.util.ServiceConfigurationError
         * will be thrown at runtime by the VM trying to locate
         * and load the service.
         *
         * Adding a try catch block to catch those runtime errors
         * if driver not available in classpath but it's
         * packaged as service and that service is there in classpath.
         */
        try{
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
        } catch(Throwable t) {
        // Do nothing
        }
        return null;
    }

遍歷使用SPI獲取到的具體實(shí)現(xiàn)评疗,實(shí)例化各個(gè)實(shí)現(xiàn)類测砂。在遍歷的時(shí)候,首先調(diào)用driversIterator.hasNext()方法百匆,這里會搜索classpath下以及jar包中所有的META-INF/services目錄下的java.sql.Driver文件砌些,并找到文件中的實(shí)現(xiàn)類的名字,此時(shí)并沒有實(shí)例化具體的實(shí)現(xiàn)類。

總結(jié)

SPI機(jī)制在實(shí)際開發(fā)中使用的場景也有很多存璃。特別是統(tǒng)一標(biāo)準(zhǔn)的不同廠商實(shí)現(xiàn)仑荐,當(dāng)有關(guān)組織或者公司定義標(biāo)準(zhǔn)之后,具體廠商或者框架開發(fā)者實(shí)現(xiàn)纵东,之后提供給開發(fā)者使用粘招。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市偎球,隨后出現(xiàn)的幾起案子洒扎,更是在濱河造成了極大的恐慌,老刑警劉巖衰絮,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袍冷,死亡現(xiàn)場離奇詭異,居然都是意外死亡猫牡,警方通過查閱死者的電腦和手機(jī)胡诗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淌友,“玉大人乃戈,你說我怎么就攤上這事∧督” “怎么了症虑?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長归薛。 經(jīng)常有香客問我谍憔,道長,這世上最難降的妖魔是什么主籍? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任习贫,我火速辦了婚禮,結(jié)果婚禮上千元,老公的妹妹穿的比我還像新娘苫昌。我一直安慰自己,他們只是感情好幸海,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布祟身。 她就那樣靜靜地躺著,像睡著了一般物独。 火紅的嫁衣襯著肌膚如雪袜硫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天挡篓,我揣著相機(jī)與錄音婉陷,去河邊找鬼帚称。 笑死,一個(gè)胖子當(dāng)著我的面吹牛秽澳,可吹牛的內(nèi)容都是我干的闯睹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼担神,長吁一口氣:“原來是場噩夢啊……” “哼楼吃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杏瞻,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤所刀,失蹤者是張志新(化名)和其女友劉穎衙荐,沒想到半個(gè)月后捞挥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忧吟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年砌函,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溜族。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡讹俊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出煌抒,到底是詐尸還是另有隱情仍劈,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布寡壮,位于F島的核電站贩疙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏况既。R本人自食惡果不足惜这溅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棒仍。 院中可真熱鬧悲靴,春花似錦、人聲如沸莫其。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乱陡。三九已至否纬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛋褥,已是汗流浹背临燃。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人膜廊。 一個(gè)月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓乏沸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親爪瓜。 傳聞我的和親對象是個(gè)殘疾皇子蹬跃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一、Java SPI是什么 SPI的英文全稱為Service Provider Interface铆铆,字面意思為服務(wù)...
    GeekerLou閱讀 4,167評論 0 39
  • SPI蝶缀,Service Provider Interface,主要是被框架的開發(fā)人員使用薄货,比如java.sql.D...
    加大裝益達(dá)閱讀 1,690評論 2 12
  • 本文以JDBC為例深入講解 java spi 機(jī)制翁都,將幫助你理解:什么是SPI,SPI實(shí)現(xiàn)原理谅猾,SPI的使用和SP...
    匠丶閱讀 5,233評論 0 8
  • 背景 一位前輩在一次技術(shù)分享中指出我們目前的包管理不規(guī)范柄慰,模塊間職責(zé)有重疊,理解成本高不易維護(hù)税娜,提出在開發(fā)過程中應(yīng)...
    XHLeee閱讀 308評論 0 0
  • SPI是什么 SPI的英文名稱是Service Provider Interface坐搔,是Java 內(nèi)置的服務(wù)發(fā)現(xiàn)機(jī)...
    GallenZhang閱讀 342評論 0 0