什么是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)浪讳。
類圖中,接口對應(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ù)接口命名的文件
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ā)者使用粘招。