SPI(Service Provider Interface),是JDK內置的一種 服務提供發現機制,能夠用來啓用框架擴展和替換組件,主要是被框架的開發人員使用,好比java.sql.Driver接口,其餘不一樣廠商能夠針對同一接口作出不一樣的實現,MySQL和PostgreSQL都有不一樣的實現提供給用戶,而Java的SPI機制能夠爲某個接口尋找服務實現。Java中SPI機制主要思想是將裝配的控制權移到程序以外,在模塊化設計中這個機制尤爲重要,其核心思想就是 解耦。java
SPI總體機制圖以下:mysql
當服務的提供者提供了一種接口的實現以後,須要在classpath下的META-INF/services/目錄裏建立一個以服務接口命名的文件,這個文件裏的內容就是這個接口的具體的實現類。當其餘的程序須要這個服務的時候,就能夠經過查找這個jar包(通常都是以jar包作依賴)的META-INF/services/中的配置文件,配置文件中有接口的具體實現類名,能夠根據這個類名進行加載實例化,就可使用該服務了。JDK中查找服務的實現的工具類是:java.util.ServiceLoader。sql
SPI擴展機制應用場景有不少,好比Common-Logging,JDBC,Dubbo,Cipher等等。數據庫
SPI流程:bootstrap
好比JDBC場景下:tomcat
咱們也能夠自定義SPI。app
先定義一個接口框架
package org.ifool.spiDemo; public interface HelloSpi { public void sayHello(); }
定義兩個實現類maven
HelloImpl1:ide
package org.ifool.spiDemo; public class HelloImpl1 implements HelloSpi { public void sayHello() { System.out.println("Hello Impl 1"); } }
HelloImpl2:
package org.ifool.spiDemo; public class HelloImpl2 implements HelloSpi { public void sayHello() { System.out.println("Hello Impl 2"); } }
在META-INF/services(對於maven工程可在src/main/resources下新建META-INF目錄)建立文件,名字就是org.ifool.spiDemo.HelloSpi,而後內容爲兩個實現類。
內容以下:
org.ifool.spiDemo.HelloImpl1 org.ifool.spiDemo.HelloImpl2
也就是說org.ifool.spiDemo.HelloSpi在這裏有兩個實現類。
main函數中用ServiceLoader進行加載
package org.ifool.spiDemo; import java.util.ServiceLoader; public class App { public static void main(String[] args) { ServiceLoader<HelloSpi> serviceLoader = ServiceLoader.load(HelloSpi.class); for (HelloSpi helloSPI : serviceLoader) { helloSPI.sayHello(); } } }
運行效果以下:
Hello Impl 1 Hello Impl 2
這個ServiceLoader實現了Iterable。
public final class ServiceLoader<S> implements Iterable<S>
也就是說,對於一個定義好的SPI接口,若是在類路徑下存在這個接口的實現,那麼咱們就能夠把使用ServiceLoader把這個接口的實現類都加載進來,而且每一個實現類都放一個實例到這個ServiceLoader,這個實例是經過newInstance()實現的,因此實現類必須有一個無參構造函數。
每一個接口,能夠有多個實現類,可是咱們只能順序的遍歷ServiceLoader來逐個獲取,無法經過map.get()使用名字獲取一個實現對象,並且這個過程是懶加載的,只有真正遍歷的時候纔會加載並建立實現類。
假如咱們把HelloImpl2修改一下,增長一個有參數的構造函數,就會報錯
package org.ifool.spiDemo; public class HelloImpl2 implements HelloSpi { public void sayHello() { System.out.println("Hello Impl 2"); } public HelloImpl2(int a) { a = 5; } }
再次執行會拋異常:
Hello Impl 1 Exception in thread "main" java.util.ServiceConfigurationError: org.ifool.spiDemo.HelloSpi: Provider org.ifool.spiDemo.HelloImpl2 could not be instantiated at java.util.ServiceLoader.fail(ServiceLoader.java:232)
這個異常並非在ServiceLoader.load(HelloSpi.class)時拋的,由於HelloImpl1已經被實例化了,而是在遍歷ServiceLoader時拋的異常,說明實現類的加載和建立是lazy模式的。
大多數框架使用ServiceLoader的時候,並不必定須要建立的這個對象,只是須要它作類的加載以及一些初始化工做。下面分析一下是怎麼實現的。
ServiceLoader這個類不復雜,調用它加載接口的實現類時,它會到各個jar包中的META-INF/services中尋找實現類,使用class.forName加載類,而後用newInstance得到一個實例,再放到Map中,由於這個Map是private的,因此外界無法使用get方法獲取實例。
下面是它的成員變量,能夠看到META-INF/services是寫死在代碼裏的。
public final class ServiceLoader<S> implements Iterable<S> { //到META-INF/services中搜索相應的ServiceProvider類名 private static final String PREFIX = "META-INF/services/"; // The class or interface representing the service being loaded private final Class<S> service; // The class loader used to locate, load, and instantiate providers private final ClassLoader loader; // The access control context taken when the ServiceLoader is created private final AccessControlContext acc; //把獲得的Provider實例放到一個LinkedHashMap中 // Cached providers, in instantiation order private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // The current lazy-lookup iterator private LazyIterator lookupIterator;
它用了一個懶加載的策略,
//在調用ServiceLoader.load(HelloSpi.class)的時候,會傳入一個ContextClassLoader,而後繼續調用ServiceLoader.load(HelloSpi.class,cl) public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } //這個函數是private的,若是cl爲空的話,使用systemClassLoader,繼續調用reload 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清空providers,而後新建一個LazyIterator,這個其實只是把實現類的名稱記下來了 public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } //真正要獲取Service實例的時候,纔會加載類,而且new一個實例放到providers中 private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = 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()); //new一個實例,而且以類名爲key放到map裏 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
Java的JDBC Driver接口在rt.jar裏,名字是java.sql.Driver,它提供了幾個接口,主要的是connect接口,而後具體實現是由廠商實現的。
public interface Driver { Connection connect(String url, java.util.Properties info) throws SQLException; boolean acceptsURL(String url) throws SQLException; DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException; int getMajorVersion(); int getMinorVersion(); boolean jdbcCompliant(); public Logger getParentLogger() throws SQLFeatureNotSupportedException; }
咱們能夠看到在mysql-connector裏的META-INF/services裏有一個文件java.sql.Driver,它的實現是com.mysql.cj.jdbc.Driver
com.mysql.cj.jdbc.Driver
咱們使用JDBC的時候,都是從DriverManager開始的,例以下面獲取Connection
package org.ifool.spiDemo; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JDBCDemo { public static void main(String[] args) throws SQLException { //Class.forName("com.mysql.cj.jdbc.Driver"); 已經不須要了 String url = "jdbc:mysql://localhost:3306/mysql?serverTimezone=GMT%2B8"; String user = "root"; String password = "123456"; Connection connections = DriverManager.getConnection(url, user, password); } }
DriverManager在初始化的時候,在它的static代碼塊中,會使用ServiceLoader加載全部的java.sql.Driver的實現類,這樣各個java.sql.Driver的實現類會被加載,它們的static塊也會被執行,同時newInstance建立一個實例,可是這裏沒用到。
/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ /** 先使用老的方式,在jdbc.properties裏找提供者,這是爲了兼容老版本,再使用ServiceLoader機制**/ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } //使用ServiceLoader加載實現類,必須調用iterator.next()纔會真正加載,由於是懶加載的,這裏的做用只是加載一下類而已,實際沒用到初始化的實例,可是類裏的static塊會被執行 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; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } });
假設咱們的類路徑裏有mysql和db2的實現類,那麼它們都會被初始化,看一下mysql的實現,它在static塊中調用了DriverManager把本身註冊到了DriverManager中。一樣,DB2可能也會作一些相似的操做。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { // // Register ourselves with the DriverManager // static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } /** * Construct a new driver and register it with DriverManager * */ public Driver() throws SQLException { // Required for Class.forName().newInstance() } }
咱們再看DriverManager的getConnection函數,它會遍歷registeredDrivers,選擇出Allowed的driver,嘗試用這個Driver去connect, 這個過程可能會有用db2的driver去連mysql數據庫,可是db2 driver鏈接的時候,根據url,發現不是db2數據庫,則立馬返回失敗,嘗試用下一個driver去連。判斷是否Allowed,用的是isDriverAllowed的,這個後面再說。
// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ 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; 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); 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"); }
爲何須要在下面的函數裏判斷⼀下是否容許使用呢?DriverManager管理着JVM⾥全部的Driver,可是同⼀個Driver可能被加載屢次,⽐如tomcat⾥,多個應⽤都會加載mysql的driver,可是DriverManager在選擇的時候,必須選擇與調⽤者的classloader⼀樣的Driver。
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) { boolean result = false; if(driver != null) { Class<?> aClass = null; try { aClass = Class.forName(driver.getClass().getName(), true, classLoader); } catch (Exception ex) { result = false; } result = ( aClass == driver.getClass() ) ? true : false; } return result; }
這個⽅法⽐較特別的地⽅在於,它拿到⼀個Driver,而後判斷這個Driver是否是與caller⽤的同⼀個classloader,若是不是的話,那麼調⽤forName的時候,正好⼜⽤這個caller的classloader加載了⼀個Driver放到了registeredDrivers⾥⾯,咱們看⼀個實例。
在⼀個tomcat⾥,有兩個war包,都⽤的同⼀個版本的mysql驅動,tomcat重啓的時候,兩個war包會有⼀個先調⽤DriverManager.getConnection(),接着另⼀個調⽤。
war1調⽤DriverManager.getConnection()
DriverManager經過SPI機制把全部的jdbc driver都加載⼀次,這時候使⽤的類加載器是war1的,咱們記做war1loader,DriverManager經過war1loader加載mysql Driver,mysql Driver主動register⾃⼰,這時候registeredDrivers的結果以下:
registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver"}
接下來,war2調⽤DriverManager.getConnection(),由於Drivermanager已經初始化過了,因此SPI那⼀套流程不會走了。會遍歷registeredDrivers,而且判斷是不是⾃⼰加載的,war2的加載器爲war2loader,在iisDriverAllowed中,會調⽤
class.forName("com.mysql.cj.jdbc.Driver", true, war2loader)
顯然這個結果與已經存在的不⼀致,可是,咱們⽤war2loader加載驅動,會再次調⽤Driver的初始化,
它繼續調⽤register,因此如今的結果就是
registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver", "war2loader: com.mysql.cj.jdbc.Driver"}
由於registeredDrivers是CopyOnWriteList,循環會繼續往下走,下⼀次就能走過isAllowed,而後能夠調⽤connect。
前⾯屢次出現了ContextClassloader,沒有展開解釋,ContextClassLoader這個機制不太好理解,咱們 先來看⼀下雙親委派機制。
底層的類加載器要加載⼀個類時,先向上委託,有沒有發現⼀個特色, 這種雙親委派機制,直接⽗加載器是惟⼀的,因此向上委託,是不會有⼆義性的(OSGI不在討論範圍內)。 可是,假如在上層的類(例如DriverManager,它是由bootstrap classloader加載的)⾥要加載底層的類,它會⽤⾃⼰的加載器去加載,對於SPI來講,它的實現類都是在下層的,須要由下層的classloader加載,
仍是以DriverManager爲例,假設它在⾃⼰的代碼⾥調⽤(雖然沒有在代碼⾥寫上mysql,可是隻要把mysql的jar包放在這,Drivermanager最終會掃描到而且調⽤class.forName("com.mysql.c.jdbc.driver")的,只是它傳了classloader):
class.forName("com.mysql.cj.jdbc.driver");
咱們看forName的代碼
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
此處會尋找caller的類,而後找它的classloader,DriverManager調⽤的forName,因此此處的caller就是DriverManager.class,可是咱們知道DriverManager是bootstrap加載的,那此處獲取classloader就是null。forName0是native⽅法,它發現classloader是null就嘗試⽤bootstrap加載,可是咱們要加載的是mysql的類,bootstrap確定是不能加載的。
假設咱們的委派鏈是個單純的單鏈表,那麼咱們⽤⼀個雙向鏈表向下委託就⾏了,可是這種機制的委託鏈並非單鏈表,因此向下委託是有⼆義性的。
那怎麼辦呢?誰調⽤我,我就⽤誰的加載器,這個加載器放在哪呢,就跟線程綁定,也就是Thread Context ClassLoader。
因此DriverManager在實際調⽤forName的時候,要⽤ContextClassLoader。 它⼀共有兩處會加載類
⼀處是類初始化調⽤ServiceLoader的時候,咱們知道ServiceLoader使⽤的是contextClassloader。
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
⼀處是getConnection的時候,先檢查⼀下caller的classloader,若是是null的話就使⽤ContextClassloader,在isDriverAllowed⾥加載類
// Worker method called by the public getConnection() methods. private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { /* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } ..... if(isDriverAllowed(aDriver.driver, callerCL)) { .....
Thread Context ClassLoader意義就是:⽗Classloader可使⽤當前線程Thread.currentthread().getContextLoader()中指定的classloader中加載的類。顛覆了⽗ClassLoader不能使⽤⼦Classloader或者是其它沒有直接⽗⼦關係的Classloader中加載的類這種狀況。這個就是Thread Context ClassLoader的意義。⼀個線程的默認ContextClassLoader是繼承⽗線程的,能夠調⽤set從新 設置,若是在main線程⾥查看,它就是AppClassLoader。