SPI ,全稱爲 Service Provider Interface,是一種服務發現機制。它經過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件裏所定義的類。java
這一機制爲不少框架擴展提供了可能,好比在Dubbo、JDBC中都使用到了SPI機制。咱們先經過一個很簡單的例子來看下它是怎麼用的。mysql
首先,咱們須要定義一個接口,SPIServicesql
package com.viewscenes.netsupervisor.spi;
public interface SPIService {
void execute();
}
複製代碼
而後,定義兩個實現類,沒別的意思,只輸入一句話。數據庫
package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{
public void execute() {
System.out.println("SpiImpl1.execute()");
}
}
----------------------我是乖巧的分割線----------------------
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{
public void execute() {
System.out.println("SpiImpl2.execute()");
}
}
複製代碼
最後呢,要在ClassPath路徑下配置添加一個文件。文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。 文件路徑以下: bash
內容就是實現類的全限定類名:com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
複製代碼
而後咱們就能夠經過ServiceLoader.load或者Service.providers
方法拿到實現類的實例。其中,Service.providers
包位於sun.misc.Service
,而ServiceLoader.load
包位於java.util.ServiceLoader
。框架
public class Test {
public static void main(String[] args) {
Iterator<SPIService> providers = Service.providers(SPIService.class);
ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);
while(providers.hasNext()) {
SPIService ser = providers.next();
ser.execute();
}
System.out.println("--------------------------------");
Iterator<SPIService> iterator = load.iterator();
while(iterator.hasNext()) {
SPIService ser = iterator.next();
ser.execute();
}
}
}
複製代碼
兩種方式的輸出結果是一致的:ide
SpiImpl1.execute()
SpiImpl2.execute()
--------------------------------
SpiImpl1.execute()
SpiImpl2.execute()
複製代碼
咱們看到一個位於sun.misc包
,一個位於java.util包
,sun包下的源碼看不到。咱們就以ServiceLoader.load爲例,經過源碼看看它裏面到底怎麼作的。源碼分析
首先,咱們先來了解下ServiceLoader,看看它的類結構。測試
public final class ServiceLoader<S> implements Iterable<S>
//配置文件的路徑
private static final String PREFIX = "META-INF/services/";
//加載的服務類或接口
private final Class<S> service;
//已加載的服務類集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//類加載器
private final ClassLoader loader;
//內部類,真正加載服務類
private LazyIterator lookupIterator;
}
複製代碼
load方法建立了一些屬性,重要的是實例化了內部類,LazyIterator。最後返回ServiceLoader的實例。ui
public final class ServiceLoader<S> implements Iterable<S>
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;
//先清空
providers.clear();
//實例化內部類
LazyIterator lookupIterator = new LazyIterator(service, loader);
}
}
複製代碼
查找實現類和建立實現類的過程,都在LazyIterator完成。當咱們調用iterator.hasNext和iterator.next方法的時候,實際上調用的都是LazyIterator的相應方法。
public Iterator<S> iterator() {
return new Iterator<S>() {
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
.......
};
}
複製代碼
因此,咱們重點關注lookupIterator.hasNext()方法,它最終會調用到hasNextService。
private class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
//第二次調用的時候,已經解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定類名,就是文件服務類的文件
//META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
String fullName = PREFIX + service.getName();
//將文件路徑轉成URL對象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件對象,讀取內容,最後返回
pending = parse(service, configs.nextElement());
}
//拿到第一個實現類的類名
nextName = pending.next();
return true;
}
}
複製代碼
固然,調用next方法的時候,實際調用到的是,lookupIterator.nextService。它經過反射的方式,建立實現類的實例並返回。
private class LazyIterator implements Iterator<S>{
private S nextService() {
//全限定類名
String cn = nextName;
nextName = null;
//建立類的Class對象
Class<?> c = Class.forName(cn, false, loader);
//經過newInstance實例化
S p = service.cast(c.newInstance());
//放入集合,返回實例
providers.put(cn, p);
return p;
}
}
複製代碼
看到這兒,我想已經很清楚了。獲取到類的實例,咱們天然就能夠對它隨心所欲了!
咱們開頭說,SPI機制爲不少框架的擴展提供了可能,其實JDBC就應用到了這一機制。回憶一下JDBC獲取數據庫鏈接的過程。在早期版本中,須要先設置數據庫驅動的鏈接,再經過DriverManager.getConnection獲取一個Connection。
String url = "jdbc:mysql:///consult?serverTimezone=UTC";
String user = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);
複製代碼
在較新版本中(具體哪一個版本,筆者沒有驗證),設置數據庫驅動鏈接,這一步驟就再也不須要,那麼它是怎麼分辨是哪一種數據庫的呢?答案就在SPI。
咱們把目光回到DriverManager
類,它在靜態代碼塊裏面作了一件比較重要的事。很明顯,它已經經過SPI機制, 把數據庫驅動鏈接初始化了。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
複製代碼
具體過程還得看loadInitialDrivers,它在裏面查找的是Driver接口的服務類,因此它的文件路徑就是:META-INF/services/java.sql.Driver。
public class DriverManager {
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//很明顯,它要加載Driver接口的服務類,Driver接口的包爲:java.sql.Driver
//因此它要找的就是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;
}
});
}
}
複製代碼
那麼,這個文件哪裏有呢?咱們來看MySQL的jar包,就是這個文件,文件內容爲:com.mysql.cj.jdbc.Driver
。
上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當調用next方法時,就會建立這個類的實例。它就完成了一件事,向DriverManager註冊自身的實例。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
//註冊
//調用DriverManager類的註冊方法
//往registeredDrivers集合中加入實例
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
複製代碼
在DriverManager.getConnection()方法就是建立鏈接的地方,它經過循環已註冊的數據庫驅動程序,調用其connect方法,獲取鏈接並返回。
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//registeredDrivers中就包含com.mysql.cj.jdbc.Driver實例
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
//調用connect方法建立鏈接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
}catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
}
複製代碼
既然咱們知道JDBC是這樣建立數據庫鏈接的,咱們能不能再擴展一下呢?若是咱們本身也建立一個java.sql.Driver文件,自定義實現類MyDriver,那麼,在獲取鏈接的先後就能夠動態修改一些信息。
仍是先在項目ClassPath下建立文件,文件內容爲自定義驅動類com.viewscenes.netsupervisor.spi.MyDriver
咱們的MyDriver實現類,繼承自MySQL中的NonRegisteringDriver,還要實現java.sql.Driver接口。這樣,在調用connect方法的時候,就會調用到此類,但實際建立的過程還靠MySQL完成。
package com.viewscenes.netsupervisor.spi
public class MyDriver extends NonRegisteringDriver implements Driver{
static {
try {
java.sql.DriverManager.registerDriver(new MyDriver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public MyDriver()throws SQLException {}
public Connection connect(String url, Properties info) throws SQLException {
System.out.println("準備建立數據庫鏈接.url:"+url);
System.out.println("JDBC配置信息:"+info);
info.setProperty("user", "root");
Connection connection = super.connect(url, info);
System.out.println("數據庫鏈接建立完成!"+connection.toString());
return connection;
}
}
--------------------輸出結果---------------------
準備建立數據庫鏈接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
數據庫鏈接建立完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f
複製代碼