類載入器第7彈:
實戰分析Tomcat的類載入器結構(使用Eclipse MAT驗證)
@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);
執行:
四、總結
大家將就看吧,第三章的測試如果仔細看下來,基本就能理解了。 其實,除了 介面這種方式,貌似 繼承 的方式也是可以的,改天再試驗下。 這一塊,不知道為啥,我是真的在網上書上沒找到,但其實很重要,改天找找虛擬機器層面的實現程式碼吧。 大家如果覺得有幫助,麻煩點個推薦,對於寫作的人來說,這莫過於最大的獎勵了。