天天看點

「jvm」通過JDBC為例談談雙親委派模型的破壞

我們都知道類加載的雙親委派模型

「jvm」通過JDBC為例談談雙親委派模型的破壞

雙親委派模型并不是一個強制限制模型,而是java設計者推薦給開發者的類加載實作方式;但是也會有例外; 今天我們主要來講一講 類似于SPI這種設計導緻的雙親委派模型被“破壞”的情況;

1JDBC

不破壞雙親委派模型的情況(不使用JNDI服務)

// 1.加載資料通路驅動
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2.連接配接到資料"庫"上去
        Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
           

Class.forName("com.mysql.cj.jdbc.Driver"); 這句會主動去加載類com.mysql.cj.jdbc.Driver

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}
           

可以看到,Class.forName()其實觸發了靜态代碼塊,然後向DriverManager中注冊了一個mysql的Driver實作。 這個時候,我們通過DriverManager去擷取connection的時候隻要周遊目前所有Driver實作,然後選擇一個建立連接配接就可以了

破壞雙親委派模型的情況

在JDBC4.0以後,開始支援使用spi的方式來注冊這個Driver,具體做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 檔案中指明目前使用的Driver是哪個,然後使用的時候就直接這樣就可以了
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
           

可以看到這裡直接擷取連接配接,省去了上面的Class.forName()注冊過程。 現在,我們分析下看使用了這種spi服務的模式原本的過程是怎樣的:

「jvm」通過JDBC為例談談雙親委派模型的破壞

在這裡插入圖檔描述

  1. 從META-INF/services/java.sql.Driver檔案中擷取具體的實作類名“com.mysql.cj.jdbc.Driver”
  2. 加載這個類,用class.forName("com.mysql.jdbc.Driver")來加載

Class.forName()加載用的是調用者的Classloader, 這個調用者DriverManager是在rt.jar中的,ClassLoader是啟動類加載器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,是以肯定是無法加載mysql中的這個類的。這就是雙親委派模型的局限性了,父級加載器無法加載子級類加載器路徑中的類。

如何解決父加載器無法加載子級類加載器路徑中的類

我們分析一下,想要正常的加載,啟動類加載器肯定不能加載,那麼隻能用應用類加載器能夠加載,那麼如果有什麼辦法能夠擷取到應用類加載器就可以解決問題了;我們看看 jdk是怎麼做的;

線程上下文類加載器

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        //省略代碼
        //這裡就是查找各個sql廠商在自己的jar包中通過spi注冊的驅動
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
             while(driversIterator.hasNext()) {
                driversIterator.next();
             }
        } catch(Throwable t) {
                // Do nothing
        }

        //省略代碼
    }
}
           

看這裡,加載的時候去擷取了一個加載器

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
           

擷取線程上下文類加載器Thread.currentThread().getContextClassLoader(); 這個值如果沒有特定設定,一般預設使用的是應用程式類加載器;

總結

為了實作SPI這種模式,實作可插拔 做出了不符合雙親委派原則行為,但是這種破壞并不具備貶義的感情色彩,隻要有足夠意義和理由,突破已有的原則就可以認為是一種創新;

對于線程上下文類加載器 的實作類似于ThreadLocal 将變量傳遞到整個線程的生命周期; 這裡無非就是将ThreadLocal裡面存放的是應用類加載器;

繼續閱讀