深刻理解SPI機制

1、什麼是SPI

SPI ,全稱爲 Service Provider Interface,是一種服務發現機制。它經過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件裏所定義的類。java

這一機制爲不少框架擴展提供了可能,好比在Dubbo、JDBC中都使用到了SPI機制。咱們先經過一個很簡單的例子來看下它是怎麼用的。mysql

一、小栗子

首先,咱們須要定義一個接口,SPIServicesql

package com.viewscenes.netsupervisor.spi;public interface SPIService {    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路徑下配置添加一個文件。文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。 文件路徑以下: 圖片 內容就是實現類的全限定類名:框架

com.viewscenes.netsupervisor.spi.SpiImpl1com.viewscenes.netsupervisor.spi.SpiImpl2

二、測試

而後咱們就能夠經過 ServiceLoader.load或者Service.providers方法拿到實現類的實例。其中, Service.providers包位於 sun.misc.Service,而 ServiceLoader.load包位於 java.util.ServiceLoaderide

 
 
  1. public class Test {源碼分析

  2.    public static void main(String[] args) {    測試

  3.        Iterator<SPIService> providers = Service.providers(SPIService.class);ui

  4.        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);url


  5.        while(providers.hasNext()) {

  6.            SPIService ser = providers.next();

  7.            ser.execute();

  8.        }

  9.        System.out.println("--------------------------------");

  10.        Iterator<SPIService> iterator = load.iterator();

  11.        while(iterator.hasNext()) {

  12.            SPIService ser = iterator.next();

  13.            ser.execute();

  14.        }

  15.    }

  16. }

兩種方式的輸出結果是一致的:

SpiImpl1.execute()SpiImpl2.execute()--------------------------------SpiImpl1.execute()SpiImpl2.execute()
2、源碼分析

咱們看到一個位於 sun.misc,一個位於 java.util,sun包下的源碼看不到。咱們就以ServiceLoader.load爲例,經過源碼看看它裏面到底怎麼作的。

一、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;}

二、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);    }}

三、查找實現類

查找實現類和建立實現類的過程,都在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;    }}

四、建立實例

固然,調用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;     }}

看到這兒,我想已經很清楚了。獲取到類的實例,咱們天然就能夠對它隨心所欲了!

3、JDBC中的應用

咱們開頭說,SPI機制爲不少框架的擴展提供了可能,其實JDBC就應用到了這一機制。回憶一下JDBC獲取數據庫鏈接的過程。在早期版本中,須要先設置數據庫驅動的鏈接,再經過DriverManager.getConnection獲取一個Connection。

 
 
  1. String url = "jdbc:mysql:///consult?serverTimezone=UTC";

  2. String user = "root";

  3. String password = "root";


  4. Class.forName("com.mysql.jdbc.Driver");

  5. Connection connection = DriverManager.getConnection(url, user, password);

在較新版本中(具體哪一個版本,筆者沒有驗證),設置數據庫驅動鏈接,這一步驟就再也不須要,那麼它是怎麼分辨是哪一種數據庫的呢?答案就在SPI。

一、加載

咱們把目光回到 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。 圖片

二、建立實例

上一步已經找到了MySQL中的com.mysql.cj.jdbc.Driver全限定類名,當調用next方法時,就會建立這個類的實例。它就完成了一件事,向DriverManager註冊自身的實例。

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()    }}

三、建立Connection

在DriverManager.getConnection()方法就是建立鏈接的地方,它經過循環已註冊的數據庫驅動程序,調用其connect方法,獲取鏈接並返回。

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());        }    }}

四、再擴展

既然咱們知道JDBC是這樣建立數據庫鏈接的,咱們能不能再擴展一下呢?若是咱們本身也建立一個java.sql.Driver文件,自定義實現類MyDriver,那麼,在獲取鏈接的先後就能夠動態修改一些信息。

仍是先在項目ClassPath下建立文件,文件內容爲自定義驅動類 com.viewscenes.netsupervisor.spi.MyDriver

圖片

咱們的MyDriver實現類,繼承自MySQL中的NonRegisteringDriver,還要實現java.sql.Driver接口。這樣,在調用connect方法的時候,就會調用到此類,但實際建立的過程還靠MySQL完成。

 
 
  1. package com.viewscenes.netsupervisor.spi


  2. public class MyDriver extends NonRegisteringDriver implements Driver{

  3.    static {

  4.        try {

  5.            java.sql.DriverManager.registerDriver(new MyDriver());

  6.        } catch (SQLException E) {

  7.            throw new RuntimeException("Can't register driver!");

  8.        }

  9.    }

  10.    public MyDriver()throws SQLException {}


  11.    public Connection connect(String url, Properties info) throws SQLException {

  12.        System.out.println("準備建立數據庫鏈接.url:"+url);

  13.        System.out.println("JDBC配置信息:"+info);

  14.        info.setProperty("user", "root");

  15.        Connection connection =  super.connect(url, info);

  16.        System.out.println("數據庫鏈接建立完成!"+connection.toString());

  17.        return connection;

  18.    }

  19. }

  20. --------------------輸出結果---------------------

  21. 準備建立數據庫鏈接.url:jdbc:mysql:///consult?serverTimezone=UTC

  22. JDBC配置信息:{user=root, password=root}

  23. 數據庫鏈接建立完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

相關文章
相關標籤/搜索