關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到

1、一個程序員的思考

你們都知道,Tomcat 處理業務,靠什麼?最終是靠咱們本身編寫的 Servlet。你可能說你不寫 servlet,你用 spring MVC,那也是人家幫你寫好了,你只須要配置就行。在這裏,有一個邊界,Tomcat 算容器,容器的相關 jar 包都放在它本身的 安裝目錄的 lib 下面; 咱們呢,算是業務,算是webapp,咱們的 servlet ,不論是自定義的,仍是 spring mvc 的DispatcherServlet,都是放在咱們的 war 包裏面 WEB-INF/lib下。 看過前面文章的同窗是曉得的, 這兩者是由不一樣的類加載器加載的。在 Tomcat 的實現中,會委託 webappclassloader 去加載WAR 包中的 servlet ,而後 反射生成對應的 servlet。後續有請求來了,調用生成的 servlet 的 service 方法便可。java

在 org.apache.catalina.core.StandardWrapper#loadServlet 中,即負責 生成 servlet:mysql

org.apache.catalina.core.DefaultInstanceManager#newInstance(java.lang.String)
    @Override
    public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException {
        Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
        return newInstance(clazz.newInstance(), clazz);
    }

在上圖中,會利用 instanceManager 根據參數中指定的 servletClass 去生成 servlet 實例。newInstance 代碼以下,主要就是用 當前 context 的classloader 去加載 該 servlet,而後 反射生成 servlet 對象。程序員

咱們重點關注的是那個紅框圈出的強轉:爲何由 webappclassloader 加載的對象,能夠轉換 爲 Tomcat common classloader 加載的 Servlet 呢? 按理說,兩個不一樣的類加載器加載的類都是互相隔離的啊,不該該拋一個 ClassCastException 嗎?說真的,我翻了很多書,歷來沒提到這個,就連網上也很含糊。web

再來一個,關於SPI的問題。  在 SPI 中,主要是由 java 社區指定規範,好比 JDBC,廠家有那麼多,mysql,oracle,postgre,你們都有本身的 jar包,要是沒有 JDBC 規範,咱們估計就得針對各個廠家的實現類編程了,那遷移就麻煩了,你針對 mysql 數據庫寫的代碼,換成 oracle 的話,代碼不改是確定不能跑的。因此, JCP組織制定了 JDBC 規範,JDBC 規範中指定了一堆的 接口,咱們平時開發,只須要針對接口來編程,而實現怎麼辦,交給各廠家唄,由廠家來實現 JDBC 規範。這裏以代碼舉例,oracle.jdbc.OracleDriver 實現了 java.sql.Driver,同時,在 oracle.jdbc.OracleDriver 的 static 初始化塊中,有下面的代碼:面試

static {
        try {
            if (defaultDriver == null) {
                defaultDriver = new oracle.jdbc.OracleDriver();
                DriverManager.registerDriver(defaultDriver);
            }
    // 省略
    }

其中,標紅這句,就是 Oracle Driver 要向 JDBC 接口註冊本身,java.sql.DriverManager#registerDriver(java.sql.Driver)的實現以下:spring

java.sql.DriverManager#registerDriver(java.sql.Driver) 

public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }

能夠看到,registerDriver(java.sql.Driver) 方法的參數爲 java.sql.Driver,而咱們傳的參數爲 oracle.jdbc.OracleDriver 類型,這兩個類型,分別由不一樣的類加載器加載(java.sql.Driver 由 jdk 的 啓動類加載器加載,而 oracle.jdbc.OracleDriver ,若是爲 web應用,則爲 tomcat 的 webappclassloader 來加載,無論怎麼說,反正不是由 jdk 加載的),這樣的兩個類型,連 類加載器都不同,怎麼就能正常轉換呢,爲啥不拋 ClassCastException?sql

2、不一樣類加載器加載的類,能夠轉換的關鍵

通過上面兩個例子的觀察,不知道你們發現沒, 咱們都是把一個實現,轉換爲一個接口。也許,這就是問題的關鍵。咱們能夠大膽地推測,基於類的雙親委派機制,在 加載 實現類的時候,jvm 遇到 實現類中引用到的其餘類,也會觸發加載,加載的過程當中,會觸發 loadClass,好比,加載 webappclassloader 在 加載 oracle.jdbc.OracleDriver 時,觸發加載 java.sql.Driver,可是 webappclassloader 明顯是不能去加載 java.sql.Driver 的,因而會委託給 jdk 的類加載,因此,最終,oracle.jdbc.OracleDriver 中 引用的 java.sql.Driver ,其實就是由 jdk 的類加載器去加載的。 而 registerDriver(java.sql.Driver driver) 中的 driver 參數的類型 java.sql.Driver 也是由 jdk 的類加載器去加載的,兩者相同,因此天然能夠相互轉換。數據庫

這裏總結一句(不必定對),在同時知足如下幾個條件的狀況下:apache

  • 前置條件一、接口 jar包 中,定義一個接口 Test編程

  • 前置條件二、實現 jar 包中,定義 Test 的實現類,好比 TestImpl。(可是不要在該類中包含該 接口,你說無法編譯,那就把接口 jar包放到 classpath)

  • 前置條件三、接口 jar 包由 interface_classLoader 加載,實現 jar 包 由 impl_classloader 加載,其中 impl_classloader 會在本身沒法加載時,委派給 interface_classLoader

則,定義在 實現jar 中的Test 接口的實現類,反射生成的對象,能夠轉換爲 Test 類型。

猜想說完了,就是求證過程。

3、求證

一、定義接口 jar
D:\classloader_interface\ITestSample.java  

/**
 * desc:
 *
 * @author : 
 * creat_date: 2019/6/16 0016
 * creat_time: 19:28
 **/
