JDK動(dòng)態(tài)代理的深入理解與實(shí)際應(yīng)用
前言
在Java的世界里,JDK的動(dòng)態(tài)代理是一項(xiàng)非常強(qiáng)大且實(shí)用的技術(shù),它為我們?cè)谶\(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建代理類提供了可能,從而實(shí)現(xiàn)對(duì)目標(biāo)對(duì)象方法調(diào)用的靈活攔截和增強(qiáng)。今天,就讓我們一起來(lái)深入探討JDK動(dòng)態(tài)代理的方方面面。
1. 什么是JDK動(dòng)態(tài)代理
JDK的動(dòng)態(tài)代理是基于Java反射機(jī)制實(shí)現(xiàn)的一種技術(shù)手段。簡(jiǎn)單來(lái)說(shuō),它能夠在程序運(yùn)行期間,動(dòng)態(tài)地生成一個(gè)代理類,這個(gè)代理類和我們的目標(biāo)對(duì)象實(shí)現(xiàn)了相同的接口。在代理類的方法內(nèi)部,會(huì)去調(diào)用目標(biāo)對(duì)象的相應(yīng)方法,而且我們可以在調(diào)用目標(biāo)方法的前后,插入自己定義的邏輯代碼,這樣就實(shí)現(xiàn)了對(duì)目標(biāo)方法的攔截和功能增強(qiáng)。
打個(gè)比方,就好像我們有一個(gè)明星(目標(biāo)對(duì)象),而代理類就像是明星的經(jīng)紀(jì)人。粉絲(調(diào)用者)想要和明星交流(調(diào)用方法),都需要通過(guò)經(jīng)紀(jì)人。經(jīng)紀(jì)人可以在粉絲和明星交流之前,詢問(wèn)粉絲一些問(wèn)題(前置邏輯),在交流之后,也可以做一些總結(jié)(后置邏輯),但實(shí)際上真正和粉絲交流的還是明星(目標(biāo)對(duì)象執(zhí)行方法)。
從Java語(yǔ)言的角度來(lái)看,動(dòng)態(tài)代理允許開(kāi)發(fā)者在運(yùn)行時(shí)創(chuàng)建一個(gè)實(shí)現(xiàn)了指定接口的代理類實(shí)例,該實(shí)例可以將方法調(diào)用轉(zhuǎn)發(fā)到指定的目標(biāo)對(duì)象,并在轉(zhuǎn)發(fā)前后執(zhí)行額外的邏輯。這使得我們可以在不修改目標(biāo)對(duì)象代碼的情況下,對(duì)其方法的行為進(jìn)行增強(qiáng)或修改,為程序的設(shè)計(jì)和維護(hù)提供了很大的靈活性。
2. JDK動(dòng)態(tài)代理的原理
JDK動(dòng)態(tài)代理的核心原理就是Java的反射機(jī)制。反射允許我們?cè)谶\(yùn)行時(shí)獲取類的信息,包括方法、字段等,并且能夠動(dòng)態(tài)地調(diào)用方法和操作字段。
在動(dòng)態(tài)代理中,當(dāng)我們使用Proxy
類的newProxyInstance
方法創(chuàng)建代理對(duì)象時(shí),JDK會(huì)在底層做以下幾件事情:
- 生成代理類字節(jié)碼:JDK會(huì)根據(jù)我們傳入的目標(biāo)對(duì)象實(shí)現(xiàn)的接口,動(dòng)態(tài)地生成一個(gè)代理類的字節(jié)碼。這個(gè)過(guò)程涉及到對(duì)接口中定義的方法進(jìn)行分析和處理,為每個(gè)方法生成相應(yīng)的代理邏輯。代理類的字節(jié)碼是在運(yùn)行時(shí)通過(guò)特殊的字節(jié)碼生成算法生成的,它實(shí)現(xiàn)了與目標(biāo)對(duì)象相同的接口,并且在每個(gè)方法內(nèi)部都包含了對(duì)
InvocationHandler
的調(diào)用邏輯。 - 加載代理類:使用指定的類加載器將生成的代理類字節(jié)碼加載到JVM中,使其成為一個(gè)可使用的類。類加載器在這個(gè)過(guò)程中起著關(guān)鍵作用,它負(fù)責(zé)將字節(jié)碼轉(zhuǎn)換為JVM能夠理解和處理的類對(duì)象。不同的類加載器可能會(huì)導(dǎo)致代理類在不同的命名空間中加載,這對(duì)于處理類的隔離和安全性等問(wèn)題非常重要。
- 創(chuàng)建代理對(duì)象:通過(guò)反射機(jī)制創(chuàng)建代理類的實(shí)例,并且將我們實(shí)現(xiàn)的
InvocationHandler
實(shí)例傳遞給代理對(duì)象。這個(gè)InvocationHandler
實(shí)例就像是代理對(duì)象的“大腦”,決定了代理對(duì)象在接收到方法調(diào)用時(shí)應(yīng)該如何處理。代理對(duì)象在創(chuàng)建時(shí)會(huì)將InvocationHandler
實(shí)例保存起來(lái),以便在方法調(diào)用時(shí)能夠調(diào)用到正確的invoke
方法。
當(dāng)代理對(duì)象的方法被調(diào)用時(shí),實(shí)際上是調(diào)用了InvocationHandler
實(shí)現(xiàn)類中的invoke
方法。在invoke
方法中,我們可以通過(guò)反射調(diào)用目標(biāo)對(duì)象的方法,并且可以在調(diào)用前后添加自己的邏輯。具體來(lái)說(shuō),invoke
方法接收三個(gè)參數(shù):代理對(duì)象本身、被調(diào)用的方法對(duì)象以及方法的參數(shù)列表。通過(guò)這些參數(shù),我們可以獲取到關(guān)于方法調(diào)用的詳細(xì)信息,并根據(jù)需要進(jìn)行處理。
3. JDK動(dòng)態(tài)代理的使用步驟
3.1定義接口
首先,我們需要定義一個(gè)接口,這個(gè)接口定義了目標(biāo)對(duì)象的方法簽名。目標(biāo)對(duì)象和代理對(duì)象都需要實(shí)現(xiàn)這個(gè)接口。這就像是給明星(目標(biāo)對(duì)象)和經(jīng)紀(jì)人(代理對(duì)象)都規(guī)定了一套可以做的事情(方法)的規(guī)范。
例如:
interface HelloWorld { void sayHello(); }
在這個(gè)接口中,我們定義了一個(gè)sayHello
方法,它沒(méi)有參數(shù)也沒(méi)有返回值。這個(gè)方法就是我們后續(xù)要在目標(biāo)對(duì)象和代理對(duì)象中進(jìn)行操作的方法。
3.2創(chuàng)建目標(biāo)對(duì)象
創(chuàng)建一個(gè)實(shí)現(xiàn)了上述接口的目標(biāo)對(duì)象類,這個(gè)類就是我們實(shí)際要被代理的對(duì)象,里面包含了真正的業(yè)務(wù)邏輯,也就是明星具體要做的事情。
class HelloWorldImpl implements HelloWorld { @Override public void sayHello() { System.out.println("Hello, World!"); } }
在HelloWorldImpl
類中,我們實(shí)現(xiàn)了HelloWorld
接口的sayHello
方法,當(dāng)調(diào)用這個(gè)方法時(shí),它會(huì)在控制臺(tái)輸出"Hello, World!"。這就是目標(biāo)對(duì)象的核心業(yè)務(wù)邏輯。
3.3創(chuàng)建InvocationHandler實(shí)現(xiàn)類
創(chuàng)建一個(gè)類實(shí)現(xiàn)InvocationHandler
接口,在invoke
方法中實(shí)現(xiàn)對(duì)目標(biāo)方法的攔截和處理邏輯。這個(gè)類就像是經(jīng)紀(jì)人的工作手冊(cè),告訴經(jīng)紀(jì)人在面對(duì)不同情況(方法調(diào)用)時(shí)應(yīng)該怎么做。
class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before method invocation"); Object result = method.invoke(target, args); System.out.println("After method invocation"); return result; } }
在MyInvocationHandler
類中,我們首先通過(guò)構(gòu)造函數(shù)接收一個(gè)目標(biāo)對(duì)象,然后在invoke
方法中,我們可以看到,在調(diào)用目標(biāo)方法之前,先輸出了"Before method invocation",然后通過(guò)method.invoke(target, args)
調(diào)用了目標(biāo)對(duì)象的方法,這里使用了反射機(jī)制來(lái)調(diào)用目標(biāo)對(duì)象的方法,確保能夠正確地執(zhí)行目標(biāo)對(duì)象的業(yè)務(wù)邏輯。最后在調(diào)用之后輸出了"After method invocation"。通過(guò)這種方式,我們就可以在目標(biāo)方法調(diào)用的前后插入自己的邏輯代碼,實(shí)現(xiàn)對(duì)目標(biāo)方法的增強(qiáng)。
3.4創(chuàng)建代理對(duì)象
使用Proxy
類的newProxyInstance
方法創(chuàng)建代理對(duì)象。這個(gè)方法就像是一個(gè)魔法工廠,根據(jù)我們提供的信息生成代理對(duì)象。
HelloWorld proxy = (HelloWorld) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler );
這里我們傳入了目標(biāo)對(duì)象的類加載器、目標(biāo)對(duì)象實(shí)現(xiàn)的接口數(shù)組以及InvocationHandler
實(shí)現(xiàn)類的實(shí)例。Proxy.newProxyInstance
方法會(huì)根據(jù)這些參數(shù)動(dòng)態(tài)地創(chuàng)建一個(gè)代理對(duì)象,這個(gè)代理對(duì)象實(shí)現(xiàn)了HelloWorld
接口,并且在內(nèi)部關(guān)聯(lián)了我們創(chuàng)建的MyInvocationHandler
實(shí)例。
3.5調(diào)用代理對(duì)象方法
通過(guò)代理對(duì)象調(diào)用方法,實(shí)際上就會(huì)調(diào)用InvocationHandler
實(shí)現(xiàn)類中的invoke
方法,我們就可以在這個(gè)方法中決定是否調(diào)用目標(biāo)對(duì)象的方法以及在調(diào)用前后執(zhí)行一些額外的邏輯。
proxy.sayHello();
當(dāng)我們調(diào)用proxy.sayHello()
時(shí),就會(huì)先執(zhí)行MyInvocationHandler
中的invoke
方法中的前置邏輯,然后調(diào)用目標(biāo)對(duì)象的sayHello
方法,最后執(zhí)行后置邏輯。在控制臺(tái)中,我們會(huì)看到先輸出"Before method invocation",然后輸出"Hello, World!",最后輸出"After method invocation"。
4. JDK動(dòng)態(tài)代理的應(yīng)用場(chǎng)景
4.1AOP(面向切面編程)
在AOP中,動(dòng)態(tài)代理是實(shí)現(xiàn)切面邏輯的重要手段。比如我們想要在多個(gè)不同的業(yè)務(wù)方法中都添加日志記錄功能,就可以使用動(dòng)態(tài)代理。通過(guò)代理,在不修改目標(biāo)對(duì)象代碼的情況下,在方法調(diào)用前后插入日志記錄的邏輯,實(shí)現(xiàn)了業(yè)務(wù)邏輯和日志記錄邏輯的分離,提高了代碼的可維護(hù)性和可擴(kuò)展性。
例如,我們有一個(gè)用戶管理系統(tǒng),其中包含了添加用戶、刪除用戶、修改用戶等多個(gè)業(yè)務(wù)方法。我們可以使用JDK動(dòng)態(tài)代理為這些方法添加日志記錄功能,記錄每個(gè)方法的調(diào)用時(shí)間、參數(shù)和返回值等信息。這樣,當(dāng)出現(xiàn)問(wèn)題時(shí),我們可以方便地通過(guò)日志來(lái)排查問(wèn)題,而不需要在每個(gè)業(yè)務(wù)方法中都手動(dòng)添加日志記錄代碼。
4.2遠(yuǎn)程代理
當(dāng)我們需要訪問(wèn)遠(yuǎn)程對(duì)象時(shí),比如調(diào)用遠(yuǎn)程服務(wù)器上的服務(wù),就可以使用動(dòng)態(tài)代理創(chuàng)建一個(gè)本地代理對(duì)象。這個(gè)代理對(duì)象就像是一個(gè)本地的“代表”,負(fù)責(zé)和遠(yuǎn)程對(duì)象進(jìn)行通信,把我們?cè)诒镜氐姆椒ㄕ{(diào)用轉(zhuǎn)換為遠(yuǎn)程服務(wù)器上的方法調(diào)用,并且把結(jié)果返回給我們。對(duì)于我們調(diào)用者來(lái)說(shuō),就好像是在調(diào)用本地對(duì)象一樣,感覺(jué)不到遠(yuǎn)程調(diào)用的復(fù)雜性。
例如,我們的應(yīng)用程序需要調(diào)用一個(gè)遠(yuǎn)程的天氣預(yù)報(bào)服務(wù),獲取某個(gè)城市的天氣信息。我們可以使用動(dòng)態(tài)代理創(chuàng)建一個(gè)本地的代理對(duì)象,這個(gè)代理對(duì)象實(shí)現(xiàn)了與遠(yuǎn)程天氣預(yù)報(bào)服務(wù)相同的接口。當(dāng)我們?cè)诒镜卣{(diào)用代理對(duì)象的方法時(shí),代理對(duì)象會(huì)將請(qǐng)求發(fā)送到遠(yuǎn)程服務(wù)器,獲取天氣信息,并將結(jié)果返回給我們。這樣,我們就可以在不關(guān)心遠(yuǎn)程調(diào)用細(xì)節(jié)的情況下,方便地使用遠(yuǎn)程服務(wù)。
4.3延遲加載
有時(shí)候,我們可能不想在程序啟動(dòng)時(shí)就加載所有的對(duì)象,而是希望在真正使用對(duì)象時(shí)才去加載它的具體實(shí)現(xiàn)。這時(shí)候,動(dòng)態(tài)代理就可以發(fā)揮作用了。我們可以在代理對(duì)象中實(shí)現(xiàn)延遲加載邏輯,只有在調(diào)用相關(guān)方法時(shí),才去創(chuàng)建實(shí)際的目標(biāo)對(duì)象,這樣可以提高系統(tǒng)的性能和資源利用率,避免了不必要的資源浪費(fèi)。
例如,在一個(gè)大型的企業(yè)級(jí)應(yīng)用中,可能有很多模塊和對(duì)象,有些對(duì)象在程序啟動(dòng)時(shí)并不需要立即使用,但如果在啟動(dòng)時(shí)就加載所有的對(duì)象,會(huì)導(dǎo)致啟動(dòng)時(shí)間過(guò)長(zhǎng),占用大量的內(nèi)存資源。我們可以使用動(dòng)態(tài)代理對(duì)這些對(duì)象進(jìn)行代理,只有在真正調(diào)用這些對(duì)象的方法時(shí),才去加載它們的具體實(shí)現(xiàn),從而提高系統(tǒng)的性能和響應(yīng)速度。
4.4事務(wù)管理
在數(shù)據(jù)庫(kù)操作中,事務(wù)管理是非常重要的一部分。我們可以使用JDK動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)操作方法的事務(wù)管理。通過(guò)代理,在方法調(diào)用之前開(kāi)啟事務(wù),在方法調(diào)用成功后提交事務(wù),在方法調(diào)用出現(xiàn)異常時(shí)回滾事務(wù)。這樣可以確保數(shù)據(jù)庫(kù)操作的一致性和完整性,避免數(shù)據(jù)不一致的情況發(fā)生。
例如,在一個(gè)銀行轉(zhuǎn)賬系統(tǒng)中,我們有一個(gè)轉(zhuǎn)賬方法,需要在轉(zhuǎn)賬操作前后進(jìn)行事務(wù)管理。我們可以使用動(dòng)態(tài)代理為轉(zhuǎn)賬方法添加事務(wù)管理邏輯,確保轉(zhuǎn)賬操作要么全部成功,要么全部失敗,不會(huì)出現(xiàn)部分成功部分失敗的情況,保證了數(shù)據(jù)的一致性。
5. JDK動(dòng)態(tài)代理的局限性
5.1只能代理接口
JDK動(dòng)態(tài)代理要求目標(biāo)對(duì)象必須實(shí)現(xiàn)一個(gè)接口,因?yàn)榇眍愂峭ㄟ^(guò)實(shí)現(xiàn)相同接口來(lái)代理目標(biāo)對(duì)象的。如果我們的目標(biāo)對(duì)象沒(méi)有實(shí)現(xiàn)接口,那么就無(wú)法使用JDK動(dòng)態(tài)代理了。這就好像我們的明星(目標(biāo)對(duì)象)沒(méi)有按照規(guī)定的規(guī)范(接口)來(lái)做事,經(jīng)紀(jì)人(代理類)就不知道該怎么代理了。
例如,如果我們有一個(gè)沒(méi)有實(shí)現(xiàn)任何接口的具體類SomeClass
,我們就無(wú)法直接使用JDK動(dòng)態(tài)代理來(lái)為它創(chuàng)建代理對(duì)象。在這種情況下,我們可能需要考慮使用其他的代理技術(shù),如CGLIB代理,它可以對(duì)沒(méi)有實(shí)現(xiàn)接口的類進(jìn)行代理。
5.2對(duì)final方法無(wú)法代理
由于Java的final
方法不能被重寫,所以JDK動(dòng)態(tài)代理無(wú)法對(duì)目標(biāo)對(duì)象中的final
方法進(jìn)行代理和攔截。就好像明星(目標(biāo)對(duì)象)有一些事情(final
方法)是絕對(duì)不能被別人干預(yù)和改變的,經(jīng)紀(jì)人(代理類)也沒(méi)辦法對(duì)這些事情進(jìn)行額外的操作。
例如,在目標(biāo)對(duì)象類中有一個(gè)final
方法finalMethod
,當(dāng)我們使用JDK動(dòng)態(tài)代理創(chuàng)建代理對(duì)象后,調(diào)用這個(gè)finalMethod
方法時(shí),代理對(duì)象無(wú)法對(duì)其進(jìn)行攔截和增強(qiáng),而是直接調(diào)用目標(biāo)對(duì)象的finalMethod
方法。這是因?yàn)?code>final方法的特性決定了它不能被重寫,而JDK動(dòng)態(tài)代理是通過(guò)重寫接口方法來(lái)實(shí)現(xiàn)代理邏輯的。
6. 與其他代理技術(shù)的比較
在Java中,除了JDK動(dòng)態(tài)代理,還有其他的代理技術(shù),如CGLIB代理和Javassist代理等。這些代理技術(shù)各有特點(diǎn),與JDK動(dòng)態(tài)代理相比,主要有以下一些區(qū)別:
- CGLIB代理:CGLIB代理是通過(guò)繼承目標(biāo)類來(lái)實(shí)現(xiàn)代理的,它不需要目標(biāo)類實(shí)現(xiàn)接口。這使得CGLIB代理可以對(duì)沒(méi)有實(shí)現(xiàn)接口的類進(jìn)行代理,彌補(bǔ)了JDK動(dòng)態(tài)代理只能代理接口的局限性。但是,由于CGLIB代理是通過(guò)繼承實(shí)現(xiàn)的,所以不能對(duì)
final
類和final
方法進(jìn)行代理。 - Javassist代理:Javassist是一個(gè)字節(jié)碼操作庫(kù),它可以在運(yùn)行時(shí)動(dòng)態(tài)地生成和修改Java類的字節(jié)碼。Javassist代理可以通過(guò)修改目標(biāo)類的字節(jié)碼來(lái)實(shí)現(xiàn)代理邏輯,它的靈活性較高,可以對(duì)類和方法進(jìn)行更細(xì)粒度的控制。但是,Javassist代理的使用相對(duì)復(fù)雜一些,需要對(duì)字節(jié)碼操作有一定的了解。
與這些代理技術(shù)相比,JDK動(dòng)態(tài)代理的優(yōu)點(diǎn)在于它是Java原生的代理技術(shù),不需要引入額外的依賴庫(kù),并且在代理接口方面具有簡(jiǎn)單易用的特點(diǎn)。但是,在面對(duì)不能實(shí)現(xiàn)接口的類或者需要對(duì)final
方法進(jìn)行代理等情況時(shí),就需要考慮使用其他的代理技術(shù)了。
以上就是JDK動(dòng)態(tài)代理的深入理解與實(shí)際應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于JDK動(dòng)態(tài)代理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java中 利用正則表達(dá)式提取( )內(nèi)內(nèi)容
本篇文章,小編為大家介紹關(guān)于java中 利用正則表達(dá)式提取( )內(nèi)內(nèi)容,有需要的朋友可以參考一下2013-04-04Java為何需要平衡方法調(diào)用與內(nèi)聯(lián)
這篇文章主要介紹了Java為何需要平衡方法調(diào)用與內(nèi)聯(lián),幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2021-01-01Java 數(shù)據(jù)結(jié)構(gòu)與算法系列精講之KMP算法
在很多地方也都經(jīng)??吹街v解KMP算法的文章,看久了好像也知道是怎么一回事,但總感覺(jué)有些地方自己還是沒(méi)有完全懂明白。這兩天花了點(diǎn)時(shí)間總結(jié)一下,有點(diǎn)小體會(huì),我希望可以通過(guò)我自己的語(yǔ)言來(lái)把這個(gè)算法的一些細(xì)節(jié)梳理清楚,也算是考驗(yàn)一下自己有真正理解這個(gè)算法2022-02-02Java中OAuth2.0第三方授權(quán)原理與實(shí)戰(zhàn)
本文主要介紹了Java中OAuth2.0第三方授權(quán)原理與實(shí)戰(zhàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Java?Mybatis的初始化之Mapper.xml映射文件的詳解
這篇文章主要介紹了Java?Mybatis的初始化之Mapper.xml映射文件的詳解,解析完全局配置文件后接下來(lái)就是解析Mapper文件了,它是通過(guò)XMLMapperBuilder來(lái)進(jìn)行解析的2022-08-08Java實(shí)現(xiàn)經(jīng)典俄羅斯方塊游戲
俄羅斯方塊是一個(gè)最初由阿列克謝帕吉特諾夫在蘇聯(lián)設(shè)計(jì)和編程的益智類視頻游戲。本文將利用Java實(shí)現(xiàn)這一經(jīng)典的小游戲,需要的可以參考一下2022-01-01解決阿里云OSS使用URL無(wú)法訪問(wèn)圖片的兩種方法
這篇文章主要介紹了解決阿里云OSS使用URL無(wú)法訪問(wèn)圖片的兩種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Spring FactoriesLoader機(jī)制實(shí)例詳解
這篇文章主要介紹了Spring FactoriesLoader機(jī)制實(shí)例詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03springboot使用Validator校驗(yàn)方式
這篇文章主要介紹了springboot使用Validator校驗(yàn)方式,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01