JDK版本為1.8
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。
Java SPI 實際上是“基於介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制。
系統設計的各個抽象,往往有很多不同的實現方案,在面向的物件的設計裡,一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改程式碼。為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制。
Java SPI就是提供這樣的一個機制:為某個介面尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模組化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。
要使用Java SPI,需要遵循如下約定:
1、當服務提供者提供了介面的一種具體實現後,在jar包的META-INF/services
目錄下建立一個以“介面全限定名”為命名的檔案,內容為實現類的全限定名;
2、介面實現類所在的jar包放在主程式的classpath中;
3、主程式透過java.util.ServiceLoder
動態裝載實現模組,它透過掃描META-INF/services
目錄下的配置檔案找到實現類的全限定名,把類載入到JVM;
4、SPI的實現類必須攜帶一個不帶引數的構造方法;
專案結構如下:
src/main
--------java
lzc.spidemo
api
Car.java
impl
BCCar.java
BMCar.java
Test.java
--------resource
META-INF.services
lzc.spidemo.api.Car
Car.java
public interface Car {
public String getCarName();
}
BCCar.java
public class BCCar implements Car {
@Override
public String getCarName() {
return "賓士車";
}
}
BMCar.java
public class BMCar implements Car {
@Override
public String getCarName() {
return "寶馬車";
}
}
META-INF/services/lzc.spidemo.api.Car
在META-INF/services/
下新建一個檔案,名字為lzc.spidemo.api.Car
lzc.spidemo.impl.BCCar
lzc.spidemo.impl.BMCar
Test.java
public class Test {
public static void main(String[] args) {
// ServiceLoader.load(Car.class)
ServiceLoader<Car> carList = ServiceLoader.load(Car.class);
// ServiceLoader.iterator()
Iterator<Car> carIterator = carList.iterator();
while (carIterator.hasNext()) { // LazyIterator.hasNext()
Car car = carIterator.next(); // LazyIterator.next()
System.out.println(car.getCarName());
}
}
}
根據執行結果可以發現,ServiceLoader.load(Car.class)
可以幫我們找到Car
的實現類BCCar
和BMCar
,接下來透過原來來分析一下它是如何找到Car
的實現類的。
Java的SPI機制實現跟ServiceLoader
這個類有關
public final class ServiceLoader<S> implements Iterable<S> {
// 讀取配置檔案的字首路徑 META-INF/services/
private static final String PREFIX = "META-INF/services/";
// 需要被載入的服務介面或者服務類
private final Class<S> service;
// 類載入器
private final ClassLoader loader;
// 建立ServiceLoader時採用的訪問控制上下文,預設情況下為 null
private final AccessControlContext acc;
// 快取SPI的實現,key是完整類名
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 當前的迭代器,預設初始化為: new LazyIterator(service, loader)
// 這裡是懶載入的,只有使用的時候才去迭代,載入
// LazyIterator 是 ServiceLoader 的內部類
private LazyIterator lookupIterator;
}
可以看到,ServiceLoader
實現了Iterable
介面,覆寫其iterator
方法能產生一個迭代器;同時ServiceLoader
有一個內部類LazyIterator
,而LazyIterator
又實現了Iterator
介面,說明LazyIterator
是一個迭代器。
ServiceLoader.load(Car.class)
從ServiceLoader.load(Car.class)
開始分析
// java.util.ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取當前執行緒上下文類載入器 AppClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 把剛才取出的執行緒上下文類載入器作為引數傳入,用於後面去載入classpath中的外部廠商提供的驅動類
// 將service介面類和執行緒上下文類載入器作為引數傳入,繼續呼叫load方法
return ServiceLoader.load(service, cl);
}
檢視ServiceLoader.load(service, cl)
// java.util.ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
// 將service介面類和執行緒上下文類載入器作為構造引數,建立一個ServiceLoader物件
return new ServiceLoader<>(service, loader);
}
檢視new ServiceLoader<>(service, loader)
// java.util.ServiceLoader
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();
}
這裡主要是對類屬性做賦值操作,然後呼叫了reload()
方法
檢視reload()
方法
// java.util.ServiceLoader
public void reload() {
// 清除快取
providers.clear();
// 新建 LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
在reload
方法中又新建了一個LazyIterator
物件,然後賦值給lookupIterator
。
// java.util.ServiceLoader
// LazyIterator 是 ServiceLoader的內部類
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
}
在構建LazyIterator
物件時,只是給其service
和loader
屬性賦值,並沒有去載入介面的實現類。
ServiceLoader.iterator()
檢視ServiceLoader.iterator()
// java.util.ServiceLoader
public Iterator<S> iterator() {
// 這裡返回的是一個匿名的迭代器物件
return new Iterator<S>() {
// 將 providers 快取的資料轉換成迭代器 Iterator 並賦值給 knownProviders
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 如果快取中有資料就直接返回true
if (knownProviders.hasNext())
return true;
// 快取中沒有資料,呼叫 LazyIterator.hasNext()
return lookupIterator.hasNext();
}
public S next() {
// 如果快取中有資料就直接從快取中返回
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 快取中沒有資料,呼叫 LazyIterator.next()
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
LazyIterator.hasNext()
檢視LazyIterator.hasNext()
// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
public boolean hasNext() {
// 預設情況下 acc = null
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
檢視hasNextService()
方法
// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
// 這裡會涉及到一個名詞 "全限定名"
// 這裡說的 "全限定名" = "包路徑"."類名名稱或者是介面名名稱"
private boolean hasNextService() {
// 如果 nextName 不為空,則直接返回 true
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = "META-INF/services/"
// service.getName() 獲取介面的全限定名
// 在構建LazyIterator物件時已經給其成員屬性service賦值
String fullName = PREFIX + service.getName();
// 在構建LazyIterator物件時已經給其成員屬性loader賦值
// 載入 "META-INF/services/介面的全限定名" 檔案
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析 "META-INF/services/介面的全限定名" 檔案內容
// 返回 "META-INF/services/介面的全限定名" 檔案內容中的服務提供者的全限定名並賦值給pending屬性
pending = parse(service, configs.nextElement());
}
// 然後取出一個服務提供者全限定名賦值給LazyIterator的成員變數nextName
nextName = pending.next();
return true;
}
這裡用到了懶載入的思想,當需要時才去載入配置檔案。
該方法的主要功能是:去META-INF/services/
目錄下載入介面檔案的內容,解析檔案內容賦值給LazyIterator
的成員變數pending
,pending
是一個Iterator
,然後取出第一個值賦值給LazyIterator
的成員變數nextName
。
比如META-INF/services/lzc.spidemo.api.Car
的檔案內容為。
lzc.spidemo.impl.BCCar
lzc.spidemo.impl.BMCar
那麼LazyIterator
的pending
屬性就儲存了lzc.spidemo.impl.BCCar
和lzc.spidemo.impl.BMCar
這兩個值,然後取出lzc.spidemo.impl.BCCar
賦值給LazyIterator
的成員變數nextName
。
執行完LazyIterator
的hasNext
方法後,會繼續執行LazyIterator
的next
方法
LazyIterator.next()
檢視LazyIterator.next()
方法
// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
public S next() {
// 預設情況下 acc = null
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
檢視LazyIterator.nextService()
方法
// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// hasNextService()方法中為 nextNam e賦值過服務提供者實現類的全限定名
String cn = nextName;
// 將 nextName 設定為空
// 下次呼叫 hasNextService() 時會從 pending 獲取值並賦值給 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集合進行快取
providers.put(cn, p);
// 返回例項
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
LazyIterator.nextService()
利用Class.forName(String name, boolean initialize,ClassLoader loader)
載入服務提供者實現類,然後利用c.newInstance()
例項化,把例項化的類儲存到providers
中快取起來。
JDBC驅動載入是利用了Java的SPI機制。JDBC提供了一組介面規範,不同的資料庫廠商只要編寫符合這套JDBC介面規範的驅動程式碼,那麼就可以用Java語言來連線資料庫。
以MySQL為例分析JDBC驅動載入原始碼。
首先需要引入mysql-connector-java
依賴包,版本為8.0.20。mysql-connector-java.jar
包目錄下的META-INF/services/
下有一個java.sql.Driver
檔案,檔案內容如下所示:
com.mysql.cj.jdbc.Driver
載入MySQL驅動類
public class DBUtils {
private static String dirverClassName = "com.mysql.cj.jdbc.Driver";
private static String url = "jdbc:mysql://127.0.0.1:3306/lzc?characterEncoding=utf8";
private static String user = "root";
private static String password = "root";
public static Connection getDBConnection() {
Connection conn = null;
// try {
// // 在JDBC 4.0 規範中可以省略這一步了
// // Class.forName 用來載入類資訊並執行類的靜態塊
// Class.forName(dirverClassName);
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// }
try {
// 透過 DriverManager 獲取 Connection
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
從上面的程式碼可以發現,透過DriverManager.getConnection(String url,String user, String password)
可以獲取Connection
連線資訊,此時肯定會執行DriverManager
的靜態塊程式碼。
檢視java.sql.DriverManager
靜態程式碼:
// java.sql.DriverManager
public class DriverManager {
// 用來儲存JDBC驅動列表
// 就是實現了 java.sql.Driver 介面的類在類載入時會主動註冊到 registeredDrivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
檢視loadInitialDrivers()
方法
// java.sql.DriverManager
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 重點看這裡
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 前面已經分析過這裡的原理了
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 呼叫 ServiceLoader 的 iterator 方法
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 在迭代的時候會去載入META-INF/services/java.sql.Driver檔案,
// 檔案內容為 com.mysql.cj.jdbc.Driver
// 然後例項化 com.mysql.cj.jdbc.Driver
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
這裡主要是利用了Java的SPI機制去例項化MySQL驅動類,下面檢視MySQL驅動類是如何註冊到DriverManager
類的registeredDrivers
集合中
驅動類註冊到DriverManager
檢視com.mysql.cj.jdbc.Driver
的靜態塊程式碼
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
// 這裡會將 com.mysql.cj.jdbc.Driver 例項註冊到
// DriverManager類的registeredDrivers集合中
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}
看到這裡可以知道,com.mysql.cj.jdbc.Driver
在例項化的時候會執行它的靜態塊程式碼,此時會將自己註冊到DriverManager
類的registeredDrivers
集合中。
MySQL驅動連線資料庫
com.mysql.cj.jdbc.Driver
已經註冊到DriverManager
類的registeredDrivers
集合中,下面檢視它是何時被呼叫的。
透過前面的例子可以知道,透過DriverManager.getConnection(String url,String user, String password)
可以獲取Connection
連線資訊,檢視這個方法的程式碼:
// java.sql.DriverManager
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
繼續往下看
// java.sql.DriverManager
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
// 主要檢視這裡
// 遍歷 registeredDrivers 集合
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
// 利用驅動類來連線資料庫
Connection con = aDriver.driver.connect(url, info);
// 只要連線上就直接放回 Connection,如果有多個驅動類,其餘的就會被忽略
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結