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中
/** * 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(); /* 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機制查詢驅動,並且在上述原始碼中
註釋有這麼一句“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