不吹不黑,關於 Java 類加載器的這一點,市面上沒有任何一本圖書講到,實戰分析Tomcat的類加載器結構(使用Eclipse MAT驗證),還是Tomcat,關於類加載器的趣味實驗,重寫類加載器,實現簡單的熱替換,@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼,@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

類加載器第7彈:

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

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

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

重寫類加載器,實現簡單的熱替換

@Java Web 程序員,我們一起給程序開個後門吧:讓你在保留現場,服務不重啟的情況下,執行我們的調試代碼

@Java web程序員,在保留現場,服務不重啟的情況下,執行我們的調試代碼(JSP 方式)

 

 

一、一個程序員的思考

大家都知道,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);

 

執行:

 

總結

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

 

參考:

深入探討 Java 類加載器

 

 

 

 

 

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

您可能也會喜歡…