一文讀懂 Java 動態(tài)代理,那些面試中你容易忽略的細(xì)節(jié)!
寫前言
本來是打算把java代理模式給寫一下的,但是整理思路的時候發(fā)現(xiàn)這是一個龐大的工程,我需要講清楚什么是代理模式;它的應(yīng)用場景有哪些;代理又分為靜態(tài)代理和動態(tài)代理,它們分別是如何實現(xiàn)的,區(qū)別又是什么,我還要舉例,分析源碼,emm。顯然,我現(xiàn)在的時間安排是無法完成這個龐大的工程的,所以我就講一下目前解決問題中遇到的動態(tài)代理吧(默認(rèn)你大致了解代理模式)
為什么要寫這篇文章
我最近在學(xué)習(xí)Retrofit2源碼,而這個框架比較核心的一點就是動態(tài)代理,所以在這里把我學(xué)習(xí)過程中的一些我認(rèn)為比較關(guān)鍵的地方整理出來,分享給有需要的童鞋。
Retrofit2的動態(tài)代理到底體現(xiàn)在哪里?請看下面代碼
//retrofit的API接口對象
ApiService apiService;
//創(chuàng)建代理對象
apiService = retrofit.create(ApiService.class);
//調(diào)用代理類中的方法
apiService.xxx();
這是retrofit.create方法的源碼,很明顯的動態(tài)代理

動態(tài)代理的本質(zhì)是什么?
我的理解:
提供一個代理來控制對象的訪問; 程序運行時動態(tài)生成代理類,這個代理類繼承于Proxy,并且實現(xiàn)自定義的委托類的接口; 豐富原始類的操作
動態(tài)代理的具體實現(xiàn)
本來是不想寫一堆代碼來說動態(tài)代理的代碼是如何實現(xiàn)的,但是沒辦法,有些問題不通過舉例無法說清楚,下面一起來看一個簡單的例子吧:
學(xué)生每學(xué)期都需要參加期末考試的
public interface Student {
//參加考試
void exam();
}
學(xué)生分為很多專業(yè)的,其中計算機專業(yè)的學(xué)生需要參加計算機專業(yè)考試
public class ComputerStudent implements Student {
private String name;
public ComputerStudent(String name) {
this.name = name;
}
@Override
public void exam() {
System.out.println(name + " 參加計算機專業(yè)考試");
}
}
由于每個學(xué)生的專業(yè)能力不一致,有的需要在考試前刷刷題,有的需要去找學(xué)霸輔導(dǎo)一下(抱大腿),等等,總之每個學(xué)生在考試前都需要做一些事情。
如果現(xiàn)在需要定義很多不同特點的學(xué)生,你怎么做呢?如果去一個個定義不同的行為的話,那將是非常龐大的工作量,那有沒有簡單的辦法呢?答案是有的,通過代理類就可以實現(xiàn)。
事實上我們沒有必要去定義每個學(xué)生,因為每個學(xué)生的行為是沒辦法確定的,我們可以通過動態(tài)代理在它做這個動作的時候去實現(xiàn)他的特定行為。
public class Test {
public static void main(String[] args){
//生成$Proxy0的class文件
//System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//被代理類
final Student jack =new ComputerStudent("jack");
//生成代理對象
Student jackProxy= (Student) Proxy.newProxyInstance(jack.getClass().getClassLoader(),
jack.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before exam do something");
//通過反射調(diào)用對象的方法
method.invoke(jack,args);
System.out.println("after exam do something");
return null;
}
});
//代理方法調(diào)用
jackProxy.exam();
}
}
當(dāng)調(diào)用代理類的exam()方法,程序運行結(jié)果如下

