深入解析Java類加載的案例與實戰(zhàn)教程
本篇文章主要介紹Tomcat類加載器架構(gòu),以及基于類加載和字節(jié)碼相關(guān)知識,去分析動態(tài)代理的原理。
一、Tomcat類加載器架構(gòu)
Tomcat有自己定義的類加載器,因為一個功能健全的Web服務(wù)器,都要解決 如下的這些問題:
- 部署在同一個服務(wù)器上的兩個Web應(yīng)用程序所使用的Java類庫可以實現(xiàn)相互隔離。這是最基本的 需求。兩個不同的應(yīng)用程序可能會依賴同一個第三方類庫的不同版本,不能要求每個類庫在一個服務(wù) 器中只能有一份。
- 部署在同一個服務(wù)器上的兩個Web應(yīng)用程序所使用的Java類庫可以互相共享。例如用戶可能有10個使用Spring組織的應(yīng)用程序部署在同一臺服務(wù)器 上,如果把10份Spring分別存放在各個應(yīng)用程序的隔離目錄中,將會是很大的資源浪費——這主要倒 不是浪費磁盤空間的問題,而是指類庫在使用時都要被加載到服務(wù)器內(nèi)存,如果類庫不能共享,虛擬 機的方法區(qū)就會很容易出現(xiàn)過度膨脹的風(fēng)險。
- 服務(wù)器需要盡可能地保證自身的安全不受部署的Web應(yīng)用程序影響?;诎?全考慮,服務(wù)器所使用的類庫應(yīng)該與應(yīng)用程序的類庫互相獨立。
- 支持JSP應(yīng)用的Web服務(wù)器,十有八九都需要支持HotSwap功能。我們知道JSP文件最終要被編譯 成Java的Class文件才能被虛擬機執(zhí)行,所謂的hotswap,就是使用新的代碼替換掉已經(jīng)加載的這個Class中的內(nèi)容。
由于存在上述問題,在部署Web應(yīng)用時,單獨的一個ClassPath就不能滿足需求了,所以各種Web服 務(wù)器都不約而同地提供了好幾個有著不同含義的ClassPath路徑供用戶存放第三方類庫
,這些路徑一般 會以“lib”或“classes”命名。被放置到不同路徑中的類庫,具備不同的訪問范圍和服務(wù)對象,通常每一 個目錄都會有一個相應(yīng)的自定義類加載器去加載放置在里面的Java類庫
。
在Tomcat目錄結(jié)構(gòu)中,把Java類庫放置在這4組目錄中,每一組都有獨立的含義,分別是:
- 放置在/common目錄中。類庫可被Tomcat和所有的Web應(yīng)用程序共同使用。
- 放置在/server目錄中。類庫可被Tomcat使用,對所有的Web應(yīng)用程序都不可見。放置在/shared目錄中。類庫可被所有的Web應(yīng)用程序共同使用,但對Tomcat自己不可見。
- 放置在/WebApp/WEB-INF目錄中。類庫僅僅可以被該Web應(yīng)用程序使用,對Tomcat和其他Web應(yīng)用程序都不可見。
為了支持這套目錄結(jié)構(gòu),并對目錄里面的類庫進行加載和隔離,Tomcat自定義了多個類加載器, 這些類加載器按照經(jīng)典的雙親委派模型來實現(xiàn),關(guān)系如下圖所示。
灰色背景的3個類加載器是默認(rèn)提供的類加載器,而JDKCommon類加載器、Catalina類加載器(也稱為Server類 加載器)、Shared類加載器和Webapp類加載器則是Tomcat自己定義的類加載器。
它們分別加 載/common/、/server/、/shared/*和/WebApp/WEB-INF/*中的Java類庫。
其中WebApp類加載器和JSP類加載器通常還會存在多個實例,每一個Web應(yīng)用程序?qū)?yīng)一個WebApp類加載器
,每一個JSP文件對應(yīng) 一個JasperLoader類加載器
。
由上圖得知:
- Common類加載器能加載的類都可以被Catalina類加載器和Shared 類加載器使用
- 而Catalina類加載器和Shared類加載器自己能加載的類則與對方相互隔離
- WebApp類 加載器可以使用Shared類加載器加載到的類,但各個WebApp類加載器實例之間相互隔離。
- JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個Class文件,它存在的目的就是為了被 丟棄:當(dāng)服務(wù)器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,并通過再建立一個新 的JSP類加載器來實現(xiàn)JSP文件的HotSwap功能。
本例中的類加載結(jié)構(gòu)在Tomcat 6以前是它默認(rèn)的類加載器結(jié)構(gòu)
,在Tomcat 6及之后的版本簡化了默 認(rèn)的目錄結(jié)構(gòu),只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader項后才會 真正建立Catalina類加載器和Shared類加載器的實例,否則會用到這兩個類加載器的地方都會用 Common類加載器的實例代替。
Tomcat 6之后也 順理成章地把/common、/server和/shared這3個目錄默認(rèn)合并到一起變成1個/lib目錄
,這個目錄里的類庫 相當(dāng)于以前/common目錄中類庫的作用。
那么筆者不妨再提一個問題讓各位讀者思考一下:前 面曾經(jīng)提到過一個場景,如果有10個Web應(yīng)用程序都是用Spring來進行組織和管理的話,可以把Spring 放到Common或Shared目錄下讓這些程序共享
。Spring要對用戶程序的類進行管理,自然要能訪問到用 戶程序的類
,而用戶的程序顯然是放在/WebApp/WEB-INF目錄中的。那么被Common類加載器或 Shared類加載器加載的Spring如何訪問并不在其加載范圍內(nèi)的用戶程序呢?
答案:如果按主流的雙親委派機制,顯然無法做到讓父類加載器加載的類去訪問子類加載器加載的類,但使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。spring加載類所用的Classloader是通過Thread.currentThread().getContextClassLoader()來獲取的,而當(dāng)線程創(chuàng)建時會默認(rèn)setContextClassLoader(AppClassLoader),即線程上下文類加載器被設(shè)置為AppClassLoader,spring中始終可以獲取到這個AppClassLoader(在Tomcat里就是WebAppClassLoader)子類加載器來加載的bean,以后任何一個線程都可以通過getContextClassLoader()獲取到WebAppClassLoader來getbean了。
二、動態(tài)代理的原理
“字節(jié)碼生成”并不是什么高深的技術(shù),因為JDK里面的Javac命令就是字節(jié)碼生成技術(shù)的“老祖 宗”,并且Javac也是一個由Java語言寫成的程序。
在Java世界里面除了Javac和字 節(jié)碼類庫外,使用到字節(jié)碼生成的例子比比皆是,如Web服務(wù)器中的JSP編譯器
,編譯時織入的AOP框 架
,還有很常用的動態(tài)代理技術(shù)
,甚至在使用反射的時候虛擬機都有可能會在運行時生成字節(jié)碼來提 高執(zhí)行速度
。我們選擇其中相對簡單的動態(tài)代理技術(shù)來講解字節(jié)碼生成技術(shù)是如何影響程序運作的。
什么是動態(tài)代理?
動態(tài)代理中所說的“動態(tài)”,是指實 現(xiàn)了可以在原始類和接口還未知的時候,就確定代理類的代理行為,當(dāng)代理類與原始類脫離直接聯(lián)系 后,就可以很靈活地重用于不同的應(yīng)用場景之中。
下面代碼演示了一個最簡單的動態(tài)代理的用法,原始的代碼邏輯是打印一句“hello world”,代 理類的邏輯是在原始類方法執(zhí)行前打印一句“welcome”。我們先看一下代碼,然后再分析JDK是如何做 到的。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxyTest { interface IHello { void sayHello(); } static class Hello implements IHello { @Override public void sayHello() { System.out.println("hello world"); } } static class DynamicProxy implements InvocationHandler { Object originalObj; Object bind(Object originalObj) { this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj, args); } } public static void main(String[] args) { IHello hello = (IHello) new DynamicProxy().bind(new Hello()); hello.sayHello(); } }
運行結(jié)果如下:
在上述代碼里,唯一的“黑匣子”就是Proxy::newProxyInstance()方法,除此之外再沒有任何特殊之 處。這個方法返回一個實現(xiàn)了IHello的接口,并且代理了new Hello()實例行為的對象。
newProxyInstance一共傳進去三個參數(shù):
- loader第一個參數(shù),代表的是被代理類的類加載器
- interfaces代理類要實現(xiàn)的被代理類接口
- InvocationHandler代表的是將方法調(diào)用分派給的調(diào)用處理程序
跟蹤這個方法的 源碼,可以看到程序進行過驗證、優(yōu)化、緩存、同步、生成字節(jié)碼、顯式類加載等操作,前面的步驟 并不是我們關(guān)注的重點,這里只分析它最后調(diào)用sun.misc.ProxyGenerator::generateProxyClass()方法來完 成生成字節(jié)碼的動作
。
這個方法會在運行時產(chǎn)生一個描述代理類的字節(jié)碼byte[]數(shù)組。如果想看一看這 個在運行時產(chǎn)生的代理類中寫了些什么,可以在main()方法中加入下面這句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
執(zhí)行完 可以用idea在debug狀態(tài)下直接雙擊shift搜索$Proxy即可找到j(luò)ava文件,如下:
import com.gzl.cn.DynamicProxyTest.IHello; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; final class $Proxy0 extends Proxy implements IHello { private static Method m1; private static Method m3; private static Method m2; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); } // 此處由于版面原因,省略equals()、hashCode()、toString()3個方法的代碼 public final void sayHello() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.gzl.cn.DynamicProxyTest$IHello").getMethod("sayHello"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
動態(tài)代理的原理:
- 通過ProxyGenerator::generateProxyClass()生成一個代理類
- 這個代理類的實現(xiàn)代碼也很簡單,它為傳入接口中的每一個方法,以及從java.lang.Object中繼承來 的equals()、hashCode()、toString()方法都生成了對應(yīng)的實現(xiàn),并且統(tǒng)一調(diào)用了InvocationHandler對象的 invoke()方法來實現(xiàn)這些方法的 內(nèi)容。
- 代碼中的“super.h”就是父類Proxy中保存的InvocationHandler實例變量,而實例變量就是剛剛傳入的new Hello()。
- 所以無論調(diào)用動態(tài)代理的哪一 個方法,實際上都是在執(zhí)行InvocationHandler::invoke()中的代理邏輯。
這個例子中并沒有講到generateProxyClass()方法具體是如何產(chǎn)生代理類“$Proxy0.class”的字節(jié)碼 的,大致的生成過程其實就是根據(jù)Class文件的格式規(guī)范去拼裝字節(jié)碼
,但是在實際開發(fā)中,以字節(jié)為 單位直接拼裝出字節(jié)碼的應(yīng)用場合很少見,這種生成方式也只能產(chǎn)生一些高度模板化的代碼。
對于用 戶的程序代碼來說,如果有要大量操作字節(jié)碼的需求,還是使用封裝好的字節(jié)碼類庫比較合適。如果 讀者對動態(tài)代理的字節(jié)碼拼裝過程確實很感興趣,可以在OpenJDK的 java.base\share\classes\java\lang\reflect目錄下找到sun.misc.ProxyGenerator的源碼。
三、Java語法糖的改變
在Java世界里,每一次JDK大版本的發(fā)布,對Java程 序編寫習(xí)慣改變最大的,肯定是那些對Java語法做出重大改變的版本。
- 譬如JDK 5時加入的自動裝箱、 泛型、動態(tài)注解、枚舉、變長參數(shù)、遍歷循環(huán)(foreach循環(huán));譬如JDK 8時加入的Lambda表達(dá)式、 Stream API、接口默認(rèn)方法等。
- 事實上在沒有這些語法特性的年代,Java程序也照樣能寫。 現(xiàn)在問題來了,如何把高版本JDK中編寫的代碼放到低版本JDK 環(huán)境中去部署使用?
為了解決這個問題,一種名為“Java逆向移植”的工具(Java Backporting Tools)應(yīng) 運而生,Retrotranslator和Retrolambda是這類工具中的杰出代表。
Retrotranslator的作用是將JDK 5編譯出來的Class文件轉(zhuǎn)變?yōu)榭梢栽贘DK 1.4或1.3上部署的版本
, 它能很好地支持自動裝箱、泛型、動態(tài)注解、枚舉、變長參數(shù)、遍歷循環(huán)、靜態(tài)導(dǎo)入這些語法特性, 甚至還可以支持JDK 5中新增的集合改進、并發(fā)包及對泛型、注解等的反射操作。
Retrolambda
的作 用與Retrotranslator是類似的,目標(biāo)是將JDK 8
的Lambda表達(dá)式和try-resources語法轉(zhuǎn)變?yōu)?code>可以在JDK 5、JDK 6、JDK 7中使用的形式,同時也對接口默認(rèn)方法提供了有限度的支持。
什么是語法糖?
在前端編譯器層面做的改進。這種改進被稱作語法糖。也就是這些語法糖主要是幫助我們這些開發(fā)人員減少代碼量,但是并沒有省略掉,只是交給了javac編譯器,來替我們做了轉(zhuǎn)換。
- 如自動裝箱拆箱,實際上就是Javac編 譯器在程序中使用到包裝對象的地方自動插入了很多Integer.valueOf()、Float.valueOf()之類的代碼
- 使用enum關(guān)鍵字定義常量,盡管從 Java語法上看起來與使用class關(guān)鍵字定義類、使用interface關(guān)鍵字定義接口是同一層次的,但實際上這 是由Javac編譯器做出來的假象,從字節(jié)碼的角度來看,枚舉僅僅是一個繼承于java.lang.Enum、自動生 成了values()和valueOf()方法的普通Java類而已。
到此這篇關(guān)于深入解析Java類加載的案例與實戰(zhàn)的文章就介紹到這了,更多相關(guān)Java類加載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Easypoi 輕松實現(xiàn)復(fù)雜excel文件導(dǎo)出功能
這篇文章主要介紹了Easypoi 輕松實現(xiàn)復(fù)雜excel文件導(dǎo)出功能,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-11-11SpringBoot攔截器excludePathPatterns方法不生效的解決方案
這篇文章主要介紹了SpringBoot攔截器excludePathPatterns方法不生效的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07spring security中Authority、Role的區(qū)別及說明
這篇文章主要介紹了spring security中Authority、Role的區(qū)別及說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09