關於Java的SPI機制 機製的一些思考

2020-08-11 22:23:45

上文分析類載入機制 機製的文章中我們提高SPI(Service Provider Interface:服務提供者介面)。

SPI是什麼

我們知道在許多系統裡抽象的各個模組,往往有很多不同的實現方案,比如日誌模組,xml解析模組、jdbc模組的方案等。
在面向的物件的設計裡,我們推薦模組之間基於介面程式設計,模組之間不對實現類進行寫死。爲了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制 機製。在模組化設計中這個機制 機製尤其重要。
SPI就是提供這樣的一個機制 機製:爲某個介面尋找服務實現的機制 機製。它通過查詢classpath路徑下的META-INF/services資料夾下的組態檔,自動載入檔案裡所定義的類(jdbc的jar的classpath對應的META-INF/services/下就有這麼一個組態檔)。

用JDBC舉例分析一下呼叫流程

閒話少說,上程式碼:

// 1. 首先 載入sqlite需要的Driver並註冊到DriverManager中
Class.forName("org.sqlite.JDBC");

Connection connection = DriverManager.getConnection("jdbc:sqlite:test.db");

然後,看一下DriverManager.getConnection

 @CallerSensitive
 public static Connection getConnection(String url)
        throws SQLException {
    java.util.Properties info = new java.util.Properties();
    return (getConnection(url, info, Reflection.getCallerClass()));
}

繼續跟進

    //  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;
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }

        if (url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // 2. ServiceLoader.load載入
        ensureDriversInitialized();

        // 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());
                    // 3. 獲得對應的Connection類
                    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");
    }

我們看一下ensureDriversInitialized的實現,SPL的載入在這裏進行的

    /*
     * Load the initial JDBC drivers by checking the System property
     * jdbc.drivers and then use the {@code ServiceLoader} mechanism
     */
    private static void ensureDriversInitialized() {
        if (driversInitialized) {
            return;
        }

        synchronized (lockForInitDrivers) {
            if (driversInitialized) {
                return;
            }
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty(JDBC_DRIVERS_PROPERTY);
                    }
                });
            } 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() {
                    // SPL的載入在這裏進行的
                    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("")) {
                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);
                    }
                }
            }

            driversInitialized = true;
            println("JDBC DriverManager initialized");
        }
    }

最後通過**Connection con = aDriver.driver.connect(url, info);**獲得Connection範例。

總結

優點

  • SPI機制 機製的優勢是實現解耦,使得第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啓用框架擴充套件或替換框架元件。

缺點

  • 雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並範例化一遍。如果你並不想用某些實現類,它也被載入並範例化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
  • 多執行緒使用ServiceLoader類的範例是不安全的。

本文我們對SPI進行了分析,下一篇文章,我們將要對類載入機制 機製的實際應用熱載入進行分析。

參考:

https://www.jianshu.com/simple/upgrade
https://www.jianshu.com/p/46b42f7f593c