java類加載器

1、 ClassLoader 是做什么的

顧名思義件蚕,它是用來加載Class的孙技。它負責將Class 的字節(jié)碼形式轉(zhuǎn)換成內(nèi)存形式的Class對象。字節(jié)碼可以來自于磁盤文件 *.class排作,也可以是 jar 包里的 *.class牵啦,也可以來自遠程服務器提供的字節(jié)流,字節(jié)碼的本質(zhì)就是一個字節(jié)數(shù)組 []byte妄痪,它有特定的復雜的內(nèi)部格式哈雏。

image.png

有很多字節(jié)碼加密技術就是依靠定制 ClassLoader 來實現(xiàn)的。先使用工具對字節(jié)碼文件進行加密拌夏,運行時使用定制的 ClassLoader 先解密文件內(nèi)容再加載這些解密后的字節(jié)碼僧著。

每個 Class 對象的內(nèi)部都有一個 classLoader 字段來標識自己是由哪個 ClassLoader 加載的。ClassLoader 就像一個容器障簿,里面裝了很多已經(jīng)加載的 Class 對象盹愚。

class Class<T> {
  ...
  private final ClassLoader classLoader;
  ...
}

在虛擬機的角度上,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader)站故,這個類加載器使用C++語言實現(xiàn)皆怕,是虛擬機自身的一部分;另外一種就是其它所有的類加載器西篓,這些類加載器都由Java語言實現(xiàn)愈腾,獨立于虛擬機外部,并且全部繼承自java.lang.ClassLoader岂津。

從Java開發(fā)人員的角度看虱黄,類加載器還可以劃分得更細一些,如下:

1吮成、啟動類加載器 (Bootstrap ClassLoader):這個類加載器負責將放置在<JAVA_HOME>\lib目錄中的橱乱,或者被-Xbootclasspath參數(shù)所指定路徑中的辜梳,并且是虛擬機能識別的(僅按照文件名識別,如rt.jar泳叠,名字不符合的類庫即使放置在lib目錄中也不會被加載)類庫加載到虛擬機內(nèi)存中作瞄。啟動類加載器無法被Java程序直接使用。

2危纫、擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn)宗挥,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫种蝶,開發(fā)者可以直接使用擴展類加載器契耿。

3、應用程序類加載器(Application ClassLoader):這個類加載器由sum.misc.Launcher.$AppClassLoader來實現(xiàn)蛤吓。由于這個類加載器是ClassLoader.getSystemClassLoader()方法的返回值宵喂,所以一般也被稱為系統(tǒng)類加載器。它負責加載用戶類路徑上所指定的類庫会傲,開發(fā)者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器拙泽,一般情況下這個就是程序中默認的類加載器淌山。

2、class延遲加載

JVM 運行并不是一次性加載所需要的全部類的顾瞻,它是按需加載泼疑,也就是延遲加載。程序在運行的過程中會逐漸遇到很多不認識的新類荷荤,這時候就會調(diào)用 ClassLoader 來加載這些類退渗。加載完成后就會將 Class 對象存在 ClassLoader 里面,下次就不需要重新加載了蕴纳。

比如你在調(diào)用某個類的靜態(tài)方法時会油,首先這個類肯定是需要被加載的,但是并不會觸及這個類的實例字段古毛,那么實例字段可以暫時不必去加載翻翩,但是它可能會加載靜態(tài)字段,因為靜態(tài)方法會訪問靜態(tài)字段稻薇。而實例字段需要等到你實例化對象的時候才可能會加載嫂冻。

3、ClassLoaders各司其職

JVM 運行實例中會存在多個 ClassLoader塞椎,不同的 ClassLoader 會從不同的地方加載字節(jié)碼文件桨仿。它可以從不同的文件目錄加載,也可以從不同的 jar 文件中加載案狠,也可以從網(wǎng)絡上不同的服務地址來加載服傍。

