7. 通過JDBC源碼來分析線程上下文類加載器以及SPI的使用


聲明:本人寫的這些,只是當時學習這些知識的理解,不代表是正確的,只是為了鞏固記憶,好記憶不如爛筆頭,如果哪里有問題,請指出,不喜勿噴,謝謝。

1. 什么是全盤負責委托機制

每個類都有自己的類加載器,那么負責加載當前類的類加載器也會去加載當前類中引用的其他類,前提是引用的類沒有被加載過
例如ClassA中有個變量 ClassB,那么加載ClassA的類加載器會去加載ClassB,如果找不到ClassB,則異常。

2. 為什么需要有線程上下文類加載器

jvm推薦我們使用雙親委托機制,主要是保證了相同的類不會被重復加載。但是,在jdk1.2之后,提出了線程上下文類加載器的概念,目的是為了打破雙親委托機制,因為在某些場景下(例如:JNDI,JDBC....)等等SPI場景中,關於什么是SPI(服務發現接口),可以參考之前寫的文檔資料(https://www.cnblogs.com/duguxiaobiao/p/12183135.html),使用雙親委托機制無法實現,那么為什么無法實現呢?

2.1 使用JDBC的例子,分析為什么雙親委托機制不能實現要求

原生的JDBC的使用,獲取數據庫連接使用的是 Connection conn = DriverManager.getConnection(xx,xx,xx);很明顯,Connection是jdk提供的接口,具體的實現是我們的廠商例如mysql 實現,加入到項目中,那么設想一下,DriverManager.getConnection(xx,xx,xx);該方法肯定是使用的mysql的jar包,返回了mysql實現的Connection對象,那么加載DriverManager類是由啟動類加載器加載,根據上面的全盤負責委托機制來說,啟動類加載器會去加載MySql的jar包,很明顯,找不到。所以使用雙親委托機制來說,無法實現該SPI場景的需求。

2.2 線程上下文類加載器的作用

雙親委托機制:子加載器對應的命名空間包含了父加載器,所以可以實現子容器訪問父容器
線程上下文類加載器:使用該類加載器,可以實現 父容器訪問子容器場景,主要設置好上下文類加載器即可。

3. 線程上下文類加載器的使用

3.1 線程上下文類加載器使用API

  1. 獲取當前線程的上下文類加載器:Thread.currentThread().getContextClassLoader();
  2. 設置當前線程的上下文類加載器:Thread.currentThread().setContextClassLoader(ClassLoader cl);

3.2 線程上下文類加載器的特征

  1. 如果沒有設置 setContextClassLoader(),那么線程將繼承父線程的上下文類加載器,這段可以通過Thread.init()方法中可以看出
  2. Java應用運行時初始上下文類加載器是系統類加載器,可以在源碼:Launcher類的構造方法中,在實例化系統類加載器后,將之設置為上下文類加載器。

3.3 線程上下文類加載器使用的通用寫法

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(null);
    //其他執行事件
    doSomthing();
} finally {
    Thread.currentThread().setContextClassLoader(contextClassLoader);
}

4. 借助JDBC源碼分析上下文類加載器的使用

4.1 示例代碼

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("xxx", "xx", "xx");

4.2 源碼分析

4.2.1 首先解析第一句 Class.forName("com.mysql.jdbc.Driver");

這里就不介紹了Class.forName()源碼了,這行代碼表示初始化 Driver類,對Driver類的主動使用,就會導致Driver的靜態代碼塊執行,那么我們進入到Driver類中,看是否有需要初始化調用的靜態代碼塊。

public Driver() throws SQLException {
}

static {
    try {
        DriverManager.registerDriver(new Driver());
    } catch (SQLException var1) {
        throw new RuntimeException("Can't register driver!");
    }
}

可以看到,存在靜態代碼塊,那么進入到靜態代碼塊中,我們來解析 DriverManager.registerDriver(new Driver());

4.2.2 DriverManager.registerDriver(new Driver());

可能有的朋友查看該代碼源碼時,直接就會進入到DriverManager類中查看registerDriver(),其實跟解析第一行一行,主動調用類的靜態方法,會導致累的初始化,執行 DriverManager中的靜態代碼塊。所以我們需要先看下面的源碼:

