什么是SPI機制
最近我建了另一個文章分類,用於擴展JDK
中一些重要但不常用的功能。
SPI
,全名Service Provider Interface
,是一種服務發現機制。它可以看成是一種針對接口實現類的解耦方案。我們只需要采用配置文件方式配置好接口的實現類,就可以利用SPI
機制去加載到它們了,當我們需要修改實現類時,改改配置文件就可以了,而不需要去改代碼。
當然,有的同學可能會問,spring
也可以做接口實現類的解耦,是不是SPI
就沒用了呢?雖然兩者都可以達到相同的目的,但是不一定所有應用都可以引入spring
框架,例如JDBC
自動發現驅動並注冊,它就是采用SPI
機制,它就不大可能引入spring
來解耦接口實現類。另外,druid
、dubbo
等都采用了SPI
機制。
怎么使用SPI
需求
利用SPI
機制加載用戶服務接口的實現類並測試。
工程環境
JDK
:1.8.0_201
maven
:3.6.1
IDE
:eclipse 4.12
主要步驟
- 編寫用戶服務類接口和實現類;
- 在
classpath
路徑下的META-INF/services
文件夾下配置好接口的實現類; - 利用
SPI
機制加載接口實現類並測試。
創建項目
項目類型Maven Project,打包方式jar
引入依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
編寫用戶服務類接口
路徑:cn.zzs.spi
public interface UserService {
void save();
}
編寫接口實現類
路徑:cn.zzs.spi
。這里就簡單實現就好了。
public class UserServiceImpl1 implements UserService {
@Override
public void save() {
System.err.println("執行服務1的save方法");
}
}
// ------------------------
public class UserServiceImpl2 implements UserService {
@Override
public void save() {
System.err.println("執行服務2的save方法");
}
}
配置接口文件
在resources
路徑下創建META-INF/services
文件夾,並以UserService
的全限定類名為文件名,創建一個文件。如圖所示。
文件中寫入接口實現類的全限定類名,多個用換行符隔開。
cn.zzs.spi.UserServiceImpl1
cn.zzs.spi.UserServiceImpl2
編寫測試方法
路徑:test下的cn.zzs.spi
。如果實際項目中配置了比較多的接口文件,可以考慮抽取工具類。
public class UserServiceTest {
@Test
public void test() {
// 1. 創建一個ServiceLoader對象
ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
// 2. 創建一個迭代器
Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
// 3. 加載配置文件並實例化接口實現類
while(userServiceIterator.hasNext()) {
UserService userService = userServiceIterator.next();
userService.save();
System.out.println("==================");
}
}
}
測試結果
執行服務1的save方法
==================
執行服務2的save方法
==================
SPI在JDBC中的應用
本文以mysql
8.0.15版本的驅動來說明。首先,當我們調用Class.forName("com.mysql.cj.jdbc.Driver")
時,會去執行這個類的靜態代碼塊,在靜態代碼塊中就會完成驅動注冊。
static {
try {
//靜態代碼塊中注冊當前驅動
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
JDK
6后不再需要Class.forName(driver)
也能注冊驅動。因為從JDK6
開始,DriverManager
增加了以下靜態代碼塊,當類被加載時會執行static代碼塊的loadInitialDrivers
方法。
而這個方法會通過查詢系統參數(jdbc.drivers
)和SPI
機制兩種方式去加載數據庫驅動。
注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。
static {
loadInitialDrivers();
}
//這個方法通過兩個渠道加載所有數據庫驅動:
//1. 查詢系統參數jdbc.drivers獲得數據驅動類名
//2. SPI機制
private static void loadInitialDrivers() {
//通過系統參數jdbc.drivers讀取數據庫驅動的全路徑名。該參數可以通過啟動參數來設置,其實引入SPI機制后這一步好像沒什么意義了。
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//使用SPI機制加載驅動
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//讀取META-INF/services/java.sql.Driver文件的類全路徑名。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//加載並初始化類
try{
while(driversIterator.hasNext()) {
// 這里才會去實例化驅動
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
if (drivers == null || drivers.equals("")) {
return;
}
//加載jdbc.drivers參數配置的實現類
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
在mysql
的驅動包中,我們可以看到SPI
的配置文件。
源碼分析
本文將根據測試例子中方法的調用順序來分析。
@Test
public void test() {
// 1. 創建一個ServiceLoader對象
ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
// 2. 創建一個迭代器
Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
// 3. 加載配置文件並實例化接口實現類
while(userServiceIterator.hasNext()) {
UserService userService = userServiceIterator.next();
userService.save();
System.out.println("==================");
}
}
注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。
創建一個ServiceLoader
我們從load(Class service)
方法開始分析,可以看到,調用這個方法時還不會去加載配置文件和初始化接口實現類。因為SPI
采用延遲加載的方式,只有去調用hasNext()
才會去加載配置文件,調用next()
才會去實例化對象。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲得當前線程上下文的類加載器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 創建一個ServiceLoader對象
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 校驗接口類型和類加載器是否為空
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 初始化訪問控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// 存放接口實現類對象。形式為全限定類名=實例對象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器,有加載和實例化接口實現類的方法
private LazyIterator lookupIterator;
public void reload() {
// 清空存放的接口實現類對象
providers.clear();
// 創建一個LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
// LazyIterator是ServiceLoader的內部類
private class LazyIterator implements Iterator<S> {
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
}
創建一個迭代器
因為SPI
機制采用了延遲加載的方式,所以在沒有調用next()
之前,providers
會是一個空的Map
,也就是說以下的knownProviders
也會是一個空的迭代器,所以,這個時候都必須去調用lookupIterator
的方法,本文討論的正是這種情況。
public Iterator<S> iterator() {
return new Iterator<S>() {
// providers的迭代器,一般為空
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
加載配置文件
前面已經提到,當調用hasNext()
時才會去加載配置文件。那么,我們直接看LazyIterator
的hasNext()
方法
// 接口類型
Class<S> service;
// 類加載器
ClassLoader loader;
// 配置文件列表,一般只有一個
Enumeration<URL> configs = null;
// 所有實現類全限定類名的迭代器
Iterator<String> pending = null;
// 下一個實現類全限定類名
String nextName = null;
public boolean hasNext() {
return hasNextService();
}
private boolean hasNextService() {
// 判斷是否有下一個實現類全限定類名,有的話直接返回true
// 第一次調用這個方法nextName肯定是null的
if(nextName != null) {
return true;
}
// 下面就是加載配置文件了
if(configs == null) {
// 本文例子中:fullName = META-INF/services/cn.zzs.spi.UserService
String fullName = PREFIX + service.getName();
if(loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
// pending是所有實現類全限定類名的迭代器,此時是空
while((pending == null) || !pending.hasNext()) {
// 如果文件中沒有配置實現類,直接返回false
if(!configs.hasMoreElements()) {
return false;
}
// 解析配置文件,並初始化pending迭代器
pending = parse(service, configs.nextElement());
}
// 將第一個實現類的全限定類名賦值給nextName
nextName = pending.next();
return true;
}
解析的過程就是簡單的IO操作,這里就不再擴展了。
實例化接口實現類
前面已經提到,當調用next()
時才會去實例化接口實現類。那么,我們直接看LazyIterator
的next()
方法。
public S next() {
return nextService();
}
private S nextService() {
// 判斷是否有下一個接口實現類。因為前面已經有nextName,所以直接返回true
if (!hasNextService())
throw new NoSuchElementException();
// 獲得下一個接口實現類的全限定類名
String cn = nextName;
// 將nextName置空,這樣下次調用hasNext()就會重新賦值nextName
nextName = null;
Class<?> c = null;
// 加載接口實現類
c = Class.forName(cn, false, loader);
// 判斷是否是指定接口的實現類
if (!service.isAssignableFrom(c)) {
fail(service,"Provider " + cn + " not a subtype");
}
// 轉化為指定類型
S p = service.cast(c.newInstance());
// 放入providers的Map中
// 前面提到過,只有調用了next()方法,這個Map才會放入元素
providers.put(cn, p);
return p;
}
以上,SPI
的源碼基本分析完。
參考資料
本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html