JVM中內(nèi)置了三個重要的ClassLoader钱雷,分別是BootstrapClassLoaderExtensionClassLoaderAppClassLoader伴嗡。

  • BootstrapClassLoader 負責加載 JVM 運行時核心類急波,這些類位于 JAVA_HOME/lib/rt.jar 文件中,我們常用內(nèi)置庫 java.xxx.* 都在里面瘪校,比如 java.util.澄暮、java.io.、java.nio.阱扬、java.lang. 等等泣懊。這個 ClassLoader 比較特殊,它是由 C 代碼實現(xiàn)的麻惶,我們將它稱之為「根加載器」馍刮。
  • ExtensionClassLoader 負責加載 JVM 擴展類,比如 swing 系列窃蹋、內(nèi)置的 js 引擎卡啰、xml 解析器 等等,這些庫名通常以 javax 開頭警没,它們的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中匈辱,有很多 jar 包。
  • AppClassLoader 才是直接面向我們用戶的加載器杀迹,它會加載 Classpath 環(huán)境變量里定義的路徑中的 jar 包和目錄亡脸。我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來加載的。

那些位于網(wǎng)絡上靜態(tài)文件服務器提供的 jar 包和 class文件树酪,jdk 內(nèi)置了一個 URLClassLoader浅碾,用戶只需要傳遞規(guī)范的網(wǎng)絡路徑給構(gòu)造器,就可以使用 URLClassLoader 來加載遠程類庫了续语。

URLClassLoader 不但可以加載遠程類庫垂谢,還可以加載本地路徑的類庫,取決于構(gòu)造器中不同的地址形式绵载。ExtensionClassLoader 和 AppClassLoader 都是從本地文件系統(tǒng)里加載類庫埂陆。

AppClassLoader 可以由 ClassLoader 類提供的靜態(tài)方法 getSystemClassLoader() 得到,它就是我們所說的「系統(tǒng)類加載器」娃豹,我們用戶平時編寫的類代碼通常都是由它加載的焚虱。當我們的 main 方法執(zhí)行的時候,這第一個用戶類的加載器就是 AppClassLoader懂版。

4鹃栽、傳遞性

程序在運行過程中,遇到了一個未知的類,它會選擇由哪個 ClassLoader 來加載它呢民鼓?虛擬機的策略是使用調(diào)用者 Class 對象的 ClassLoader 來加載當前未知的類薇芝。何為調(diào)用者 Class 對象?就是在遇到這個未知的類時丰嘉,虛擬機肯定正在運行一個方法調(diào)用(靜態(tài)方法或者實例方法)夯到,這個方法掛在哪個類上面,那這個類就是調(diào)用者 Class 對象饮亏。前面我們提到每個 Class 對象里面都有一個 classLoader 屬性記錄了當前的類是由誰來加載的耍贾。

因為 ClassLoader 的傳遞性,所有延遲加載的類都會由初始調(diào)用 main 方法的這個 ClassLoader 全權(quán)負責路幸,它就是 AppClassLoader荐开。

5、雙親委派機制

前面我們提到 AppClassLoader 只負責加載 Classpath 下面的類庫简肴,如果遇到?jīng)]有加載的系統(tǒng)類庫怎么辦晃听,AppClassLoader 必須將系統(tǒng)類庫的加載工作交給 BootstrapClassLoader 和 ExtensionClassLoader 來做,這就是我們常說的「雙親委派」砰识。

image.png

AppClassLoader 在加載一個未知的類名時能扒,它并不是立即去搜尋 Classpath,它會首先將這個類名稱交給 ExtensionClassLoader 來加載辫狼,如果 ExtensionClassLoader 可以加載赫粥,那么 AppClassLoader 就不用麻煩了。否則它就會搜索 Classpath 予借。

而 ExtensionClassLoader 在加載一個未知的類名時,它也并不是立即搜尋 ext 路徑频蛔,它會首先將類名稱交給 BootstrapClassLoader 來加載灵迫,如果 BootstrapClassLoader 可以加載,那么 ExtensionClassLoader 也就不用麻煩了晦溪。否則它就會搜索 ext 路徑下的 jar 包瀑粥。

