SPI機制剖析——基於DriverManager+ServiceLoader的原始碼分析

數小錢錢的種花兔發表於2020-12-19

我的上一篇部落格類載入器與雙親委派中提到,SPI機制是一種上級類載入器呼叫下級類載入器的情形,因此會打破類載入的雙親委派模型。為了深入理解其中的細節,本部落格詳細剖析一下SPI機制,並以JDBC為例,基於原始碼來進行分析。

SPI

原理介紹

SPI(Service Provider Interface),是JDK內建的服務提供發現機制。即JDK內部定義規範的介面,不同廠商基於標準服務介面實現具體的實現類和方法。SPI一般被用來做框架擴充套件的開發。
下面這張圖,很簡明扼要地闡釋了SPI的機理。
SPI機制
與SPI相對應的,是我們耳熟能詳的API。API不需要上圖中“標準服務介面”這一環節,而是呼叫方直接呼叫服務提供方。按照上一篇部落格的分析,“標準服務介面”位於Java核心類庫中,使用boot類載入器進行載入,而boot類載入器是無法獲取“第三方實現類”的位置的。所以,相較於API而言,SPI需要打破雙親委派模型。

優缺點

好處

但是,我陷入思考,SPI這樣的模式有什麼好處嗎,或者說API有什麼缺點嗎?

想象一下,如果程式直接呼叫第三方類庫,當第三方類庫發生改動時,應用程式程式碼很可能需要隨之改動。但如果在JDK內部定義標準服務介面,要求第三方廠商實現這些介面,那無論實現類如何改動,只要標準介面不變,都不會影響到應用程式。所以我認為SPI機制的根本目的是為了“解耦”。這也就是物件導向中所謂的“介面程式設計”,把裝配的控制權移到程式之外。

許多著名的第三方類庫都採納了SPI機制,JDBC就是其中之一。資料庫廠商會基於標準介面來開發相應的連線庫。如MySQL何PostgreSql的驅動都實現了標準介面:java.sql.Driver。對於應用程式而言,無需關心是MySQL還是PostgreSql,只需要與標準服務介面打交道即可。SPI正是基於這種模式完成了解耦合。

不足

當然,即便如此,SPI依舊是存在缺點和不足的,如下:

  1. 不能按需載入。需要遍歷所有的實現,並且進行例項化,某些實現的例項化可能很耗時,這樣會造成浪費;
  2. 獲取實現類的方式不夠靈活,只能通過Iterator獲取,不能根據某個引數來獲取實現類;
  3. ServiceLoader類的例項執行緒不安全。

JDBC的SPI機制

首先來看一段使用JDBC的簡單程式碼:

@Test
public void testJDBC() throws SQLException, ClassNotFoundException {
    String url = "jdbc:mysql://localhost:3307/mls";
    String userName = "root";
    String password = "123456";
    // Class.forName("com.mysql.cj.jdbc.Driver");
    Connection con = DriverManager.getConnection(url, userName, password);
    Statement statement = con.createStatement();
    String sql = "select * from mlsdb where id=1";
    ResultSet rs = statement.executeQuery(sql);
    while (rs.next()) {
        System.out.println(rs.getString("province"));
    }
}

注意到中間有一行註釋的程式碼Class.forName("com.mysql.cj.jdbc.Driver");,其實這一行可寫可不寫。

我的倒數第二篇部落格類載入時機與過程裡提到,Class.forName方法會觸發“初始化”,即觸發類載入的進行。因此如果寫上這行程式碼,此處則是使用APP類載入器載入mysql的jdbc驅動類。

然而,這一句Class.forName不用寫,程式碼也能正常執行。因為載入DriverManager類時,會將MySQL的Driver物件註冊進DriverManager中。具體流程後文會細說。其實這就是SPI思想的一個典型的實現。得益於SPI思想,應用程式中無需指定類似"com.mysql.cj.jdbc.Driver"這種全類名,儘可能地將第三方驅動從應用程式中解耦出來。

下面,通過原始碼來分析驅動載入以及服務發現的過程,主要涉及到DriverManager和ServiceLoader兩個類

原始碼分析

DriverManager是用於管理Jdbc驅動的基礎服務類,位於Java.sql包中,因此是由boot類載入器來進行載入。載入該類時,會執行如下程式碼塊:

/**
 * 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()方法,該方法用於載入各個資料庫驅動。程式碼如下:

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

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//例項化ServiceLoader物件,並注入執行緒上下文類載入器和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);
        }
    }
}
  • ServiceLoader.load(Driver.class)此方法會把例項化一個ServiceLoader物件,並且向其注入執行緒上下文類載入器和Driver.class;
  • loadedDrivers.iterator():獲得ServiceLoader物件的迭代器;
  • driversIterator.hasNext():查詢Driver類;
  • driversIterator.next():在實現的“next()”方法中進行類載入,使用上面的執行緒上下文類載入器。

ServiceLoader.load(Driver.class);的程式碼及相關呼叫方法如下:

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

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

經過上述過程,用成員變數private final ClassLoader loader;引用傳入的類載入器,用service接收Driver.class。同時,上述過程中例項化了一個LazyIterator物件,並用成員變數lookupIterator來引用。
執行ServiceLoader的“hasNext()”方法時最終會呼叫lookupIterator迭代器的“hasNext()”方法(此處暫且省略呼叫過程),如下:

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            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;
}
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

上述過程通過configs = loader.getResources(fullName)來查詢實現Driver介面的類。

同樣,ServiceLoader的迭代器的“next()”方法最終會呼叫lookupIterator迭代器的“next()”方法,如下:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);//使用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
}

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

可以看到,next()會最終呼叫到nextService()方法,並在此方法中通過c = Class.forName(cn, false, loader);執行類載入。此處的loader也是由ServiceLoader中的loader傳入的,即為前文提到的執行緒上下文類載入器。

經歷了上述ServiceLoader類中一系列操作之後(包括服務發現和類載入),位於mysql驅動包中的Driver類會被初始化。該類如下所示

package com.mysql.cj.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!");
        }
    }
}

上述Driver類載入時,會執行靜態程式碼塊,即執行DriverManager.registerDriver(new Driver());方法向DriverManager中註冊一個Driver例項。
我們再回到DriverManager類中,看看registerDriver方法:

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

}

會將該MySQL驅動新增到成員變數registeredDrivers中,該成員變數存放已註冊的jdbc驅動列表,如下:

// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

如此一來,服務發現、類載入、驅動註冊便到此結束。接下來,應用程式執行資料庫連線操作時,會呼叫“getConnection”方法,遍歷registeredDrivers,獲取驅動,建立資料庫連線。

總結

以上便是JDBC的SPI機制總結,最核心的地方在於,ServiceLoader中使用低階別的載入器發現Driver類,並進行類載入。這些工作是BootStrap類載入器所辦不到的。由於DriverManager和ServiceLoader都位於Java核心類庫中,使用BootStrap類載入器來載入,所以需要通過執行緒上下文類載入器向ServiceLoader物件中傳入一個低階別的類載入器,如系統類載入器,從而來打破雙親委派機制。

相關文章