深入理解SPI機制


一、什么是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

 
MySQL SPI文件

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM