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