深入理解SPI機制
連結:https://www.jianshu.com/p/3a3edbcd8f24
一、什麼是SPI
SPI ,全稱為 Service Provider Interface,是一種服務發現機制。它通過在ClassPath路徑下的META-INF/services資料夾查詢檔案,自動載入檔案裡所定義的類。
這一機制為很多框架擴充套件提供了可能,比如在Dubbo、JDBC中都使用到了SPI機制。我們先通過一個很簡單的例子來看下它是怎麼用的。
1、小栗子
首先,我們需要定義一個介面,SPIService
package com.viewscenes.netsupervisor.spi; public interfaceSPIService { 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路徑下配置新增一個檔案。檔名字是介面的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。
檔案路徑如下:
SPI配置檔案位置
內容就是實現類的全限定類名:
com.viewscenes.netsupervisor.spi.SpiImpl1 com.viewscenes.netsupervisor.spi.SpiImpl2
2、測試
然後我們就可以通過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(); } } }
兩種方式的輸出結果是一致的:
SpiImpl1.execute() SpiImpl2.execute() -------------------------------- SpiImpl1.execute() SpiImpl2.execute()
二、原始碼分析
我們看到一個位於sun.misc包
,一個位於java.util包
,sun包下的原始碼看不到。我們就以ServiceLoader.load為例,通過原始碼看看它裡面到底怎麼做的。
1、ServiceLoader
首先,我們先來了解下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; }
2、Load
load方法建立了一些屬性,重要的是例項化了內部類,LazyIterator。最後返回ServiceLoader的例項。
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); } }
3、查詢實現類
查詢實現類和建立實現類的過程,都在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; } }
4、建立例項
當然,呼叫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; } }
看到這兒,我想已經很清楚了。獲取到類的例項,我們自然就可以對它為所欲為了!
三、JDBC中的應用
我們開頭說,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。
1、載入
我們把目光回到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
。
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() } }
2、建立例項
上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當呼叫next方法時,就會建立這個類的例項。它就完成了一件事,向DriverManager註冊自身的例項。
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()); } } }
4、再擴充套件
既然我們知道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
連結:https://www.jianshu.com/p/3a3edbcd8f24
來源:簡書