SPI 原理

LZC發表於2020-06-12

JDK版本為1.8

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。

Java SPI 實際上是“基於介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制。

系統設計的各個抽象,往往有很多不同的實現方案,在面向的物件的設計裡,一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改程式碼。為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制。
Java SPI就是提供這樣的一個機制:為某個介面尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模組化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。

要使用Java SPI,需要遵循如下約定:

1、當服務提供者提供了介面的一種具體實現後,在jar包的META-INF/services目錄下建立一個以“介面全限定名”為命名的檔案,內容為實現類的全限定名;
2、介面實現類所在的jar包放在主程式的classpath中;
3、主程式透過java.util.ServiceLoder動態裝載實現模組,它透過掃描META-INF/services目錄下的配置檔案找到實現類的全限定名,把類載入到JVM;
4、SPI的實現類必須攜帶一個不帶引數的構造方法;
專案結構如下:

src/main
--------java
        lzc.spidemo
            api
                Car.java
            impl
                BCCar.java
                BMCar.java
             Test.java   
--------resource
        META-INF.services
            lzc.spidemo.api.Car

Car.java

public interface Car {
    public String getCarName();
}

BCCar.java

public class BCCar implements Car {
    @Override
    public String getCarName() {
        return "賓士車";
    }
}

BMCar.java

public class BMCar implements Car {
    @Override
    public String getCarName() {
        return "寶馬車";
    }
}

META-INF/services/lzc.spidemo.api.Car

META-INF/services/下新建一個檔案,名字為lzc.spidemo.api.Car

lzc.spidemo.impl.BCCar
lzc.spidemo.impl.BMCar

Test.java

public class Test {
    public static void main(String[] args) {
        // ServiceLoader.load(Car.class)
        ServiceLoader<Car> carList = ServiceLoader.load(Car.class);
        // ServiceLoader.iterator()
        Iterator<Car> carIterator = carList.iterator();
        while (carIterator.hasNext()) { // LazyIterator.hasNext()
            Car car = carIterator.next(); // LazyIterator.next()
            System.out.println(car.getCarName());
        }
    }
}

根據執行結果可以發現,ServiceLoader.load(Car.class)可以幫我們找到Car的實現類BCCarBMCar,接下來透過原來來分析一下它是如何找到Car的實現類的。

Java的SPI機制實現跟ServiceLoader這個類有關

public final class ServiceLoader<S> implements Iterable<S> {
    // 讀取配置檔案的字首路徑 META-INF/services/
    private static final String PREFIX = "META-INF/services/";
    // 需要被載入的服務介面或者服務類
    private final Class<S> service;
    // 類載入器
    private final ClassLoader loader;
    // 建立ServiceLoader時採用的訪問控制上下文,預設情況下為 null
    private final AccessControlContext acc;
    // 快取SPI的實現,key是完整類名
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 當前的迭代器,預設初始化為: new LazyIterator(service, loader)
    // 這裡是懶載入的,只有使用的時候才去迭代,載入
    // LazyIterator 是 ServiceLoader 的內部類
    private LazyIterator lookupIterator;
}

可以看到,ServiceLoader實現了Iterable介面,覆寫其iterator方法能產生一個迭代器;同時ServiceLoader有一個內部類LazyIterator,而LazyIterator又實現了Iterator介面,說明LazyIterator是一個迭代器。

ServiceLoader.load(Car.class)

ServiceLoader.load(Car.class)開始分析

// java.util.ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 獲取當前執行緒上下文類載入器 AppClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 把剛才取出的執行緒上下文類載入器作為引數傳入,用於後面去載入classpath中的外部廠商提供的驅動類
    // 將service介面類和執行緒上下文類載入器作為引數傳入,繼續呼叫load方法
    return ServiceLoader.load(service, cl);
}

檢視ServiceLoader.load(service, cl)

// java.util.ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    // 將service介面類和執行緒上下文類載入器作為構造引數,建立一個ServiceLoader物件
    return new ServiceLoader<>(service, loader);
}

檢視new ServiceLoader<>(service, loader)

// java.util.ServiceLoader
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()方法

檢視reload()方法