這三個 ClassLoader 之間形成了級聯(lián)的父子關系,每個 ClassLoader 都很懶三圆,盡量把工作交給父親做狞换,父親干不了了自己才會干。每個 ClassLoader 對象內(nèi)部都會有一個 parent 屬性指向它的父加載器舟肉。

值得注意的是圖中的 ExtensionClassLoader 的 parent 指針畫了虛線修噪,這是因為它的parent的值是null,當parent 字段是null 時就表示它的父加載器是「根加載器」路媚。如果某個Class對象的 classLoader 屬性值是null黄琼,那么就表示這個類也是「根加載器」加載的。

6整慎、 Class.forName方法

當我們在使用 jdbc驅(qū)動時脏款,經(jīng)常會使用 Class.forName 方法來動態(tài)加載驅(qū)動類围苫。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是mysql驅(qū)動的Driver類里有一個靜態(tài)代碼塊,它會在Driver 類被加載的時候執(zhí)行撤师。這個靜態(tài)代碼塊會將mysql驅(qū)動實例注冊到全局的jdbc驅(qū)動管理器里剂府。

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

forName 方法同樣也是使用調(diào)用者 Class 對象的 ClassLoader 來加載目標類。不過 forName 還提供了多參數(shù)版本剃盾,可以指定使用哪個ClassLoader 來加載

Class<?> forName(String name, boolean initialize, ClassLoader cl)

通過這種形式的 forName方法可以突破內(nèi)置加載器的限制腺占,通過使用自定義類加載器允許我們自由加載其它任意來源的類庫。根據(jù)ClassLoader的傳遞性万俗,目標類庫傳遞引用到的其它類庫也將會使用自定義加載器加載湾笛。

7、自定義類加載器

ClassLoader 里面有三個重要的方法 loadClass()闰歪、findClass() 和 defineClass()嚎研。

loadClass() 方法是加載目標類的入口,它首先會查找當前 ClassLoader 以及它的雙親里面是否已經(jīng)加載了目標類库倘,如果沒有找到就會讓雙親嘗試加載临扮,如果雙親都加載不了,就會調(diào)用 findClass() 讓自定義加載器自己來加載目標類教翩。ClassLoader 的 findClass() 方法是需要子類來覆蓋的杆勇,不同的加載器將使用不同的邏輯來獲取目標類的字節(jié)碼。拿到這個字節(jié)碼之后再調(diào)用 defineClass() 方法將字節(jié)碼轉(zhuǎn)換成 Class 對象饱亿。下面我使用偽代碼表示一下基本過程:

class ClassLoader {

  // 加載入口蚜退,定義了雙親委派規(guī)則
  Class loadClass(String name) {
    // 是否已經(jīng)加載了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交給雙親
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 雙親都不行,只能靠自己了
      t = this.findClass(name);
    }
    return t;
  }

  // 交給子類自己去實現(xiàn)
  Class findClass(String name) {
    throw ClassNotFoundException();
  }

  // 組裝Class對象
  Class defineClass(byte[] code, String name) {
    return buildClassFromCode(code, name);
  }
}

class CustomClassLoader extends ClassLoader {

  Class findClass(String name) {
    // 尋找字節(jié)碼
    byte[] code = findCodeFromSomewhere(name);
    // 組裝Class對象
    return this.defineClass(code, name);
  }
}

自定義類加載器不易破壞雙親委派規(guī)則彪笼,不要輕易覆蓋 loadClass 方法钻注。否則可能會導致自定義加載器無法加載內(nèi)置的核心類庫。在使用自定義加載器時配猫,要明確好它的父加載器是誰幅恋,將父加載器通過子類的構(gòu)造器傳入。如果父類加載器是 null泵肄,那就表示父加載器是「根加載器」捆交。

8、鉆石依賴

項目管理上有一個著名的概念叫著「鉆石依賴」腐巢,是指軟件依賴導致同一個軟件包的兩個版本需要共存而不能沖突品追。

