類加載器第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);
執行:
四、總結
大家將就看吧,第三章的測試如果仔細看下來,基本就能理解了。 其實,除了 接口這種方式,貌似 繼承 的方式也是可以的,改天再試驗下。 這一塊,不知道為啥,我是真的在網上書上沒找到,但其實很重要,改天找找虛擬機層面的實現代碼吧。 大家如果覺得有幫助,麻煩點個推薦,對於寫作的人來說,這莫過於最大的獎勵了。
參考:
https://blog.csdn.net/conquer0715/article/details/51283632