一、什么是SPI
SPI ,全稱為 Service Provider Interface,是一種服務發現機制。它通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件里所定義的類。
這一機制為很多框架擴展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機制。我們先通過一個很簡單的例子來看下它是怎么用的。
1、小栗子
首先,我們需要定義一個接口,SPIService
package com.viewscenes.netsupervisor.spi; public interface SPIService { void execute(); }
然后,定義兩個實現類,沒別的意思,只輸入一句話。
1 package com.viewscenes.netsupervisor.spi; 2 public class SpiImpl1 implements SPIService{ 3 public void execute() { 4 System.out.println("SpiImpl1.execute()"); 5 } 6 } 7 ----------------------我是乖巧的分割線---------------------- 8 package com.viewscenes.netsupervisor.spi; 9 public class SpiImpl2 implements SPIService{ 10 public void execute() { 11 System.out.println("SpiImpl2.execute()"); 12 } 13 }
最后呢,要在ClassPath路徑下配置添加一個文件。文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。
文件路徑如下:

內容就是實現類的全限定類名:
1 com.viewscenes.netsupervisor.spi.SpiImpl1 2 com.viewscenes.netsupervisor.spi.SpiImpl2
2、測試
然后我們就可以通過ServiceLoader.load或者Service.providers方法拿到實現類的實例。其中,Service.providers包位於sun.misc.Service,而ServiceLoader.load包位於java.util.ServiceLoader。
1 public class Test { 2 public static void main(String[] args) { 3 Iterator<SPIService> providers = Service.providers(SPIService.class); 4 ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class); 5 6 while(providers.hasNext()) { 7 SPIService ser = providers.next(); 8 ser.execute(); 9 } 10 System.out.println("--------------------------------"); 11 Iterator<SPIService> iterator = load.iterator(); 12 while(iterator.hasNext()) { 13 SPIService ser = iterator.next(); 14 ser.execute(); 15 } 16 } 17 }
兩種方式的輸出結果是一致的:
1 SpiImpl1.execute() 2 SpiImpl2.execute() 3 -------------------------------- 4 SpiImpl1.execute() 5 SpiImpl2.execute()
二、源碼分析
我們看到一個位於sun.misc包,一個位於java.util包,sun包下的源碼看不到。我們就以ServiceLoader.load為例,通過源碼看看它里面到底怎么做的。
1、ServiceLoader
首先,我們先來了解下ServiceLoader,看看它的類結構。
1 public final class ServiceLoader<S> implements Iterable<S> 2 //配置文件的路徑 3 private static final String PREFIX = "META-INF/services/"; 4 //加載的服務類或接口 5 private final Class<S> service; 6 //已加載的服務類集合 7 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); 8 //類加載器 9 private final ClassLoader loader; 10 //內部類,真正加載服務類 11 private LazyIterator lookupIterator; 12 }
2、Load
load方法創建了一些屬性,重要的是實例化了內部類,LazyIterator。最后返回ServiceLoader的實例。
1 public final class ServiceLoader<S> implements Iterable<S> 2 private ServiceLoader(Class<S> svc, ClassLoader cl) { 3 //要加載的接口 4 service = Objects.requireNonNull(svc, "Service interface cannot be null"); 5 //類加載器 6 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; 7 //訪問控制器 8 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; 9 //先清空 10 providers.clear(); 11 //實例化內部類 12 LazyIterator lookupIterator = new LazyIterator(service, loader); 13 } 14 }
3、查找實現類
查找實現類和創建實現類的過程,都在LazyIterator完成。當我們調用iterator.hasNext和iterator.next方法的時候,實際上調用的都是LazyIterator的相應方法。
1 public Iterator<S> iterator() { 2 return new Iterator<S>() { 3 public boolean hasNext() { 4 return lookupIterator.hasNext(); 5 } 6 public S next() { 7 return lookupIterator.next(); 8 } 9 ....... 10 }; 11 }
所以,我們重點關注lookupIterator.hasNext()方法,它最終會調用到hasNextService。
1 private class LazyIterator implements Iterator<S>{ 2 Class<S> service; 3 ClassLoader loader; 4 Enumeration<URL> configs = null; 5 Iterator<String> pending = null; 6 String nextName = null; 7 private boolean hasNextService() { 8 //第二次調用的時候,已經解析完成了,直接返回 9 if (nextName != null) { 10 return true; 11 } 12 if (configs == null) { 13 //META-INF/services/ 加上接口的全限定類名,就是文件服務類的文件 14 //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService 15 String fullName = PREFIX + service.getName(); 16 //將文件路徑轉成URL對象 17 configs = loader.getResources(fullName); 18 } 19 while ((pending == null) || !pending.hasNext()) { 20 //解析URL文件對象,讀取內容,最后返回 21 pending = parse(service, configs.nextElement()); 22 } 23 //拿到第一個實現類的類名 24 nextName = pending.next(); 25 return true; 26 } 27 }
4、創建實例
當然,調用next方法的時候,實際調用到的是,lookupIterator.nextService。它通過反射的方式,創建實現類的實例並返回。
1 private class LazyIterator implements Iterator<S>{ 2 private S nextService() { 3 //全限定類名 4 String cn = nextName; 5 nextName = null; 6 //創建類的Class對象 7 Class<?> c = Class.forName(cn, false, loader); 8 //通過newInstance實例化 9 S p = service.cast(c.newInstance()); 10 //放入集合,返回實例 11 providers.put(cn, p); 12 return p; 13 } 14 }
看到這兒,我想已經很清楚了。獲取到類的實例,我們自然就可以對它為所欲為了!
三、JDBC中的應用
我們開頭說,SPI機制為很多框架的擴展提供了可能,其實JDBC就應用到了這一機制。回憶一下JDBC獲取數據庫連接的過程。在早期版本中,需要先設置數據庫驅動的連接,再通過DriverManager.getConnection獲取一個Connection。
1 String url = "jdbc:mysql:///consult?serverTimezone=UTC"; 2 String user = "root"; 3 String password = "root"; 4 5 Class.forName("com.mysql.jdbc.Driver"); 6 Connection connection = DriverManager.getConnection(url, user, password);
在較新版本中(具體哪個版本,筆者沒有驗證),設置數據庫驅動連接,這一步驟就不再需要,那么它是怎么分辨是哪種數據庫的呢?答案就在SPI。
1、加載
我們把目光回到DriverManager類,它在靜態代碼塊里面做了一件比較重要的事。很明顯,它已經通過SPI機制, 把數據庫驅動連接初始化了。
1 public class DriverManager { 2 static { 3 loadInitialDrivers(); 4 println("JDBC DriverManager initialized"); 5 } 6 }
具體過程還得看loadInitialDrivers,它在里面查找的是Driver接口的服務類,所以它的文件路徑就是:META-INF/services/java.sql.Driver。
1 public class DriverManager { 2 private static void loadInitialDrivers() { 3 AccessController.doPrivileged(new PrivilegedAction<Void>() { 4 public Void run() { 5 //很明顯,它要加載Driver接口的服務類,Driver接口的包為:java.sql.Driver 6 //所以它要找的就是META-INF/services/java.sql.Driver文件 7 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 8 Iterator<Driver> driversIterator = loadedDrivers.iterator(); 9 try{ 10 //查到之后創建對象 11 while(driversIterator.hasNext()) { 12 driversIterator.next(); 13 } 14 } catch(Throwable t) { 15 // Do nothing 16 } 17 return null; 18 } 19 }); 20 } 21 }
那么,這個文件哪里有呢?我們來看MySQL的jar包,就是這個文件,文件內容為:com.mysql.cj.jdbc.Driver。
2、創建實例
上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當調用next方法時,就會創建這個類的實例。它就完成了一件事,向DriverManager注冊自身的實例。
1 public class Driver extends NonRegisteringDriver implements java.sql.Driver { 2 static { 3 try { 4 //注冊 5 //調用DriverManager類的注冊方法 6 //往registeredDrivers集合中加入實例 7 java.sql.DriverManager.registerDriver(new Driver()); 8 } catch (SQLException E) { 9 throw new RuntimeException("Can't register driver!"); 10 } 11 } 12 public Driver() throws SQLException { 13 // Required for Class.forName().newInstance() 14 } 15 }
3、創建Connection
在DriverManager.getConnection()方法就是創建連接的地方,它通過循環已注冊的數據庫驅動程序,調用其connect方法,獲取連接並返回。
1 private static Connection getConnection( 2 String url, java.util.Properties info, Class<?> caller) throws SQLException { 3 //registeredDrivers中就包含com.mysql.cj.jdbc.Driver實例 4 for(DriverInfo aDriver : registeredDrivers) { 5 if(isDriverAllowed(aDriver.driver, callerCL)) { 6 try { 7 //調用connect方法創建連接 8 Connection con = aDriver.driver.connect(url, info); 9 if (con != null) { 10 return (con); 11 } 12 }catch (SQLException ex) { 13 if (reason == null) { 14 reason = ex; 15 } 16 } 17 } else { 18 println(" skipping: " + aDriver.getClass().getName()); 19 } 20 } 21 }
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