public interface ITestSample {
}

cmd下,執行:

D:\classloader_interface>javac ITestSample.java
D:\classloader_interface>jar cvf interface.jar ITestSample.class
已添加清單
正在添加: ITestSample.class(輸入 = 103) (輸出 = 86)(壓縮了 16%)

此時,便可在當前目錄下,生成 名爲 interface.jar 的接口jar包。

二、定義接口的實現 jar

在不一樣目錄下,新建了一個實現類。

D:\classloader_impl\TestSampleImpl.java

/**
 * Created by Administrator on 2019/6/25.
 */
public class TestSampleImpl implements  ITestSample{

}

編譯,打包:

D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI
mpl.java
 
D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class
已添加清單
正在添加: TestSampleImpl.class(輸入 = 221) (輸出 = 176)(壓縮了 20%)

請注意上面的標紅行,不加編譯不過。

三、測試

測試的思路是,用一個urlclassloader 去加載 interface.jar 中的 ITestSample,用另一個 URLClassLoader 去加載 impl.jar 中的 TestSampleImpl ,而後用java.lang.Class#isAssignableFrom 判斷後者是否能轉成前者。

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * desc:
 *
 * @author : caokunliang
 * creat_date: 2019/6/14 0014
 * creat_time: 17:04
 **/
public class MainTest {


    public static void testInterfaceByOneAndImplByAnother()throws Exception{
        URL url = new URL("file:D:\\classloader_interface\\interface.jar");
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample");


        URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
        Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");


        System.out.println("實現類能轉否?:"  + iTestSampleClass.isAssignableFrom(testSampleImplClass));

    }

    public static void main(String[] args) throws Exception {
        testInterfaceByOneAndImplByAnother();
    }

}

打印以下:

四、延伸測試1

若是咱們作以下改動,你猜會怎樣? 這裏的主要差異是:

改以前,urlClassloader 做爲 parentClassloader:

URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);

改以後,不傳,默認會以 jdk 的應用類加載器做爲 parent:

URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl});

打印結果是:

Exception in thread "main" java.lang.NoClassDefFoundError: ITestSample
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at MainTest.testInterfaceByOneAndImplByAnother(MainTest.java:23)
    at MainTest.main(MainTest.java:33)
Caused by: java.lang.ClassNotFoundException: ITestSample
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 13 more

結果就是,第23行,Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl"); 這裏報錯了,提示找不到 ITestSample。

這就是由於,在加載了 implUrlClassLoader 後,觸發了對 ITestSample 的隱式加載,這個隱式加載會用哪一個加載器去加載呢,沒有默認指明的狀況下,就是用當前的類加載器,而當前類加載器就是 implUrlClassLoader ,可是這個類加載器開始加載 ITestSample,它是遵循雙親委派的,它的parent 加載器 即爲 appclassloader,(jdk的默認應用類加載器),但appclassloader 根本不能加載 ITestSample,因而仍是還給 implUrlClassLoader ,可是 implUrlClassLoader 也不能加載,因而拋出異常。

五、延伸測試2

咱們再作一個改動, 改動處和上一個測試同樣,只是此次,咱們傳入了一個特別的類加載器,做爲其 parentClassLoader。 它的特殊之處在於,almostSameUrlClassLoader 和 前面加載 interface.jar 的類加載器如出一轍,只是是一個新的實例。

URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);

此次,看看結果吧,也許你猜到了?

此次沒報錯了,畢竟 almostSameUrlClassLoader 知道去哪裏加載 ITestSample,可是,最後的結果顯示,實現類的 class 並不能 轉成 ITestSample。

六、延伸測試3

說實話,有些同窗可能對 java.lang.Class#isAssignableFrom 不是很熟悉,咱們換個你更不熟悉的,如何?

URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
        URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, almostSameUrlClassLoader);
        Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
        Object o = testSampleImplClass.newInstance();
        Object cast = iTestSampleClass.cast(o); // 將 o 轉成 接口的那個類
        System.out.println(cast);

結果:

若是換成下面這樣,就沒啥問題:

URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
        URLClassLoader almostSameUrlClassLoader = new URLClassLoader(new URL[]{url});
        URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
        Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
        Object o = testSampleImplClass.newInstance();
        Object cast = iTestSampleClass.cast(o);
        System.out.println(cast);

執行:

若是你如今在JAVA這條路上掙扎,也想在IT行業拿高薪,能夠參加咱們的訓練營課程,選擇最適合本身的課程學習,技術大牛親授,7個月後,進入名企拿高薪。咱們的課程內容有:Java工程化、高性能及分佈式、高架構。性能調優、Spring,MyBatis,Netty源碼分析和大數據等多個知識點。若是你想拿高薪的,想學習的,想就業前景好的,想跟別人競爭能取得優點的,想進阿里面試但擔憂面試不過的,你均可以來,q羣號爲:956011797

注:加羣要求

一、具備1-5工做經驗的,面對目前流行的技術不知從何下手,須要突破技術瓶頸的能夠加。

二、在公司待久了,過得很安逸,但跳槽時面試碰壁。須要在短期內進修、跳槽拿高薪的能夠加。

三、若是沒有工做經驗,但基礎很是紮實,對java工做機制,經常使用設計思想,經常使用java開發框架掌握熟練的,能夠加。

四、以爲本身很牛B,通常需求都能搞定。可是所學的知識點沒有系統化,很難在技術領域繼續突破的能夠加。

5.阿里Java高級大牛直播講解知識點,分享知識,多年工做經驗的梳理和總結,帶着你們全面、科學地創建本身的技術體系和技術認知!

相關文章
相關標籤/搜索