image.png

我們平時使用的 maven 是這樣解決鉆石依賴的,它會從多個沖突的版本中選擇一個來使用系忙,如果不同的版本之間兼容性很糟糕诵盼,那么程序?qū)o法正常編譯運行。Maven 這種形式叫「扁平化」依賴管理。

使用 ClassLoader 可以解決鉆石依賴問題风宁。不同版本的軟件包使用不同的 ClassLoader 來加載洁墙,位于不同 ClassLoader 中名稱一樣的類實際上是不同的類。下面讓我們使用 URLClassLoader 來嘗試一個簡單的例子戒财,它默認的父加載器是 AppClassLoader

$ cat ~/source/jcl/v1/Dep.java
public class Dep {
    public void print() {
        System.out.println("v1");
    }
}

$ cat ~/source/jcl/v2/Dep.java
public class Dep {
 public void print() {
  System.out.println("v1");
 }
}

$ cat ~/source/jcl/Test.java
public class Test {
    public static void main(String[] args) throws Exception {
        String v1dir = "file:///Users/qianwp/source/jcl/v1/";
        String v2dir = "file:///Users/qianwp/source/jcl/v2/";
        URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
        URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});

        Class<?> depv1Class = v1.loadClass("Dep");
        Object depv1 = depv1Class.getConstructor().newInstance();
        depv1Class.getMethod("print").invoke(depv1);

        Class<?> depv2Class = v2.loadClass("Dep");
        Object depv2 = depv2Class.getConstructor().newInstance();
        depv2Class.getMethod("print").invoke(depv2);

        System.out.println(depv1Class.equals(depv2Class));
   }
}

在運行之前热监,我們需要對依賴的類庫進行編譯

$ cd ~/source/jcl/v1
$ javac Dep.java
$ cd ~/source/jcl/v2
$ javac Dep.java
$ cd ~/source/jcl
$ javac Test.java
$ java Test
v1
v2
false

在這個例子中即使兩個 URLClassLoader 指向的路徑是一樣的,下面這個表達式還是 false饮寞,因為即使是同樣的字節(jié)碼用不同的 ClassLoader 加載出來的類都不能算同一個類孝扛。

ClassLoader 固然可以解決依賴沖突問題,不過它也限制了不同軟件包的操作界面必須使用反射或接口的方式進行動態(tài)調(diào)用幽崩。

Maven 沒有這種限制苦始,它依賴于虛擬機的默認懶惰加載策略,運行過程中如果沒有顯示使用定制的 ClassLoader慌申,那么從頭到尾都是在使用 AppClassLoader陌选,而不同版本的同名類必須使用不同的 ClassLoader 加載,所以 Maven 不能完美解決鉆石依賴蹄溉。

如果你想知道有沒有開源的包管理工具可以解決鉆石依賴的咨油,我推薦你了解一下 sofa-ark,它是螞蟻金服開源的輕量級類隔離框架柒爵。

9役电、線程上下文加載器

Thread.contextClassLoader
如果你稍微閱讀過 Thread 的源代碼,你會在它的實例字段中發(fā)現(xiàn)有一個字段非常特別:

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

contextClassLoader「線程上下文類加載器」棉胀,這究竟是什么東西法瑟?

首先 contextClassLoader 是那種需要顯示使用的類加載器,如果你沒有顯示使用它唁奢,也就永遠不會在任何地方用到它瓢谢。你可以使用下面這種方式來顯示使用它。

Thread.currentThread().getContextClassLoader().loadClass(name);

這意味著如果你使用 forName(string name) 方法加載目標類驮瞧,它不會自動使用 contextClassLoader。那些因為代碼上的依賴關系而懶惰加載的類也不會自動使用 contextClassLoader來加載枯芬。

其次線程的 contextClassLoader 是從父線程那里繼承過來的论笔,所謂父線程就是創(chuàng)建了當前線程的線程。程序啟動時的 main 線程的 contextClassLoader 就是 AppClassLoader千所。這意味著如果沒有人工去設置狂魔,那么所有的線程的 contextClassLoader 都是 AppClassLoader。

