最近我建了另外一個文章分類,用於擴展JDK
中一些重要但不經常使用的功能。html
SPI
,全名Service Provider Interface
,是一種服務發現機制。它能夠當作是一種針對接口實現類的解耦方案。咱們只須要採用配置文件方式配置好接口的實現類,就能夠利用SPI
機制去加載到它們了,當咱們須要修改實現類時,改改配置文件就能夠了,而不須要去改代碼。java
固然,有的同窗可能會問,spring
也能夠作接口實現類的解耦,是否是SPI
就沒用了呢?雖然二者均可以達到相同的目的,可是不必定全部應用均可以引入spring
框架,例如JDBC
自動發現驅動並註冊,它就是採用SPI
機制,它就不大可能引入spring
來解耦接口實現類。另外,druid
、dubbo
等都採用了SPI
機制。mysql
利用SPI
機制加載用戶服務接口的實現類並測試。git
JDK
:1.8.0_201github
maven
:3.6.1spring
IDE
:eclipse 4.12sql
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
。這裏就簡單實現就行了。eclipse
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方法 ==================
本文以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("=================="); } }
注意:考慮篇幅,如下代碼通過修改,僅保留所需部分。
咱們從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://github.com/ZhangZiSheng001/01-spi-demo
本文爲原創文章,轉載請附上原文出處連接:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html