static {
    //首先會添加初始化加載drivers,這里引入了ServiceLoader
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

//因為我們沒有設置 jdbc.drivers屬性,所以這里只展示關鍵代碼,對於其他不影響流程的代碼有所刪減,具體的可以看源碼
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() {
            //主要的引入各個廠家的Driver類是的服務是在這里加入的
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
              }
            return null;
        }
    });
}

如果熟悉針對SPI場景的服務加載方式ServiceLoader使用的,當看到這經典的幾行的代碼時,就知道具體的加載方式了,如果有對ServiceLoader或者SPI不是很熟悉的,可以先閱讀或者百度下相關文檔(https://www.cnblogs.com/duguxiaobiao/p/12183135.html),那么通過下面這行代碼就可以將mysql依賴加載到內存中了。

ServiceLoader<Driver> loadedDrivers =  ServiceLoader.load(Driver.class);

那么是怎么在DriverManager對應的類加載器啟動類加載器中加載到mysql jar包的呢,下面來分析 ServiceLaoder.load()方法,go

4.2.3 ServiceLaoder.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);
}

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 class LazyIterator implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

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

    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
  }

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

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

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

}

查閱源碼,我們可以看出當我們執行 ServiceLaoder.load()的時候,首先會獲取當前線程的上下文類加載器。而且在構造方法中也可以看到,如果獲取的上下文類加載器為空時,也會使用默認的系統類加載器,而默認設置當前線程的上下文類加載器的時候,默認運行時也是系統類加載器作為上下文類加載器,所以先肯定一點,后續加載類的類加載器肯定是 系統類加載器。

致此,通過 ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);代碼我們可以得到待加載的類的父類是Driver類,以及使用系統類加載器來負責加載,這就可以解釋了為什么啟動類加載器可以加載mysqljar了,因為是使用了系統類加載器來加載的,沒有問題,且創建了一個 LazyIterator對象。下面我們將分析如何找到對應的mysql的針對Driver的實現類的。

Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
    // Do nothing
}

4.2.4 driversIterator.hasNext(); driversIterator.next();

從ServiceLoader.iterator()源碼中可以看出, driversIterator.hasNext()其實是調用了 load()時候創建的 LazyIterator.hasNext()方法

public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

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

    };
}

LazyIterator.hasNext() 最終調用了 LazyIterator.hasNextService()方法,致此,我們就可以看到ServiceLoader是如何在指定目錄下獲取到指定類名對應的實現類全類名信息的。有興趣的可以看看。

我們的重點是在 LazyIterator.next()方法,該方法最終調用了 LazyIterator.nextService()方法,在該方法中我們可以看到如何將mysql的Driver實現類使用上下文類加載器所加載到內存中。

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    //在遍歷的時候獲取的當前item的文件內容
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //是否初始化參數為false,表示這里只加載類,不初始化
        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 {
        //這里才將類初始化,觸發mysql Driver類的靜態代碼塊
        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
}

上面代碼說簡單點,就是在 hasNext()中找到待實例化的全類名,而在 next()中實例化該類。

注意:這里有一個問題 我們在最之前的代碼中,使用了Class.forName()將mysql驅動的Driver類加載了,這里又重復了一次,那我們的第一步豈不是多余的操作。

這個疑問很好,也是正確的,所以在新版本的JDBC處理上,已經使用了SPI方式的ServiceLoader加載方式,不在需要第一步驟的手動加載初始化具體的驅動全類名了。

既然已經觸發了mysql Driver累的初始化,那么跟最開始一樣,不再重復,這回DriverManager靜態代碼塊已經執行完畢,可以真正的執行 DriverManager.registerDriver(new Driver());了,其實最終也是調用的 DriverManager.registerDriver()

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

}

從上述代碼上看,其實做的東西很簡單,就是判斷該Driver如果在緩存中沒有就添加到緩存中而已。
致此,第一行代碼解析完畢,后面的具體如何獲取數據庫連接的,跟當前文章想表達的偏離了,所以不再繼續了,over


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM