JDK動態(tài)代理接口和接口實(shí)現(xiàn)類深入詳解
前言
在Java開發(fā)中,JDK動態(tài)代理是一種非常有用的技術(shù),它允許開發(fā)者在不修改目標(biāo)類代碼的情況下,為目標(biāo)類添加額外的功能。然而,JDK動態(tài)代理的使用有一些限制,特別是它只能代理接口和接口實(shí)現(xiàn)類。本文將深入探討這一限制的原因。
1.JDK動態(tài)代理原理
下面是一個(gè)簡單的動態(tài)代理的例子
/**
* 要代理的接口
*/
public interface Target{
String say();
}
/**
* 真實(shí)調(diào)用對象
*/
public class RealTarget {
public String invoke(){
return "i'm proxy";
}
}
/**
* JDK代理類生成
*/
public class JDKProxy implements InvocationHandler {
private Object target;
JDKProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] paramValues) {
System.out.println("Before method invocation");
Object result = ((RealTarget)target).invoke();
System.out.println("After method invocation");
return result;
}
}
/**
* 測試?yán)?
*/
public class TestProxy {
public static void main(String[] args){
// 構(gòu)建代理器
JDKProxy proxy = new JDKProxy(new RealTarget());
ClassLoader classLoader = ClassLoaderUtils.getCurrentClassLoader();
// 把生成的代理類保存到文件
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
// 生成代理類
Target test = (Target) Proxy.newProxyInstance(classLoader, new Class[]{Target.class}, proxy);
// 方法調(diào)用
System.out.println(test.say());
}
}這段代碼想表達(dá)的意思就是:給 Target接口生成一個(gè)動態(tài)代理類,并調(diào)用接口 say() 方法,但真實(shí)返回的值居然是來自 RealTarget里面的 invoke() 方法返回值。你看,短短50行的代碼,就完成了這個(gè)功能,是不是還挺有意思的?
那既然重點(diǎn)是代理類的生成,那我們就去看下 Proxy.newProxyInstance 里面究竟發(fā)生了什么?
一起看下下面的流程圖,具體代碼細(xì)節(jié)你可以對照著 JDK 的源碼看(上文中有類和方法,可以直接定位),我是按照 1.7.X 版本梳理的。

