SPI機制簡介
????????SPI(Service Provider Interface),是從JDK6開始引入的,一種基于ClassLoader來發現并加載服務的機制。
? ? ? ? 一個標準的SPI,由3個組件構成,分別是:
- Service:是一個公開的接口或者抽象類,定義了一個抽象的功能模塊;
- Service Provider:是Service接口的實現子類;
- ServiceLoader:是SPI機制的核心組件,負責在運行時發現并加載Service Provider。
SPI運行流程
? ? ? ? SPI運行流程如下圖所示,
ServiceLoader類
? ? ? ? ServiceLoader是SPI機制的核心組件,負責在運行時發現并加載Service Provider。該類提供了load方法,用于在程序運行過程中去加載第三方提供的Service接口實現類,得到接口實例;后續過程中,只需要通過接口實例去執行對應的操作即可。
? ? ? ? 假設,我們有這樣一個InternetService?接口,用來提供網絡連接服務。
/*** 網絡連接服務接口SPI-Service*/
public interface InternetService {void connectInternet();
}
? ? ? ? 然后為其提供接口實現子類,
package cn.mobile;import spi.InternetService;public class BeijingChinaMobileMobile implements InternetService {@Overridepublic void connectInternet() {System.out.println("connect internet By [Beijing China Mobile]");}
}
? ? ? ?這樣寫在單體項目中自然是可以的,但是,如果我們要讓別人也能在項目中使用這個接口提供的網絡連接服務,就有點難受了。好在SPI機制就是用來做服務發現和加載工作的,我們可以將其改造成符合SPI標準的一套通用工具。
service服務定義
? ? ? ? 接口就是在定義標準,而這個標準需要交由第三方進行實現。
1. 創建maven項目,
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>simple_spi_example</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>simple-api</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties></project>
?2.定義接口標準,
package spi;/*** 網絡連接服務接口SPI-Service*/
public interface InternetService {void connectInternet();
}
service provider服務的第三方實現
? ? ? ? service provider是Service接口的實現子類。以下我們提供兩個第三方實現,分別命名為A、B。
第三方A實現
1. 創建Maven項目,引入接口標準依賴,
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>simple_spi_example</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>simple-spi-mobile</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><artifactId>simple-api</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></dependency></dependencies></project>
2. 實現SPI接口,
package cn.mobile;import spi.InternetService;public class ChinaMobile implements InternetService {@Overridepublic void connectInternet() {System.out.println("connect internet By [China Mobile]");}
}
package cn.mobile;import spi.InternetService;public class BeijingChinaMobileMobile implements InternetService {@Overridepublic void connectInternet() {System.out.println("connect internet By [Beijing China Mobile]");}
}
3.提供service元數據,
? ? ? ? 在resources目錄下,新建資源文件META-INF\services\spi.InternetService,文件名:spi.InternetService(父接口的全路徑名稱),
cn.mobile.ChinaMobile
cn.mobile.BeijingChinaMobileMobile
第三方B實現
1. 創建Maven項目,引入接口標準依賴,
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>simple_spi_example</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>simple-spi-unicom</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><artifactId>simple-api</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></dependency></dependencies>
</project>
2. 實現SPI接口,
package cn.unicom;import spi.InternetService;public class ChinaUniCom implements InternetService {@Overridepublic void connectInternet() {System.out.println("connect internet By [ChinaUniCom]");}
}
3.提供service元數據,
? ? ? ? 在resources目錄下,新建資源文件META-INF\services\spi.InternetService,文件名:spi.InternetService(父接口的全路徑名稱),
cn.unicom.ChinaUniCom
ServiceLoader服務發現和服務加載
?1. 在主項目中引入service服務的第三方實現相關依賴,
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>simple_spi_example</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>smaple-company</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><artifactId>simple-api</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.example</groupId><artifactId>simple-spi-unicom</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.example</groupId><artifactId>simple-spi-mobile</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies></project>
2. 通過ServiceLoader發現并加載服務
package com.company;import spi.InternetService;import java.util.ServiceLoader;public class Application {public static void main(String[] args) {ServiceLoader<InternetService> load = ServiceLoader.load(InternetService.class);for (InternetService provider : load) {provider.connectInternet();}}
}
3.執行結果如下
ServiceLoader源碼分析
?
? ? ? ? 分析,
// ServiceLoader實現了Iterable接口,可以遍歷所有的服務實現者
public final class ServiceLoader<S> implements Iterable<S>
{// 查找配置文件的目錄private static final String PREFIX = "META-INF/services/";// 表示要被加載的服務的類或接口private final Class<S> service;// 這個ClassLoader用來定位,加載,實例化服務提供者private final ClassLoader loader;// 訪問控制上下文private final AccessControlContext acc;// 緩存已經被實例化的服務提供者,按照實例化的順序存儲private LinkedHashMap<String,S> providers = new LinkedHashMap<>();// 迭代器private LazyIterator lookupIterator;
}
// 服務提供者查找的迭代器
public Iterator<S> iterator() {return new Iterator<S>() {Iterator<Map.Entry<String,S>> knownProviders= providers.entrySet().iterator();// hasNext方法public boolean hasNext() {if (knownProviders.hasNext())return true;return lookupIterator.hasNext();}// next方法public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next();}};
}
// 服務提供者查找的迭代器
private class LazyIterator implements Iterator<S> {// 服務提供者接口Class<S> service;// 類加載器ClassLoader loader;// 保存實現類的urlEnumeration<URL> configs = null;// 保存實現類的全名Iterator<String> pending = null;// 迭代器中下一個實現類的全名String nextName = null;public boolean hasNext() {if (nextName != null) {return true;}if (configs == null) {try {String fullName = PREFIX + service.getName();if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;}public S next() {if (!hasNext()) {throw new NoSuchElementException();}String cn = nextName;nextName = null;Class<?> c = null;try {c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service, "Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service, "Provider " + cn + " could not be instantiated: " + x, x);}throw new Error(); // This cannot happen}
}
SPI應用場景舉例
在JDBC4.0之前,連接數據庫的時候,通常會用
Class.forName("com.mysql.jdbc.Driver")
這句先加載數據庫相關的驅動,然后再進行獲取連接等的操作。而JDBC4.0之后不需要Class.forName
來加載驅動,直接獲取連接即可,這里使用了Java的SPI擴展機制來實現。
? ? ? ? 注冊數據庫連接驅動就是一個典型的例子,以PostGreSQL數據庫連接驅動為例,我們知道:java中定義了接口java.sql.Driver,但是并沒有提供具體的實現,具體的實現都是由不同廠商來提供的,所以我們實際開發時,需要先去找到對應的數據庫連接驅動,把驅動加載到應用中,然后才能去執行數據庫的種種操作。
? ? ? ? 查看postgresql依賴jar包,會發現在META-INFO下的services路徑下,也提供了java.sql.Driver驅動類的實現子類信息,
? ? ? ? 文件內容如下,
org.postgresql.Driver
? ? ? ? ?這樣,就可以基于SPI機制,動態加載第三方提供的Driver數據庫連接驅動,實現數據庫相關的操作。