Java的SPI機制

SPI是什么

SPI的英文名稱是Service Provider Interface,是Java 內置的服務發現機制。

在開發過程中,將問題進抽象成API,可以為API提供各種實現。如果現在需要對API提供一種新的實現,我們可以不用修改原來的代碼,直接生成新的Jar包,在包里提供API的新實現。通過Java的SPI機制,可以實現了框架的動態擴展,讓第三方的實現能像插件一樣嵌入到系統中。

Java的SPI類似于IOC的功能,將裝配的控制權移到了程序之外,實現在模塊裝配的時候不用在程序中動態指明。所以SPI的核心思想就是解耦,這在模塊化設計中尤其重要。

SPI使用示例

SPI使用方法并不復雜,只需要簡單的3步就可以搞定。

  • 定義一個接口
  • 提供方''META-INF/services''目錄下新建一個名稱為接口全限定名的文件,內容為接口實現類的全限定名。
  • 調用方通過ServiceLoader.load方法加載接口的實現類實例

下面通過一個例子來具體演示下Java SPI是如何使用的,項目的工程結構如下:

(1) 「spi-api」項目

  • 定義接口類,SpiService.java:
package com.gallenzhang.spi;

/**
 * @author : zhangxq
 * @date : 2019/01/25
 * @description :
 */
public interface SpiService {

    void sayHello(String name);
}

(2) 「spi-service-a」項目

  • SpiService實現類SpiServiceImplA.java
package com.gallenzhang.spi.service;

import com.gallenzhang.spi.SpiService;

/**
 * @author : zhangxq
 * @date : 2019/01/25
 * @description :
 */
public class SpiServiceImplA implements SpiService {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "! from service-a");
    }
}

  • resources下創建 "META-INF/service"目錄,并在該目錄下新建com.gallenzhang.spi.SpiService文件,文件內容為:
com.gallenzhang.spi.service.SpiServiceImplA

「spi-service-b」項目與「spi-service-a」項目類似,這里就不再贅述。

(3) 「spi-application」項目

  • SpiMain.java
package com.gallenzhang.spi;

import java.sql.SQLException;
import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * @author : zhangxq
 * @date : 2019/01/25
 * @description :Spi測試類
 */
public class SpiMain {
    public static void main(String[] args) throws SQLException {
        ServiceLoader<SpiService> loadedParsers = ServiceLoader.load(SpiService.class);
        Iterator<SpiService> iterator = loadedParsers.iterator();
        while (iterator.hasNext()){
            SpiService spiService = iterator.next();
            spiService.sayHello("gallenzhang");
        }
    }
}

(4) 運行結果如圖:



可以看到通過Java SPI成功的加載到了「spi-service-a」和「spi-service-b」中接口的實現類

應用場景

比較常見的應用場景:
JDK提供一個數據庫驅動接口類,JDBC加載不同的數據庫驅動實現類

日志門面接口實現類加載,SLF4J加載不同廠商提供的日志實現類。

這里以JDBC為例,看看SPI是如何自動加載驅動的。下面一段代碼大家都應該很熟悉了,首先加載驅動程序,然后獲取數據庫連接。

//加載驅動程序
//Class.forName("com.mysql.jdbc.Driver");
//獲取數據庫連接
Connection conn = DriverManager.getConnection(url, user, password);

這里首先要說明一下,使用mysql-connector-java連接數據庫,在5.1.6之前的版本都需要加上Class.forName("com.mysql.jdbc.Driver"); 但是從5.1.6版本以及后面的版本,這句代碼就可以去掉了。這是為什么呢?下面通過代碼來一探究竟。

public class DriverManager {
    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
    private 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");
    }

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

    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        registerDriver(driver, null);
    }

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);
    }
    
    @CallerSensitive
    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()));
    }

    //  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;
        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;

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

DriverManager.getConnection(url, user, password); 執行時首先執行靜態代碼塊,會調用loadInitialDrivers(); 這個方法能清楚看到這兒有段代碼,就是通過Java的SPI加載Driver接口的所有實例,并將實例初始化。mysql-connector-java 包中META-INF/services目錄下有個名為java.sql.Driver的文件,內容就是Driver接口的實現類。
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
  } catch(Throwable t) {
        // Do nothing
  }

當Driver 和 FabricMySQLDriver實例化的時候,會先執行靜態代碼塊,向DriverManager注冊一個自己的實例,在DriverManager中注冊的驅動信息都保存在registeredDrivers中。

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.getConnection方法真正調用的時候,就是遍歷registeredDrivers 中驅動信息,找到可以使用的驅動,拿到數據庫連接。

這里通過SPI機制成功的進行解耦,代碼中不再強制指定使用哪個驅動實現,而是將裝配的控制權移到了程序外,成功的做到了業務代碼和與第三方裝配邏輯分離。

優缺點

優點:使用Java SPI機制的優勢是實現了解耦,使第三方模塊的裝配邏輯與業務代碼分離。應用程序可以根據實際業務情況使用新的框架拓展或者替換原有組件。

缺點:ServiceLoader在加載實現類的時候會全部加載并實例化,假如不想使用某些實現類,它也會被加載示例化的,這就造成了浪費。另外獲取某個實現類只能通過迭代器迭代獲取,不能根據某個參數來獲取,使用方式上不夠靈活。

Dubbo框架中大量使用了SPI來進行框架擴展,但它是重新對SPI進行了實現,完美的解決上面提到的問題。

示例代碼

github地址:spi-parent

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 最近在閱讀Dubbo框架源代碼時,經常看到@Spi,查了一下SPI: Service Provider Inter...
    kobe0429閱讀 316評論 0 0
  • SPI的概念 英文全稱為Service Provider Interface 是JDK內置的一種服務提供發現機制 ...
    孫先森不可不弘毅閱讀 251評論 0 0
  • 當服務的提供者,提供了服務接口的一種實現之后,在jar包的META-INF/services/目錄里同時創建一個以...
    男人三餅閱讀 276評論 0 2
  • 本文通過探析JDK提供的,在開源項目中比較常用的Java SPI機制,希望給大家在實際開發實踐、學習開源項目提供參...
    caison閱讀 126,027評論 25 156
  • 本文通過探析JDK提供的,在開源項目中比較常用的Java SPI機制,希望給大家在實際開發實踐、學習開源項目提供參...
    簡祥閱讀 1,141評論 0 0