// java.util.ServiceLoader
public void reload() {
    // 清除快取
    providers.clear();
    // 新建 LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}

reload方法中又新建了一個LazyIterator物件,然後賦值給lookupIterator

// java.util.ServiceLoader
// LazyIterator 是 ServiceLoader的內部類
private class LazyIterator implements Iterator<S> {
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
}

在構建LazyIterator物件時,只是給其serviceloader屬性賦值,並沒有去載入介面的實現類。

ServiceLoader.iterator()

檢視ServiceLoader.iterator()

// java.util.ServiceLoader
public Iterator<S> iterator() {
    // 這裡返回的是一個匿名的迭代器物件
    return new Iterator<S>() {
        // 將 providers 快取的資料轉換成迭代器 Iterator 並賦值給 knownProviders
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 如果快取中有資料就直接返回true
            if (knownProviders.hasNext())
                return true;
            // 快取中沒有資料,呼叫 LazyIterator.hasNext()
            return lookupIterator.hasNext();
        }

        public S next() {
            // 如果快取中有資料就直接從快取中返回
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            // 快取中沒有資料,呼叫 LazyIterator.next()
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

LazyIterator.hasNext()

檢視LazyIterator.hasNext()

// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
public boolean hasNext() {
    // 預設情況下 acc = null
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

檢視hasNextService()方法

// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
// 這裡會涉及到一個名詞 "全限定名"
// 這裡說的 "全限定名" = "包路徑"."類名名稱或者是介面名名稱"
private boolean hasNextService() {
    // 如果 nextName 不為空,則直接返回 true
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // PREFIX = "META-INF/services/"
            // service.getName() 獲取介面的全限定名
            // 在構建LazyIterator物件時已經給其成員屬性service賦值
            String fullName = PREFIX + service.getName();

            // 在構建LazyIterator物件時已經給其成員屬性loader賦值
            // 載入 "META-INF/services/介面的全限定名" 檔案
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        // 解析 "META-INF/services/介面的全限定名" 檔案內容
        // 返回 "META-INF/services/介面的全限定名" 檔案內容中的服務提供者的全限定名並賦值給pending屬性
        pending = parse(service, configs.nextElement());
    }
    // 然後取出一個服務提供者全限定名賦值給LazyIterator的成員變數nextName
    nextName = pending.next();
    return true;
}

這裡用到了懶載入的思想,當需要時才去載入配置檔案。

該方法的主要功能是:去META-INF/services/目錄下載入介面檔案的內容,解析檔案內容賦值給LazyIterator的成員變數pendingpending是一個Iterator,然後取出第一個值賦值給LazyIterator的成員變數nextName

比如META-INF/services/lzc.spidemo.api.Car的檔案內容為。

lzc.spidemo.impl.BCCar
lzc.spidemo.impl.BMCar

那麼LazyIteratorpending屬性就儲存了lzc.spidemo.impl.BCCarlzc.spidemo.impl.BMCar這兩個值,然後取出lzc.spidemo.impl.BCCar賦值給LazyIterator的成員變數nextName

執行完LazyIteratorhasNext方法後,會繼續執行LazyIteratornext方法

LazyIterator.next()

檢視LazyIterator.next()方法

// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
public S next() {
    // 預設情況下 acc = null
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

檢視LazyIterator.nextService()方法

// java.util.ServiceLoader.LazyIterator
// LazyIterator 是 ServiceLoader 的內部類
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    // hasNextService()方法中為 nextNam e賦值過服務提供者實現類的全限定名
    String cn = nextName;
    // 將 nextName 設定為空
    // 下次呼叫 hasNextService() 時會從 pending 獲取值並賦值給 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());
        // 將例項化後的服務提供者實現類放進providers集合進行快取
        providers.put(cn, p);
        // 返回例項
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

LazyIterator.nextService()利用Class.forName(String name, boolean initialize,ClassLoader loader)載入服務提供者實現類,然後利用c.newInstance()例項化,把例項化的類儲存到providers中快取起來。

JDBC驅動載入是利用了Java的SPI機制。JDBC提供了一組介面規範,不同的資料庫廠商只要編寫符合這套JDBC介面規範的驅動程式碼,那麼就可以用Java語言來連線資料庫。

以MySQL為例分析JDBC驅動載入原始碼。

首先需要引入mysql-connector-java依賴包,版本為8.0.20。mysql-connector-java.jar包目錄下的META-INF/services/下有一個java.sql.Driver檔案,檔案內容如下所示:

com.mysql.cj.jdbc.Driver

載入MySQL驅動類

public class DBUtils {
    private static String dirverClassName = "com.mysql.cj.jdbc.Driver";
    private static String url = "jdbc:mysql://127.0.0.1:3306/lzc?characterEncoding=utf8";
    private static String user = "root";
    private static String password = "root";
    public static Connection getDBConnection() {
        Connection conn = null;
//        try {
//            // 在JDBC 4.0 規範中可以省略這一步了  
//            // Class.forName 用來載入類資訊並執行類的靜態塊
//            Class.forName(dirverClassName);
//        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
//        }
        try {
            // 透過 DriverManager 獲取 Connection
            conn = DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}

從上面的程式碼可以發現,透過DriverManager.getConnection(String url,String user, String password)可以獲取Connection連線資訊,此時肯定會執行DriverManager的靜態塊程式碼。

檢視java.sql.DriverManager靜態程式碼:

// java.sql.DriverManager
public class DriverManager {
    // 用來儲存JDBC驅動列表
    // 就是實現了 java.sql.Driver 介面的類在類載入時會主動註冊到 registeredDrivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

檢視loadInitialDrivers()方法

// java.sql.DriverManager
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;
    }

    // 重點看這裡
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 前面已經分析過這裡的原理了
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 呼叫 ServiceLoader 的 iterator 方法
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                // 在迭代的時候會去載入META-INF/services/java.sql.Driver檔案,
                // 檔案內容為 com.mysql.cj.jdbc.Driver
                // 然後例項化 com.mysql.cj.jdbc.Driver
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                          ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

這裡主要是利用了Java的SPI機制去例項化MySQL驅動類,下面檢視MySQL驅動類是如何註冊到DriverManager類的registeredDrivers集合中

驅動類註冊到DriverManager

檢視com.mysql.cj.jdbc.Driver的靜態塊程式碼

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            // 這裡會將 com.mysql.cj.jdbc.Driver 例項註冊到
            // DriverManager類的registeredDrivers集合中
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

看到這裡可以知道,com.mysql.cj.jdbc.Driver在例項化的時候會執行它的靜態塊程式碼,此時會將自己註冊到DriverManager類的registeredDrivers集合中。

MySQL驅動連線資料庫

com.mysql.cj.jdbc.Driver已經註冊到DriverManager類的registeredDrivers集合中,下面檢視它是何時被呼叫的。

透過前面的例子可以知道,透過DriverManager.getConnection(String url,String user, String password)可以獲取Connection連線資訊,檢視這個方法的程式碼:

// java.sql.DriverManager
public static Connection getConnection(String url, String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();
    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }
    return (getConnection(url, info, Reflection.getCallerClass()));
}

繼續往下看

// java.sql.DriverManager
private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    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;

    // 主要檢視這裡
    // 遍歷 registeredDrivers 集合
    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);
                // 只要連線上就直接放回 Connection,如果有多個驅動類,其餘的就會被忽略
                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");
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章