1. 程式人生 > 實用技巧 >java spi機制詳解

java spi機制詳解

1.什麼是spi?

SPI 全稱為 (Service Provider Interface) ,是JDK內建的一種服務提供發現機制。SPI是一種動態替換髮現的機制, 比如有個介面,想執行時動態的給它新增實現,你只需要新增一個實現。我們經常遇到的就是java.sql.Driver介面,其他不同廠商可以針對同一介面做出不同的實現,mysql和postgresql都有不同的實現提供給使用者,而Java的SPI機制可以為某個介面尋找服務實現。

如上圖所示,介面對應的抽象SPI介面;實現方實現SPI介面;呼叫方依賴SPI介面。

SPI介面的定義在呼叫方,在概念上更依賴呼叫方;組織上位於呼叫方所在的包中,實現位於獨立的包中。

當服務的提供者提供了一種介面的實現之後,需要在classpath下的META-INF/services/目錄裡建立一個以服務介面命名的檔案,這個檔案裡的內容就是這個介面的具體的實現類。當其他的程式需要這個服務的時候,就可以通過查詢這個jar包(一般都是以jar包做依賴)的META-INF/services/中的配置檔案,配置檔案中有介面的具體實現類名,可以根據這個類名進行載入例項化,就可以使用該服務了。JDK中查詢服務實現的工具類是:java.util.ServiceLoader。

2.spi的用途

資料庫DriverManager、Spring、ConfigurableBeanFactory等都用到了SPI機制,這裡以資料庫DriverManager為例,看一下其實現的內幕。

DriverManager是jdbc裡管理和註冊不同資料庫driver的工具類。針對一個數據庫,可能會存在著不同的資料庫驅動實現。我們在使用特定的驅動實現時,不希望修改現有的程式碼,而希望通過一個簡單的配置就可以達到效果。 在使用mysql驅動的時候,會有一個疑問,DriverManager是怎麼獲得某確定驅動類的?

  先來看看我們平時用的Class.forName("com.mysql.jdbc.Driver")方法,該方法在此處返回com.mysql.jdbc.Driver物件,並執行其中的靜態方法把driver註冊到DriverManager中,以便後續的使用。

driver實現

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

驅動的類的靜態程式碼塊中,呼叫DriverManager的註冊驅動方法new一個自己當引數傳給驅動管理器。

jdk中DriverManager實現

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

可以看到其內部的靜態程式碼塊中有一個loadInitialDrivers方法,loadInitialDrivers用法用到了上文提到的spi工具類ServiceLoader:

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

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

  先查詢jdbc.drivers屬性的值,然後通過SPI機制查詢驅動,並且在上述原始碼中

可以看到載入META-INF/services/ 資料夾下類名為檔名(這裡相當於Driver.class.getName())的資源,然後將其載入到虛擬機器。

註釋有這麼一句“Load these drivers, so that they can be instantiated.” 意思是載入SPI掃描到的驅動來觸發他們的初始化。即觸發他們的static程式碼塊

public final class ServiceLoader<S>
    implements Iterable<S>
{

    private static final String PREFIX = "META-INF/services/";

  最後我們再來看下mysql-connector.jar包下的檔案,有圖有真相:

檔名為jdk下的java.sql.driver介面的全限定名,檔案內容為mysql驅動包下的實現類com.mysql.cj.jdbc.Driver