在生成字節(jié)碼的那個(gè)地方,也就是 ProxyGenerator.generateProxyClass() 方法里面,通過代碼我們可以看到,里面是用參數(shù) saveGeneratedFiles 來控制是否把生成的字節(jié)碼保存到本地磁盤。同時(shí)為了更直觀地了解代理的本質(zhì),我們需要把參數(shù) saveGeneratedFiles 設(shè)置成true,但這個(gè)參數(shù)的值是由key為“sun.misc.ProxyGenerator.saveGeneratedFiles”的Property來控制的,動態(tài)生成的類會保存在工程根目錄下的 com/sun/proxy 目錄里面?,F(xiàn)在我們找到剛才生成的 $Proxy0.class,通過反編譯工具打開class文件,你會看到這樣的代碼:
package com.sun.proxy;
import com.proxy.Hello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements Target {
private static Method m3;
private static Method m1;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler paramInvocationHandler) {
super(paramInvocationHandler);
}
public final String say() {
try {
return (String)this.h.invoke(this, m3, null);
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final boolean equals(Object paramObject) {
try {
return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode() {
try {
return ((Integer)this.h.invoke(this, m0, null)).intValue();
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString() {
try {
return (String)this.h.invoke(this, m2, null);
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
static {
try {
m3 = Class.forName("com.proxy.Target").getMethod("say", new Class[0]);
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
return;
} catch (NoSuchMethodException noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
} catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}
}我們可以看到 $Proxy0 類里面有一個(gè)跟 Target一樣簽名的 say() 方法,其中 this.h 綁定的是剛才傳入的 JDKProxy 對象,所以當(dāng)我們調(diào)用 Target.say() 的時(shí)候,其實(shí)它是被轉(zhuǎn)發(fā)到了JDKProxy.invoke()。到這兒,整個(gè)魔術(shù)過程就透明了。
2.回答JDK動態(tài)代理的疑問
為何只能代理具有接口的類
這是因?yàn)镴DK動態(tài)代理的機(jī)制所限。在Java中,動態(tài)代理通過Proxy.newProxyInstance()方法實(shí)現(xiàn),此方法要求傳入一個(gè)接口類作為被代理對象。這源于JDK動態(tài)代理的底層實(shí)現(xiàn):它在程序運(yùn)行時(shí)動態(tài)生成一個(gè)名為$Proxy0的代理類,該代理類繼承自java.lang.reflect.Proxy并實(shí)現(xiàn)了被代理的接口。由于Java不支持多重繼承,每個(gè)動態(tài)代理類都繼承自Proxy,因此只能代理接口,而無法代理具體實(shí)現(xiàn)類。
JDK動態(tài)代理能否代理類
JDK中的Proxy類主要用于保存動態(tài)代理的處理器InvocationHandler。理論上,如果不通過Proxy類而直接在動態(tài)生成的代理類內(nèi)部設(shè)置處理器,可能實(shí)現(xiàn)對類的動態(tài)代理。然而,JDK的設(shè)計(jì)者并未采取這種方式,這主要是出于設(shè)計(jì)上的考慮和限制。
為何這樣設(shè)計(jì)
- 使用場景與需求:動態(tài)代理的主要用途是在不修改原始實(shí)現(xiàn)的前提下,對方法進(jìn)行攔截以實(shí)現(xiàn)功能增強(qiáng)或擴(kuò)展。在實(shí)際開發(fā)中,基于接口編程是常見模式,因此基于接口實(shí)現(xiàn)動態(tài)代理符合需求和場景。當(dāng)然,也存在沒有實(shí)現(xiàn)接口的類,此時(shí)JDK動態(tài)代理無法滿足需求。
- 代碼重用與擴(kuò)展性:在Java中,類的設(shè)計(jì)更注重共性能力的抽象,以提高代碼的重用性和擴(kuò)展性。動態(tài)代理也遵循這一原則,它封裝了代理類的生成邏輯、接口判斷以及
InvocationHandler的持有等,將這些抽象邏輯放在Proxy父類中是一個(gè)合理的選擇。
其他實(shí)現(xiàn)方式
對于需要代理沒有接口的類,可以選擇使用CGLIB動態(tài)代理。CGLIB通過動態(tài)生成被代理類的子類,并重寫非final修飾的方法,在子類中攔截父類方法的調(diào)用,從而實(shí)現(xiàn)動態(tài)代理。這種方式彌補(bǔ)了JDK動態(tài)代理只能代理接口的不足。
3.總結(jié)
JDK動態(tài)代理是Java中一種強(qiáng)大而靈活的技術(shù),它允許在不修改原始代碼的情況下對目標(biāo)對象的方法進(jìn)行功能增強(qiáng)。然而,由于其基于接口的代理機(jī)制,它只能代理接口和接口實(shí)現(xiàn)類。對于需要代理沒有實(shí)現(xiàn)接口的類的情況,可以考慮使用CGLIB動態(tài)代理等替代方案。在實(shí)際開發(fā)中,應(yīng)根據(jù)具體需求選擇合適的代理機(jī)制,以實(shí)現(xiàn)最佳的性能和可維護(hù)性。
到此這篇關(guān)于JDK動態(tài)代理接口和接口實(shí)現(xiàn)類深入詳解的文章就介紹到這了,更多相關(guān)JDK動態(tài)代理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 實(shí)現(xiàn)瀏覽器下載文件及文件預(yù)覽
這篇文章主要介紹了Java 實(shí)現(xiàn)瀏覽器下載文件及文件預(yù)覽,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
Springboot基于Redisson實(shí)現(xiàn)Redis分布式可重入鎖源碼解析
這篇文章主要介紹了Springboot基于Redisson實(shí)現(xiàn)Redis分布式可重入鎖,本文通過案例源碼分析給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
Spring整合mybatis實(shí)現(xiàn)過程詳解
這篇文章主要介紹了Spring整合mybatis實(shí)現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
解決SpringMVC使用@RequestBody注解報(bào)400錯(cuò)誤的問題
這篇文章主要介紹了解決SpringMVC使用@RequestBody注解報(bào)400錯(cuò)誤的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09
SpringBoot從Nacos讀取MySQL數(shù)據(jù)庫配置錯(cuò)誤:Public Key Retrieva
最近的項(xiàng)目,突然都從MySQL5.7升級到8.0了,有些項(xiàng)目能運(yùn)行成功,有些項(xiàng)目遇到了問題,啟動不成功,顯示數(shù)據(jù)庫方面的異常信息,本文給大家介紹了SpringBoot從Nacos讀取MySQL數(shù)據(jù)庫配置錯(cuò)誤:Public Key Retrieval is not allowed的解決方案,需要的朋友可以參考下2024-04-04

