jvm-sanbox詳解-SPI機制

什麼是SPI機制

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的接口。我們知道JDK代碼提供了大量的方便的工具類給我們使用,JDK會對經常使用接口進行抽象統一。如鏈接數據庫我們可以使用java.sql.DriverManager,但各種數據庫的實現各自不同,所以爲了給用戶統一使用,屏蔽底層各自難懂的細節,我們這種SPI機制產生了。

mysql加載驅動

回想我們經常使用mysql的過程

0.配置文件:
    url: jdbc:mysql://localhost:3306/xxxxx?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: xxx
    password: xxxxxx
1.註冊驅動(現在已經不需要這一步加載了,寫了也兼容不報錯)
Class.forName("com.mysql.jdbc.Driver");
2.創建連接:
conn = (Connection) DriverManager.getConnection(url,username,password);
3.執行sql
stat = conn.createStatement();
String sql = "SELECT * FROM tb_person";
ResultSet rs = stat.executeQuery(sql);

讓我們看看java.sql.DriverManager做了什麼事情

//類初始化代碼加載驅動
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}


private static void loadInitialDrivers() {
    String drivers;
    try {
        //加載系統屬性jdbc.drivers指定的drivers
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    
    //如果driver驅動是Service Provider形式的,直接加載,並且替換上面系統屬性指定方式
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            
            //使用ServiceLoader加載Driver
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            //遍歷driver,driver有可能不存在或者配置錯誤,直接catch異常並忽略
            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);
        }
    }
}

java.sql.Driver.class 就定義了幾個接口,實現都交給廠商,然後我們看ServiceLoader幹了個啥

public final class ServiceLoader<S>
    implements Iterable<S>
{
	//這裏配置寫死 SPI機制加載配置文件的路徑
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
    
    //DriverManager.load調用此方法
    public static <S> ServiceLoader<S> load(Class<S> service) {
        //注意這裏使用的線程上下文類加載器,加載這個類的是系統類加載器,因此加載驅動是使用系統類加載器來加載的,打破了類加載器的雙親委託機制。
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    //真實調用的方法,初始化賦值
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //保留加載的類
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //保留加載的classloader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    //DriverManager加載完Driver.class後,調用此方法
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            //上面聲明的屬性providers是已加載的驅動   
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            
            //下面依次iterator依次優先遍歷已加載,其次是懶加載lookupIterator
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

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

        };
    }
}

懶加載中重點方法如下:JDK源碼java.util.ServiceLoader.LazyIterator

//核心邏輯:解析SPI位置的配置文件,確定實現接口的類
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //如sql的驅動:META-INF/services/java.sql.Driver
            //這裏就是SPI的核心實現,從這個文件位置加載實現java.sql.Driver接口的class類
            String fullName = PREFIX + service.getName();
            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;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
//根據上個方法實現的類,去初始化類,並保存到providers中
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = 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.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

讓我們看看Service Provider的概念

Files in the META-INF/services directory are service provider configuration files. A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers may be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers may also be made available by adding them to the applet or application class path or by some other platform-specific means.
A service is represented by an abstract class. A provider of a given service contains one or more concrete classes that extend this service class with data and code specific to the provider. This provider class will typically not be the entire provider itself but rather a proxy that contains enough information to decide whether the provider is able to satisfy a particular request together with code that can create the actual provider on demand. The details of provider classes tend to be highly service-specific; no single class or interface could possibly unify them, so no such class has been defined. The only requirement enforced here is that provider classes must have a zero-argument constructor so that they may be instantiated during lookup.

簡述就是在jar包 META-INF/services目錄下定義一個文件,文件名就是接口或者抽象類,文件內容是定義的實現類,可以有多個實現類。如mysql驅動包 META-INF/services/java.sql.Driver 這個文件下有兩個驅動

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

那麼mysql如何知道加載哪個驅動呢,我們來看 java.sql.DriverManager#getConnection 方法

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

至此,驅動的加載就徹底完成。回顧一下JDBC鏈接MySql過程:JDK使用了SPI的機制,讓不同服務提供方提供服務,註冊驅動,在使用驅動的時候,遍歷所有註冊過的驅動,並嘗試創建鏈接,鏈接成功就返回。

SPI機制使用限制

  • 需要啓動加載配置,所以在運行時無法增加新的服務(猜測更多是爲了安全,因爲三方服務如mysql都是通過系統類加載器加載的)

SPI註解

我們在使用SPI特性的時候需要寫 /META-INF/services/xxx 文件,手動將SPI的配置寫入配置文件很是不方便,我們可以使用 @MetaInfServices註解
引入包

<dependency>
  <groupId>org.kohsuke.metainf-services</groupId>
  <artifactId>metainf-services</artifactId>
  <version>xxx</version>
  <optional>true</optional>
</dependency>

總結/回顧

  • 代碼編寫要點:
    • 實現某個接口類,接口類可以是JDK源碼中的,也可以是自定義的(如sl4j日誌打印門面)
    • 打包時需要添加 /META-INF/services/xx 文件,xx文件名是接口名,文件內容是實現的接口類
  • 加載邏輯要點:
    • 對於JDK原生接口類,會使用系統類加載器來加載,而不是用戶類加載器加載,打破了類加載器的雙親委派模型
    • 爲防止service的無效加載,JDK採用懶加載(同時給我們提供了懶加載的方法),只有真正使用驅動的時候加載

使用方式以及參考地址:http://metainf-services.kohsuke.org/index.html

參考文檔

serviceloader:https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html
SPI說明以及官方示例:https://docs.oracle.com/javase/tutorial/ext/basics/spi.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章