1. 程式人生 > 其它 >以JDBC為例聊:執行緒ClassLoader切換

以JDBC為例聊:執行緒ClassLoader切換

以JDBC為例談雙親委派模型的破壞


java本身有一套資源管理服務JNDI,是放置在rt.jar中,由啟動類載入器載入的。以對資料庫管理JDBC為例,
java給資料庫操作提供了一個Driver介面:

public interface Driver {
   
    Connection connect(String url, java.util.Properties info)   throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)     throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

然後提供了一個DriverManager來管理這些Driver的具體實現:

public class DriverManager {

    // List of registered JDBC drivers 這裡用來儲存所有Driver的具體實現
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    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);

    }
    

}

這裡省略了大部分程式碼,可以看到我們使用資料庫驅動前必須先要在DriverManager中使用registerDriver()註冊,然後我們才能正常使用。

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

我們看下mysql的驅動是如何被載入的:

         // 1.載入資料訪問驅動
        Class.forName("com.mysql.jdbc.Driver");
        //2.連線到資料"庫"上去
        Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

核心就是這句Class.forName()觸發了mysql驅動的載入,我們看下mysql對Driver介面的實現:

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

可以看到,Class.forName()其實觸發了靜態程式碼塊,然後向DriverManager中註冊了一個mysql的Driver實現。
這個時候,我們通過DriverManager去獲取connection的時候只要遍歷當前所有Driver實現,然後選擇一個建立連線就可以了。

2、破壞雙親委派模型的情況

在JDBC4.0以後,開始支援使用spi的方式來註冊這個Driver,具體做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 檔案中指明當前使用的Driver是哪個,然後使用的時候就直接這樣就可以了:

 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

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

  • 第一,從META-INF/services/java.sql.Driver檔案中獲取具體的實現類名“com.mysql.jdbc.Driver”
  • 第二,載入這個類,這裡肯定只能用class.forName("com.mysql.jdbc.Driver")來載入

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

那麼,這個問題如何解決呢?按照目前情況來分析,這個mysql的drvier只有應用類載入器能載入,那麼我們只要在啟動類載入器中有方法獲取應用程式類載入器,然後通過它去載入就可以了。這就是所謂的執行緒上下文載入器。
執行緒上下文類載入器可以通過Thread.setContextClassLoaser()方法設定,如果不特殊設定會從父類繼承,一般預設使用的是應用程式類載入器

很明顯,執行緒上下文類載入器讓父級類載入器能通過呼叫子級類載入器來載入類,這打破了雙親委派模型的原則

現在我們看下DriverManager是如何使用執行緒上下文類載入器去載入第三方jar包中的Driver類的。

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
        }

        //省略程式碼
    }
}

使用時,我們直接呼叫DriverManager.getConn()方法自然會觸發靜態程式碼塊的執行,開始載入驅動
然後我們看下ServiceLoader.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);
    }

可以看到核心就是拿到執行緒上下文類載入器,然後構造了一個ServiceLoader,後續的具體查詢過程,我們不再深入分析,這裡只要知道這個ServiceLoader已經拿到了執行緒上下文類載入器即可。
接下來,DriverManager的loadInitialDrivers()方法中有一句driversIterator.next();,它的具體實現如下:

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //此處的cn就是產商在META-INF/services/java.sql.Driver檔案中註冊的Driver具體實現類的名稱
               //此處的loader就是之前構造ServiceLoader時傳進去的執行緒上下文類載入器
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
         //省略部分程式碼
        }

現在,我們成功的做到了通過執行緒上下文類載入器拿到了應用程式類載入器(或者自定義的然後塞到執行緒上下文中的),同時我們也查詢到了廠商在子級的jar包中註冊的驅動具體實現類名,這樣我們就可以成功的在rt.jar包中的DriverManager中成功的載入了放在第三方應用程式包中的類了。

3、總結

這個時候我們再看下整個mysql的驅動載入過程:

  • 第一,獲取執行緒上下文類載入器,從而也就獲得了應用程式類載入器(也可能是自定義的類載入器)
  • 第二,從META-INF/services/java.sql.Driver檔案中獲取具體的實現類名“com.mysql.jdbc.Driver”
  • 第三,通過執行緒上下文類載入器去載入這個Driver類,從而避開了雙親委派模型的弊端

很明顯,mysql驅動採用的這種spi服務確確實實是破壞了雙親委派模型的,畢竟做到了父級類載入器載入了子級路徑中的類。