新聞中心
如果不出意外的話,這篇文章發(fā)出的時間是2022年2月22日的22點22,農(nóng)歷正月廿二,星期二。畢竟是個有點意思的日期,不發(fā)點什么總感覺有點浪費,畢竟我們大部分人大概率都活不到2222年的2月22日~

甘谷網(wǎng)站建設公司創(chuàng)新互聯(lián)建站,甘谷網(wǎng)站設計制作,有大型網(wǎng)站制作公司豐富經(jīng)驗。已為甘谷上1000家提供企業(yè)網(wǎng)站建設服務。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站建設公司要多少錢,請找那個售后服務好的甘谷做網(wǎng)站的公司定做!
哈哈扯遠了,前幾天的時候,交流群里的小伙伴拋出了一個問題,為什么JDK的動態(tài)代理一定要基于接口實現(xiàn)呢?
好的安排,其實要想弄懂這個問題還是需要一些關(guān)于代理和反射的底層知識的,我們今天就盤一盤這個問題,走你~
一個簡單的例子
在分析原因之前,我們先完整的看一下實現(xiàn)jdk動態(tài)代理需要幾個步驟,首先需要定義一個接口:
public interface Worker {
void work();
}
再寫一個基于這個接口的實現(xiàn)類:
public class Programmer implements Worker {
@Override
public void work() {
System.out.println("coding...");
}
}
自定義一個Handler,實現(xiàn)InvocationHandler接口,通過重寫內(nèi)部的invoke方法實現(xiàn)邏輯增強。其實這個InvocationHandler可以使用匿名內(nèi)部類的形式定義,這里為了結(jié)構(gòu)清晰拿出來單獨聲明。
public class WorkHandler implements InvocationHandler {
private Object target;
WorkHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("work")) {
System.out.println("before work...");
Object result = method.invoke(target, args);
System.out.println("after work...");
return result;
}
return method.invoke(target, args);
}
}
在main方法中進行測試,使用Proxy類的靜態(tài)方法newProxyInstance生成一個代理對象并調(diào)用方法:
public static void main(String[] args) {
Programmer programmer = new Programmer();
Worker worker = (Worker) Proxy.newProxyInstance(
programmer.getClass().getClassLoader(),
programmer.getClass().getInterfaces(),
new WorkHandler(programmer));
worker.work();
}
執(zhí)行上面的代碼,輸出:
before work...
coding...
after work...
可以看到,執(zhí)行了方法邏輯的增強,到這,一個簡單的動態(tài)代理過程就實現(xiàn)了,下面我們分析一下源碼。
Proxy源碼解析
既然是一個代理的過程,那么肯定存在原生對象和代理對象之分,下面我們查看源碼中是如何動態(tài)的創(chuàng)建代理對象的過程。上面例子中,創(chuàng)建代理對象調(diào)用的是Proxy類的靜態(tài)方法newProxyInstance,查看一下源碼:
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,Class>[] interfaces,InvocationHandler h) throws IllegalArgumentException{
Objects.requireNonNull(h);
final Class>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
}//省略catch
}
概括一下上面代碼中重點部分:
- 在checkProxyAccess方法中,進行參數(shù)驗證
- 在getProxyClass0方法中,生成一個代理類Class或者尋找已生成過的代理類的緩存
- 通過getConstructor方法,獲取生成的代理類的構(gòu)造方法
- 通過newInstance方法,生成實例對象,也就是最終的代理對象
上面這個過程中,獲取構(gòu)造方法和生成對象都是直接利用的反射,而需要重點看看的是生成代理類的方法getProxyClass0。
private static Class> getProxyClass0(ClassLoader loader,
Class>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
注釋寫的非常清晰,如果緩存中已經(jīng)存在了就直接從緩存中取,這里的proxyClassCache是一個WeakCache類型,如果緩存中目標classLoader和接口數(shù)組對應的類已經(jīng)存在,那么返回緩存的副本。如果沒有就使用ProxyClassFactory去生成Class對象。中間的調(diào)用流程可以省略,最終實際調(diào)用了ProxyClassFactory的apply方法生成Class。在apply方法中,主要做了下面3件事。
- 首先,根據(jù)規(guī)則生成文件名:
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
如果接口被定義為public公有,那么默認會使用com.sun.proxy作為包名,類名是$Proxy加上一個自增的整數(shù)值,初始時是0,因此生成的文件名是$Proxy0。
如果是非公有接口,那么會使用和被代理類一樣的包名,可以寫一個private接口的例子進行一下測試。
package com.hydra.test.face;
public class InnerTest {
private interface InnerInterface {
void run();
}
class InnerClazz implements InnerInterface {
@Override
public void run() {
System.out.println("go");
}
}
}
這時生成的代理類的包名為com.hydra.test.face,與被代理類相同:
- 然后,利用ProxyGenerator.generateProxyClass方法生成代理的字節(jié)碼數(shù)組:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
在generateProxyClass方法中,有一個重要的參數(shù)會發(fā)揮作用:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
如果這個屬性被配置為true,那么會把字節(jié)碼存儲到硬盤上的class文件中,否則不會保存臨時的字節(jié)碼文件。
- 最后,調(diào)用本地方法defineClass0生成Class對象:
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
返回代理類的Class后的流程我們在前面就已經(jīng)介紹過了,先獲得構(gòu)造方法,再使用構(gòu)造方法反射的方式創(chuàng)建代理對象。
神秘的代理對象
創(chuàng)建代理對象流程的源碼分析完了,我們可以先通過debug來看看上面生成的這個代理對象究竟是個什么:
和源碼中看到的規(guī)則一樣,是一個Class為$Proxy0的神秘對象,再看一下代理對象的Class的詳細信息:
類的全限定名是com.sun.proxy.$Proxy0,在上面我們提到過,這個類是在運行過程中動態(tài)生成的,并且程序執(zhí)行完成后,會自動刪除掉class文件。如果想要保留這個臨時文件不被刪除,就要修改我們上面提到的參數(shù),具體操作起來有兩種方式,第一種是在啟動VM參數(shù)中加入:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
第二種是在代碼中加入下面這一句,注意要加在生成動態(tài)代理對象之前:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
使用了上面兩種方式中的任意一種后,就可以保存下來臨時的字節(jié)碼文件了,需要注意這個文件生成的位置,并不是在target目錄下,而是生成在項目目錄下的com\sun\proxy中,正好和默認生成的包名對應。
拿到字節(jié)碼文件后,就可以使用反編譯工具來反編譯它了,這里使用jad在cmd下一條命令直接搞定:
jad -s java $Proxy0.class
看一下反編譯后$Proxy0.java文件的內(nèi)容,下面的代碼中,我只保留了核心部分,省略了無關(guān)緊要的equals、toString、hashCode方法的定義。
public final class $Proxy0 extends Proxy implements Worker{
public $Proxy0(InvocationHandler invocationhandler){
super(invocationhandler);
}
public final void work(){
try{
super.h.invoke(this, m3, null);
return;
}catch(Error _ex) { }
catch(Throwable throwable){
throw new UndeclaredThrowableException(throwable);
}
}
private static Method m3;
static {
try{
m3 = Class.forName("com.hydra.test.Worker").getMethod("work", new Class[0]);
//省略其他Method
}//省略catch
}
}
這個臨時生成的代理類$Proxy0中主要做了下面的幾件事:
- 在這個類的靜態(tài)代碼塊中,通過反射初始化了多個靜態(tài)方法Method變量,除了接口中的方法還有equals、toString、hashCode這三個方法
- 繼承父類Proxy,實例化的過程中會調(diào)用父類的構(gòu)造方法,構(gòu)造方法中傳入的invocationHandler對象實際上就是我們自定義的WorkHandler的實例
- 實現(xiàn)了自定義的接口Worker,并重寫了work方法,方法內(nèi)調(diào)用了InvocationHandler的invoke方法,也就是實際上調(diào)用了WorkHandler的invoke方法
- 省略的equals、toString、hashCode方法實現(xiàn)也一樣,都是調(diào)用super.h.invoke()方法
到這里,整體的流程就分析完了,我們可以用一張圖來簡要總結(jié)上面的過程:
為什么要有接口?
通過上面的分析,我們已經(jīng)知道了代理對象是如何生成的了,那么回到開頭的問題,為什么jdk的動態(tài)代理一定要基于接口呢?
其實如果不看上面的分析,我們也應該知道,要擴展一個類有常見的兩種方式,繼承父類或?qū)崿F(xiàn)接口。這兩種方式都允許我們對方法的邏輯進行增強,但現(xiàn)在不是由我們自己來重寫方法,而是要想辦法讓jvm去調(diào)用InvocationHandler中的invoke方法,也就是說代理類需要和兩個東西關(guān)聯(lián)在一起:
- 被代理類
- InvocationHandler
而jdk處理這個問題的方式是選擇繼承父類Proxy,并把InvocationHandler存在父類的對象中:
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
//...
}
通過父類Proxy的構(gòu)造方法,保存了創(chuàng)建代理對象過程中傳進來的InvocationHandler的實例,使用protected修飾保證了它可以在子類中被訪問和使用。但是同時,因為java是單繼承的,因此在繼承了Proxy后,只能通過實現(xiàn)目標接口的方式來實現(xiàn)方法的擴展,達到我們增強目標方法邏輯的目的。
扯點別的
其實看完源碼、弄明白代理對象生成的流程后,我們還可以用另一種方法實現(xiàn)動態(tài)代理:
public static void main(String[] args) throws Exception {
Class> proxyClass = Proxy.getProxyClass(Test3.class.getClassLoader(), Worker.class);
Constructor> constructor = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler workHandler = new WorkHandler(new Programmer());
Worker worker = (Worker) constructor.newInstance(workHandler);
worker.work();
}
運行結(jié)果與之前相同,這種寫法其實就是抽出了我們前面介紹的幾個核心方法,中間省略了一些參數(shù)的校驗過程,這種方式可以幫助大家熟悉jdk動態(tài)代理原理,但是在使用過程中還是建議大家使用標準方式,相對更加安全規(guī)范。
總結(jié)
本文從源碼以及實驗的角度,分析了jdk動態(tài)代理生成代理對象的流程,通過代理類的實現(xiàn)原理分析了為什么jdk動態(tài)代理一定要基于接口實現(xiàn)??偟膩碚f,jdk動態(tài)代理的應用還是非常廣泛的,例如在Spring、Mybatis以及Feign等很多框架中動態(tài)代理都被大量的使用,可以說學好jdk動態(tài)代理,對于我們閱讀這些框架的底層源碼還是很有幫助的。
新聞標題:JDK動態(tài)代理為什么必須要基于接口?
轉(zhuǎn)載來源:http://fisionsoft.com.cn/article/djioese.html


咨詢
建站咨詢