那這個 contextClassLoader 究竟是做什么用的淫痰?我們要使用前面提到了類加載器分工與合作的原理來解釋它的用途最楷。它可以做到跨線程共享類,只要它們共享同一個 contextClassLoader。父子線程之間會自動傳遞 contextClassLoader籽孙,所以共享起來將是自動化的烈评。

如果不同的線程使用不同的 contextClassLoader,那么不同的線程使用的類就可以隔離開來犯建。如果我們對業(yè)務進行劃分讲冠,不同的業(yè)務使用不同的線程池,線程池內(nèi)部共享同一個 contextClassLoader适瓦,線程池之間使用不同的 contextClassLoader竿开,就可以很好的起到隔離保護的作用,避免類版本沖突玻熙。

如果我們不去定制 contextClassLoader否彩,那么所有的線程將會默認使用 AppClassLoader,所有的類都將會是共享的嗦随。線程的 contextClassLoader 使用場合比較罕見列荔,如果上面的邏輯晦澀難懂也不必過于計較。

JDK9 增加了模塊功能之后對類加載器的結(jié)構(gòu)設計做了一定程度的修改称杨,不過類加載器的原理還是類似的肌毅,作為類的容器,它起到類隔離的作用姑原,同時還需要依靠雙親委派機制來建立不同的類加載器之間的合作關系悬而。

10、 雙親委派機制的缺陷與破壞

雙親委派模型是有缺陷的锭汛,雙親委派模型很好地解決了各個類加載器的基礎類統(tǒng)一問題(越基礎的類由越上層的加載器進行加載)笨奠,基礎類之所以被稱為“基礎”,是因為它們總是作為被調(diào)用代碼調(diào)用的API唤殴。但是般婆,如果基礎類又要調(diào)用用戶的代碼,那該怎么辦呢朵逝。

這并非是不可能的事情蔚袍,一個典型的例子便是JNDI服務,它的代碼由啟動類加載器去加載(在JDK1.3時放進rt.jar)配名,但JNDI的目的就是對資源進行集中管理和查找啤咽,它需要調(diào)用獨立廠商實現(xiàn)部部署在應用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動類加載器不可能“認識”這些代碼渠脉,該怎么辦宇整?

為了解決這個困境,Java設計團隊只好引入了一個不太優(yōu)雅的設計:線程上下文件類加載器(Thread Context ClassLoader)芋膘。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置鳞青,如果創(chuàng)建線程時還未設置霸饲,它將會從父線程中繼承一個;如果在應用程序的全局范圍內(nèi)都沒有設置過臂拓,那么這個類加載器默認就是應用程序類加載器厚脉。有了有線程上下文類加載器,JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼埃儿,也就是父類加載器請求子類加載器去完成類加載動作器仗,這種行為實際上就是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,已經(jīng)違背了雙親委派模型童番,但這也是無可奈何的事情精钮。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI剃斧、JDBC轨香、JCE、JAXB和JBI等幼东。

以對數(shù)據(jù)庫管理JDBC為例臂容。java給數(shù)據(jù)庫操作提供了一個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的具體實現(xiàn):

public class DriverManager {


    // List of registered JDBC drivers 這里用來保存所有Driver的具體實現(xiàn)
    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);
    }
}

這里省略了大部分代碼,可以看到我們使用數(shù)據(jù)庫驅(qū)動前必須先要在DriverManager中使用registerDriver()注冊根蟹,然后我們才能正常使用脓杉。

