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

三國夢迴發表於2019-06-25

類載入器第7彈:

實戰分析Tomcat的類載入器結構(使用Eclipse MAT驗證)

還是Tomcat,關於類載入器的趣味實驗

了不得,我可能發現了Jar 包衝突的祕密

一、一個程式設計師的思考

大家都知道,Tomcat 處理業務,靠什麼?最終是靠我們自己編寫的 Servlet。你可能說你不寫 servlet,你用 spring MVC,那也是人家幫你寫好了,你只需要配置就行。在這裡,有一個邊界,Tomcat 算容器,容器的相關 jar 包都放在它自己的 安裝目錄的 lib 下面; 我們呢,算是業務,算是webapp,我們的 servlet ,不管是自定義的,還是 spring mvc 的DispatcherServlet,都是放在我們的 war 包裡面 WEB-INF/lib下。 看過前面文章的同學是曉得的, 這二者是由不同的類載入器載入的。在 Tomcat 的實現中,會委託 webappclassloader 去載入WAR 包中的 servlet ,然後 反射生成對應的 servlet。後續有請求來了,呼叫生成的 servlet 的 service 方法即可。

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

 

    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 嗎?說真的,我翻了不少書,從來沒提到這個,就連網上也很含糊。

 

再來一個,關於SPI的問題。  在 SPI 中(有興趣的同學可以自行查詢,網上很多,我隨便找了一篇:https://www.jianshu.com/p/46b42f7f593c),主要是由 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)的實現如下:

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?

 

 二、不同類載入器載入的類,可以轉換的關鍵

經過上面兩個例子的觀察,不知道大家發現沒, 我們都是把一個實現,轉換為一個介面。也許,這就是問題的關鍵。我們可以大膽地推測,基於類的雙親委派機制,在 載入 實現類的時候,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 的類載入器去載入的,二者相同,所以自然可以相互轉換。

 

這裡總結一句(不一定對),在同時滿足以下幾個條件的情況下:

前置條件1、介面 jar包 中,定義一個介面 Test

前置條件2、實現 jar 包中,定義 Test 的實現類,比如 TestImpl。(但是不要在該類中包含該 介面,你說沒法編譯,那就把介面 jar包放到 classpath)

前置條件3、介面 jar 包由 interface_classLoader 載入,實現 jar 包 由 impl_classloader 載入,其中 impl_classloader 會在自己無法載入時,委派給 interface_classLoader 

 

則,定義在 實現jar 中的Test 介面的實現類,反射生成的物件,可以轉換為 Test 型別。

 

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

 

三、求證

1、定義介面 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包。

 

2、定義介面的實現 jar

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

D:\classloader_impl\TestSampleImpl.java

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

}

編譯,打包:

1 D:\classloader_impl>javac -cp D:\classloader_interface\interface.jar TestSampleI
2 mpl.java
3 
4 D:\classloader_impl>jar -cvf impl.jar TestSampleImpl.class
5 已新增清單
6 正在新增: TestSampleImpl.class(輸入 = 221) (輸出 = 176)(壓縮了 20%)

 

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

 

3、測試

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

 

 1 import java.lang.reflect.Method;
 2 import java.net.URL;
 3 import java.net.URLClassLoader;
 4 
 5 /**
 6  * desc:
 7  *
 8  * @author : caokunliang
 9  * creat_date: 2019/6/14 0014
10  * creat_time: 17:04
11  **/
12 public class MainTest {
13 
14 
15     public static void testInterfaceByOneAndImplByAnother()throws Exception{
16         URL url = new URL("file:D:\\classloader_interface\\interface.jar");
17         URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
18         Class<?> iTestSampleClass = urlClassLoader.loadClass("ITestSample");
19 
20 
21         URL implUrl = new URL("file:D:\\classloader_impl\\impl.jar");
22         URLClassLoader implUrlClassLoader = new URLClassLoader(new URL[]{implUrl}, urlClassLoader);
23         Class<?> testSampleImplClass = implUrlClassLoader.loadClass("TestSampleImpl");
24 
25 
26         System.out.println("實現類能轉否?:"  + iTestSampleClass.isAssignableFrom(testSampleImplClass));
27 
28     }
29 
30     public static void main(String[] args) throws Exception {
31         testInterfaceByOneAndImplByAnother();
32     }
33 
34 }

 

列印如下:

 

4、延伸測試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  也不能載入,於是丟擲異常。

 

5、延伸測試2

我們再做一個改動, 改動處和上一個測試一樣,只是這次,我們傳入了一個特別的類載入器,作為其 parentClassLoader。 它的特殊之處在於,almostSameUrlClassLoader 和 前面載入 interface.jar 的類載入器一模一樣,只是是一個新的例項。

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

 

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

 

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

 

6、延伸測試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);

 

執行:

 

總結

大家將就看吧,第三章的測試如果仔細看下來,基本就能理解了。 其實,除了 介面這種方式,貌似 繼承 的方式也是可以的,改天再試驗下。 這一塊,不知道為啥,我是真的在網上書上沒找到,但其實很重要,改天找找虛擬機器層面的實現程式碼吧。 大家如果覺得有幫助,麻煩點個推薦,對於寫作的人來說,這莫過於最大的獎勵了。

 

相關文章