這里有個重點,通過代理類對象jackProxy去調(diào)用方法和接口實現(xiàn)類對象jack去調(diào)用方法是有明顯區(qū)別的,通過代理方式去調(diào)用,可以在原來方法執(zhí)行前后做一些其它操作,這就是代理模式的特點
那些你容易忽略的細(xì)節(jié)
首先回顧一下動態(tài)代理的實現(xiàn)流程:
1、通過 Proxy.newProxyInstance方法創(chuàng)建一個代理對象;2、在內(nèi)部類 InvocationHandler的invoke()方法中做一些操作:利用反射調(diào)用被代理類(這里是ComputerStudent)中的方法,通常在這個調(diào)用方法前后還會做一些其它操作;3、代理對象jackProxy調(diào)用方法 對整個流程有個了解之后,下面來看一些細(xì)節(jié)問題。
Q1:Proxy.newProxyInstance中的3個參數(shù)到底是什么?為什么要穿入這3個參數(shù)?
系統(tǒng)接口定義如下:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
loader:定義代理類的類加載器,這里要代理的是jack,所以用jack的類加載器interfaces:是一個接口類的集合,具體來說是代理類實現(xiàn)的接口的集合,也是被代理類實現(xiàn)的接口的集合;h:代理類對象調(diào)用方法時需要用到的一個接口對象,在系統(tǒng)生成的代理類內(nèi)部會用到它。
到這里,我想細(xì)心的童鞋會想這個代理類到底是什么?似乎從頭到尾沒有露面過。的確是這樣,即使你去翻遍源碼你也找不到這個代理類,因為在動態(tài)代理模式中它是在運行時生成的,所以你在源碼甚至.class中都找不到他的影子。
我先說結(jié)論:運行時動態(tài)生成的代理類叫做$Proxy0,關(guān)于這個類怎么看源碼,很多介紹代理的文章都沒有說清楚,讀者也是一臉懵逼;如果你想要看它的內(nèi)容,可以通過如下方法
public class Test {
public static void main(String[] args){
//生成$Proxy0的class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//被代理類
Student jack =new ComputerStudent("jack");
//生成代理對象
Student jackProxy= (Student) Proxy.newProxyInstance();
}
在運行方法中添加這一行代碼,在運行后會自動在項目根目錄生成com文件夾,其中...\IdeaProjects\com\sun\proxy下會生成$Proxy0.class文件。注意:我是在IDEA上調(diào)試成功的,我在Android Studio上測試是沒有生成的。暫時不知道原理,有了解的大佬可以科普一下。System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
代理類$Proxy0源碼如下:
package com.sun.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import student.Student;
public final class $Proxy0 extends Proxy implements Student {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void exam() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (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("student.Student").getMethod("exam");
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());
}
}
}
關(guān)于$Proxy0先說幾個結(jié)論:
$Proxy0繼承了Proxy,實現(xiàn)了自定義的目標(biāo)接口Student;$Proxy0定義了接口Student中的方法exam(),以及Object對象的幾個方法equals()、toString()、hashCode()構(gòu)造方法中傳了 InvocationHandler對象,并且在定義的方法中調(diào)用了它的invoke()方法
回到剛才的問題,Proxy.newProxyInstance為什么要穿入這3個參數(shù)?
//生成代理對象
Student jackProxy= (Student) Proxy.newProxyInstance(jack.getClass().getClassLoader(),
jack.getClass().getInterfaces(),
new InvocationHandler() );
因為代理對象是基于自定義接口Student和jack類加載器代理出來的。
Q2:InvocationHandler的作用及其invoke()方法等解釋
當(dāng)代理對象調(diào)用方法時,會回調(diào)執(zhí)行剛才new出來的InvocationHandler中的invoke()方法。
關(guān)于invoke()方法,在看了源碼和反復(fù)代碼驗證之后,我做出的解釋如下:
/**
* 這個方法不是我們顯示的去調(diào)用,是系統(tǒng)生成的代理類$Proxy0中調(diào)用的
*
* @param proxy 代理對象:也就是Proxy.newProxyInstance返回的對象
* @param method 要調(diào)用的方法
* @param args 調(diào)用方法的參數(shù)
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {}
注意: invoke()不是顯示調(diào)用的,是在代理類中去調(diào)用的。比如調(diào)用exam()方法時, 該方法中會調(diào)用super.h.invoke(this, m3, null);,就是調(diào)用父類的h的invoke(),它的父類是Proxy,h是一個InvocationHandler對象;所以說當(dāng)調(diào)用exam()方法時最后回調(diào)到剛才new出來的InvocationHandler的invoke方法。
public final void exam() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
下面再來看一個問題
public class Test {
public static void main(String[] args){
//生成$Proxy0的class文件
//System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//被代理類
final Student jack =new ComputerStudent("jack");
//生成代理對象
Student jackProxy= (Student) Proxy.newProxyInstance(jack.getClass().getClassLoader(),
jack.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before exam do something");
//通過反射調(diào)用對象的方法
method.invoke(jack,args);
System.out.println("after exam do something");
return null;
}
});
//方法調(diào)用
jackProxy.exam();
}
}
看一下InvocationHandler中invoke()方法內(nèi)部的調(diào)用
//通過反射調(diào)用對象的方法
method.invoke(jack,args);
這句代碼的作用是通過反射調(diào)用一個方法,如果把實現(xiàn)類對象jack換成代理類對象proxy會發(fā)生什么?
結(jié)論:會循環(huán)報錯,停不下來那種。因為proxy是代理實例,也就是這里的jackProxy,當(dāng)這個對象的方法被調(diào)用的時候會觸發(fā)InvocationHandler中invoke()方法,而InvocationHandler內(nèi)部又再次調(diào)用proxy的方法,如此不停循環(huán)。
動態(tài)代理的使用場景
優(yōu)點:在運行時切入原始類,改變類的方法,這樣可以豐富該方法的操作,比如在方法之前、之后做一些其它操作。
應(yīng)用的話,比如Retrofit框架、AOP(面向切面編程)等等。
推薦閱讀
? 從零搭建SpringCloud服務(wù)(史上最詳細(xì))
? 公司高管寫出低級Bug,導(dǎo)致公司70GB數(shù)據(jù)泄露
? 955 互聯(lián)網(wǎng)公司白名單來了!這些公司月薪20k,沒有996!福利榜國內(nèi)大廠只有這家!
?一個完整的、全面k8s化的集群穩(wěn)定架構(gòu)(值得借鑒)
? 2020年國內(nèi)互聯(lián)網(wǎng)公司的薪酬排名!
? 基于SpringBoot 的CMS系統(tǒng),拿去開發(fā)企業(yè)官網(wǎng)真香
? 程序員因違反竟業(yè)協(xié)議,賠騰訊97.6萬...
? 阿里一面:如何保證API接口數(shù)據(jù)安全?
? 徒手?jǐn)]了一個RPC框架,理解更透徹了,代碼已上傳github,自取~
