Java 中經(jīng)常被提到的 SPI 到底是什么?
Java? 程序員在日常工作中經(jīng)常會聽到 SPI?,而且很多框架都使用了 SPI? 的技術,那么問題來了,到底什么是 SPI 呢?今天阿粉就帶大家好好了解一下 SPI。
SPI 概念
SPI? 全稱是 Service Provider Interface?,是一種 JDK? 內(nèi)置的動態(tài)加載實現(xiàn)擴展點的機制,通過 SPI 技術我們可以動態(tài)獲取接口的實現(xiàn)類,不用自己來創(chuàng)建。
這里提到了接口和實現(xiàn)類,那么 SPI 技術上具體有哪些技術細節(jié)呢?
- 接口:需要有一個功能接口;
- 實現(xiàn)類:接口只是規(guī)范,具體的執(zhí)行需要有實現(xiàn)類才行,所以不可缺少的需要有實現(xiàn)類;
- 配置文件:要實現(xiàn)SPI? 機制,必須有一個與接口同名的文件存放于類路徑下面的 META-INF/services 文件夾中,并且文件中的每一行的內(nèi)容都是一個實現(xiàn)類的全路徑;
- 類加載器ServiceLoader:JDK 內(nèi)置的一個類加載器,用于加載配置文件中的實現(xiàn)類;
舉個栗子
上面說了 SPI 的幾個概念,接下來阿粉就通過一個栗子來帶大家感受一下具體的用法。
第一步
創(chuàng)建一個接口,這里我們創(chuàng)建一個解壓縮的接口,其中定義了壓縮和解壓的兩個方法。
package com.example.demo.spi;
public interface Compresser {
byte[] compress(byte[] bytes);
byte[] decompress(byte[] bytes);
}
第二步
再寫兩個對應的實現(xiàn)類,分別是 GzipCompresser.java? 和 WinRarCompresser.java 代碼如下
package com.example.demo.spi.impl;
import com.example.demo.spi.Compresser;
import java.nio.charset.StandardCharsets;
public class GzipCompresser implements Compresser {
@Override
public byte[] compress(byte[] bytes) {
return"compress by Gzip".getBytes(StandardCharsets.UTF_8);
}
@Override
public byte[] decompress(byte[] bytes) {
return "decompress by Gzip".getBytes(StandardCharsets.UTF_8);
}
}
package com.example.demo.spi.impl;
import com.example.demo.spi.Compresser;
import java.nio.charset.StandardCharsets;
public class WinRarCompresser implements Compresser {
@Override
public byte[] compress(byte[] bytes) {
return "compress by WinRar".getBytes(StandardCharsets.UTF_8);
}
@Override
public byte[] decompress(byte[] bytes) {
return "decompress by WinRar".getBytes(StandardCharsets.UTF_8);
}
}
第三步
創(chuàng)建配置文件,我們接著在 resources? 目錄下創(chuàng)建一個名為 META-INF/services? 的文件夾,在其中創(chuàng)建一個名為 com.example.demo.spi.Compresser 的文件,其中的內(nèi)容如下:
com.example.demo.spi.impl.WinRarCompresser
com.example.demo.spi.impl.GzipCompresser
注意該文件的名稱必須是接口的全路徑,文件里面的內(nèi)容每一行都是一個實現(xiàn)類的全路徑,多個實現(xiàn)類就寫在多行里面,效果如下。
第四步
有了上面的接口,實現(xiàn)類和配置文件,接下來我們就可以使用 ServiceLoader? 動態(tài)加載實現(xiàn)類,來實現(xiàn) SPI 技術了,如下所示:
package com.example.demo;
import com.example.demo.spi.Compresser;
import java.nio.charset.StandardCharsets;
import java.util.ServiceLoader;
public class TestSPI {
public static void main(String[] args) {
ServiceLoader<Compresser> compressers = ServiceLoader.load(Compresser.class);
for (Compresser compresser : compressers) {
System.out.println(compresser.getClass());
}
}
}
運行的結果如下
可以看到我們正常的獲取到了接口的實現(xiàn)類,并且可以直接使用實現(xiàn)類的解壓縮方法。
原理
知道了如何使用 SPI? 接下來我們來研究一下是如何實現(xiàn)的,通過上面的測試我們可以看到,核心的邏輯是 ServiceLoader.load()? 方法,這個方法有點類似于 Spring 中的根據(jù)接口獲取所有實現(xiàn)類一樣。
點開 ServiceLoader? 我們可以看到有一個常量 PREFIX?,如下所示,這也是為什么我們必須在這個路徑下面創(chuàng)建配置文件,因為 JDK 代碼里面會從這個路徑里面去讀取我們的文件。
同時又因為在讀取文件的時候使用了 class? 的路徑名稱,因為我們使用 load? 方法的時候只會傳遞一個 class,所以我們的文件名也必須是接口的全路徑。
通過 load? 方法我們可以看到底層構造了一個 java.util.ServiceLoader.LazyIterator 迭代器。
在迭代器中的 parse? 方法中,就獲取了配置文件中的實現(xiàn)類名稱集合,然后在通過反射創(chuàng)建出具體的實現(xiàn)類對象存放到 LinkedHashMap<String,S> providers = new LinkedHashMap<>(); 中。
常用的框架
SPI 技術的使用非常廣泛,比如在 Dubble?,不過 Dubble? 中的 SPI? 有經(jīng)過改造的,還有我們很常見的數(shù)據(jù)庫的驅(qū)動中也使用了 SPI?,感興趣的小伙伴可以去翻翻看,還有 SLF4J? 用來加載不同提供商的日志實現(xiàn)類以及 Spring 框架等。
優(yōu)缺點
前面介紹了 SPI? 的原理和使用,那 SPI 有什么優(yōu)缺點呢?
優(yōu)點
優(yōu)點當然是解耦,服務方只要定義好接口規(guī)范就好了,具體的實現(xiàn)可以由不同的 Jar 進行實現(xiàn),只要按照規(guī)范實現(xiàn)功能就可以被直接拿來使用,在某些場合會被進行熱插拔使用,實現(xiàn)了解耦的功能。
缺點
一個很明顯的缺點那就是做不到按需加載,通過源碼我們看到了是會將所有的實現(xiàn)類都進行創(chuàng)建的,這種做法會降低性能,如果某些實現(xiàn)類實現(xiàn)很耗時了話將影響加載時間。同時實現(xiàn)類的命名也沒有規(guī)范,讓使用者不方便引用。