SPI的全名為Service Provider Interface,主要是應用于廠商自定義組件或插件中简逮,在java.util.ServiceLoader的文檔里有比較詳細的介紹球散。簡單的總結(jié)下java SPI機制的思想:我們系統(tǒng)里抽象的各個模塊,往往有很多不同的實現(xiàn)方案,比如日志模塊、xml解析模塊质涛、jdbc模塊等方案。面向的對象的設計里陕凹,我們一般推薦模塊之間基于接口編程,模塊之間不對實現(xiàn)類進行硬編碼。一旦代碼里涉及具體的實現(xiàn)類,就違反了可拔插的原則皿渗,如果需要替換一種實現(xiàn),就需要修改代碼轻腺。為了實現(xiàn)在模塊裝配的時候能不在程序里動態(tài)指明羹奉,這就需要一種服務發(fā)現(xiàn)機制。 Java SPI就是提供這樣的一個機制:為某個接口尋找服務實現(xiàn)的機制约计。 有點類似IOC的思想,就是將裝配的控制權(quán)移到程序之外迁筛,在模塊化設計中這個機制尤其重要煤蚌。

Java SPI的具體約定為:當服務的提供者提供了服務接口的一種實現(xiàn)之后耕挨,在jar包的META-INF/services/目錄里同時創(chuàng)建一個以服務接口命名的文件尉桩,該文件里就是實現(xiàn)該服務接口的具體實現(xiàn)類蜘犁。而當外部程序裝配這個模塊的時候这橙,就能通過該jar包META-INF/services/里的配置文件找到具體的實現(xiàn)類名屈扎,并裝載實例化鹰晨,完成模塊的注入模蜡〈炒基于這樣一個約定就能很好的找到服務接口的實現(xiàn)類丸边,而不需要再代碼里制定荚孵。jdk提供服務實現(xiàn)查找的一個工具類:java.util.ServiceLoader收叶。JDBC SPI mysql的實現(xiàn)如下所示:

image.png

Java 提供了很多服務SPI,允許第三方為這些接口提供實現(xiàn)蜓萄。這些 SPI 的接口由 Java 核心庫來提供嫉沽,而這些 SPI 的實現(xiàn)則是由各供應商來完成绸硕。終端只需要將所需的實現(xiàn)作為 Java 應用所依賴的 jar 包包含進類路徑(CLASSPATH)就可以了玻佩。問題在于SPI接口中的代碼經(jīng)常需要加載具體的實現(xiàn)類:SPI的接口是Java核心庫的一部分咬崔,是由啟動類加載器來加載的郎仆;而SPI的實現(xiàn)類是由系統(tǒng)類加載器來加載的丸升。啟動類加載器是無法找到SPI的實現(xiàn)類的(因為它只加載Java的核心庫)狡耻,按照雙親委派模型夷狰,啟動類加載器無法委派系統(tǒng)類加載器去加載類。也就是說郊霎,類加載器的雙親委派模式無法解決這個問題。

線程上下文類加載器正好解決了這個問題进倍。線程上下文類加載器破壞了“雙親委派模型”,可以在執(zhí)行線程中拋棄雙親委派加載鏈模式猾昆,使程序可以逆向使用類加載器。

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

我們看下mysql的驅(qū)動是如何被加載的:

// 1.加載數(shù)據(jù)訪問驅(qū)動
Class.forName("com.mysql.jdbc.Driver");
 //2.連接到數(shù)據(jù)"庫"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

核心就是這句Class.forName()觸發(fā)了mysql驅(qū)動的加載骡苞,我們看下mysql對Driver接口的實現(xiàn):

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()其實觸發(fā)了靜態(tài)代碼塊解幽,然后向DriverManager中注冊了一個mysql的Driver實現(xiàn)躲株。這個時候霜定,我們通過DriverManager去獲取connection的時候只要遍歷當前所有Driver實現(xiàn),然后選擇一個建立連接就可以了站粟。

破壞雙親委派模型的情況

在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()注冊過程搔弄。
現(xiàn)在幅虑,我們分析下看使用了這種spi服務的模式原本的過程是怎樣的:

第一,從META-INF/services/java.sql.Driver文件中獲取具體的實現(xiàn)類名com.mysql.jdbc.Driver
第二顾犹,加載這個類倒庵,這里肯定只能用class.forName(“com.mysql.jdbc.Driver”)來加載
好了,問題來了炫刷,Class.forName()加載用的是調(diào)用者的Classloader擎宝,這個調(diào)用者DriverManager是在rt.jar中的,ClassLoader是啟動類加載器浑玛,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下绍申,所以肯定是無法加載mysql中的這個類的。這就是雙親委派模型的局限性了顾彰,父級加載器無法加載子級類加載器路徑中的類极阅。

那么,這個問題如何解決呢涨享?按照目前情況來分析筋搏,這個mysql的drvier只有應用類加載器能加載,那么我們只要在啟動類加載器中有方法獲取應用程序類加載器灰伟,然后通過它去加載就可以了拆又。這就是所謂的線程上下文加載器。線程上下文類加載器可以通過Thread.setContextClassLoaser()方法設置栏账,如果不特殊設置會從父類繼承帖族,一般默認使用的是應用程序類加載器,很明顯挡爵,線程上下文類加載器讓父級類加載器能通過調(diào)用子級類加載器來加載類竖般,這打破了雙親委派模型的原則。

現(xiàn)在我們看下DriverManager是如何使用線程上下文類加載器去加載第三方jar包中的Driver類的

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

使用時茶鹃,我們直接調(diào)用DriverManager.getConn()方法自然會觸發(fā)靜態(tài)代碼塊的執(zhí)行涣雕,開始加載驅(qū)動艰亮。然后我們看下ServiceLoader.load()的具體實現(xiàn):

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

可以看到核心就是拿到線程上下文類加載器,然后構(gòu)造了一個ServiceLoader,后續(xù)的具體查找過程挣郭,我們不再深入分析迄埃,這里只要知道這個ServiceLoader已經(jīng)拿到了線程上下文類加載器即可。

接下來兑障,DriverManager的loadInitialDrivers()方法中有一句driversIterator.next();,它的具體實現(xiàn)如下:

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

現(xiàn)在侄非,我們成功的做到了通過線程上下文類加載器拿到了應用程序類加載器(或者自定義的然后塞到線程上下文中的),同時我們也查找到了廠商在子級的jar包中注冊的驅(qū)動具體實現(xiàn)類名流译,這樣我們就可以成功的在rt.jar包中的DriverManager中成功的加載了放在第三方應用程序包中的類了逞怨。

很明顯,mysql驅(qū)動采用的這種spi服務確確實實是破壞了雙親委派模型的福澡,畢竟做到了父級類加載器加載了子級路徑中的類叠赦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市革砸,隨后出現(xiàn)的幾起案子除秀,更是在濱河造成了極大的恐慌,老刑警劉巖业岁,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳞仙,死亡現(xiàn)場離奇詭異,居然都是意外死亡笔时,警方通過查閱死者的電腦和手機棍好,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來允耿,“玉大人借笙,你說我怎么就攤上這事〗衔” “怎么了业稼?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蚂蕴。 經(jīng)常有香客問我低散,道長,這世上最難降的妖魔是什么骡楼? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任熔号,我火速辦了婚禮,結(jié)果婚禮上鸟整,老公的妹妹穿的比我還像新娘引镊。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布弟头。 她就那樣靜靜地躺著吩抓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赴恨。 梳的紋絲不亂的頭發(fā)上疹娶,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音伦连,去河邊找鬼蚓胸。 笑死,一個胖子當著我的面吹牛除师,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扔枫,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼汛聚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了短荐?” 一聲冷哼從身側(cè)響起倚舀,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忍宋,沒想到半個月后痕貌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡糠排,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年舵稠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片入宦。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡哺徊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出乾闰,到底是詐尸還是另有隱情落追,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布涯肩,位于F島的核電站轿钠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏病苗。R本人自食惡果不足惜疗垛,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铅乡。 院中可真熱鬧继谚,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诡壁,卻和暖如春济瓢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妹卿。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工旺矾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人夺克。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓箕宙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铺纽。 傳聞我的和親對象是個殘疾皇子柬帕,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354