動態代理是Java語言中非常經典的一種設計模式,也是所有設計模式中最難理解的一種。本文將通過一個簡單的例子模擬JDK動態代理實現,讓你徹底明白動態代理設計模式的本質,文章中可能會涉及到一些你沒有學習過的知識點或概念。如果恰好遇到了這些知識盲點,請先去學習這部分知識,再來閱讀這篇文章。
什么是代理
從字面意思來看,代理比較好理解,無非就是代為處理的意思。舉個例子,你在上大學的時候,總是喜歡逃課。因此,你拜托你的同學幫你答到,而自己卻窩在宿舍玩游戲... 你的這個同學恰好就充當了代理的作用,代替你去上課。
是的,你沒有看錯,代理就是這么簡單!
理解了代理的意思,你腦海中恐怕還有兩個巨大的疑問:
- 怎么實現代理模式
- 代理模式有什么實際用途
要理解這兩個問題,看一個簡單的例子:
public interface Flyable { void fly(); } public class Bird implements Flyable { @Override public void fly() { System.out.println("Bird is flying..."); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } }
很簡單的一個例子,用一個隨機睡眠時間模擬小鳥在空中的飛行時間。接下來問題來了,如果我要知道小鳥在天空中飛行了多久,怎么辦?
有人說,很簡單,在Bird->fly()方法的開頭記錄起始時間,在方法結束記錄完成時間,兩個時間相減就得到了飛行時間。
@Override public void fly() { long start = System.currentTimeMillis(); System.out.println("Bird is flying..."); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); }
的確,這個方法沒有任何問題,接下來加大問題的難度。如果Bird這個類來自於某個SDK(或者說Jar包)提供,你無法改動源碼,怎么辦?
一定會有人說,我可以在調用的地方這樣寫:
public static void main(String[] args) { Bird bird = new Bird(); long start = System.currentTimeMillis(); bird.fly(); long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); }
這個方案看起來似乎沒有問題,但其實你忽略了准備這些方法所需要的時間,執行一個方法,需要開辟棧內存、壓棧、出棧等操作,這部分時間也是不可以忽略的。因此,這個解決方案不可行。那么,還有什么方法可以做到呢?
a)使用繼承
繼承是最直觀的解決方案,相信你已經想到了,至少我最開始想到的解決方案就是繼承。
為此,我們重新創建一個類Bird2,在Bird2中我們只做一件事情,就是調用父類的fly方法,在前后記錄時間,並打印時間差:
public class Bird2 extends Bird { @Override public void fly() { long start = System.currentTimeMillis(); super.fly(); long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); } }
這是一種解決方案,還有一種解決方案叫做:聚合,其實也是比較容易想到的。
我們再次創建新類Bird3,在Bird3的構造方法中傳入Bird實例。同時,讓Bird3也實現Flyable接口,並在fly方法中調用傳入的Bird實例的fly方法:
public class Bird3 implements Flyable { private Bird bird; public Bird3(Bird bird) { this.bird = bird; } @Override public void fly() { long start = System.currentTimeMillis(); bird.fly(); long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); } }
為了記錄Bird->fly()方法的執行時間,我們在前后添加了記錄時間的代碼。同樣地,通過這種方法我們也可以獲得小鳥的飛行時間。那么,這兩種方法孰優孰劣呢?咋一看,不好評判!
繼續深入思考,用問題推導來解答這個問題:
問題一:如果我還需要在fly方法前后打印日志,記錄飛行開始和飛行結束,怎么辦?
有人說,很簡單!繼承Bird2並在在前后添加打印語句即可。那么,問題來了,請看問題二。
問題二:如果我需要調換執行順序,先打印日志,再獲取飛行時間,怎么辦?
有人說,再新建一個類Bird4繼承Bird,打印日志。再新建一個類Bird5繼承Bird4,獲取方法執行時間。
問題顯而易見:使用繼承將導致類無限制擴展,同時靈活性也無法獲得保障。那么,使用 聚合 是否可以避免這個問題呢?
答案是:可以!但我們的類需要稍微改造一下。修改Bird3類,將聚合對象Bird類型修改為Flyable
public class Bird3 implements Flyable { private Flyable flyable; public Bird3(Flyable flyable) { this.flyable = flyable; } @Override public void fly() { long start = System.currentTimeMillis(); flyable.fly(); long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); } }
為了讓你看的更清楚,我將Bird3更名為BirdTimeProxy,即用於獲取方法執行時間的代理的意思。同時我們新建BirdLogProxy代理類用於打印日志:
public class BirdLogProxy implements Flyable { private Flyable flyable; public BirdLogProxy(Flyable flyable) { this.flyable = flyable; } @Override public void fly() { System.out.println("Bird fly start..."); flyable.fly(); System.out.println("Bird fly end..."); } }
接下來神奇的事情發生了,如果我們需要先記錄日志,再獲取飛行時間,可以在調用的地方這么做:
public static void main(String[] args) { Bird bird = new Bird(); BirdLogProxy p1 = new BirdLogProxy(bird); BirdTimeProxy p2 = new BirdTimeProxy(p1); p2.fly(); }
反過來,可以這么做:
public static void main(String[] args) { Bird bird = new Bird(); BirdTimeProxy p2 = new BirdTimeProxy(bird); BirdLogProxy p1 = new BirdLogProxy(p2); p1.fly(); }
看到這里,有同學可能會有疑問了。雖然現象看起來,聚合可以靈活調換執行順序。可是,為什么 聚合 可以做到,而繼承不行呢。我們用一張圖來解釋一下:

靜態代理
接下來,觀察上面的類BirdTimeProxy,在它的fly方法中我們直接調用了flyable->fly()方法。換而言之,BirdTimeProxy其實代理了傳入的Flyable對象,這就是典型的靜態代理實現。
從表面上看,靜態代理已經完美解決了我們的問題。可是,試想一下,如果我們需要計算SDK中100個方法的運行時間,同樣的代碼至少需要重復100次,並且創建至少100個代理類。往小了說,如果Bird類有多個方法,我們需要知道其他方法的運行時間,同樣的代碼也至少需要重復多次。因此,靜態代理至少有以下兩個局限性問題:
- 如果同時代理多個類,依然會導致類無限制擴展
- 如果類中有多個方法,同樣的邏輯需要反復實現
那么,我們是否可以使用同一個代理類來代理任意對象呢?我們以獲取方法運行時間為例,是否可以使用同一個類(例如:TimeProxy)來計算任意對象的任一方法的執行時間呢?甚至再大膽一點,代理的邏輯也可以自己指定。比如,獲取方法的執行時間,打印日志,這類邏輯都可以自己指定。這就是本文重點探討的問題,也是最難理解的部分:動態代理。
動態代理
繼續回到上面這個問題:是否可以使用同一個類(例如:TimeProxy)來計算任意對象的任一方法的執行時間呢。
這個部分需要一定的抽象思維,我想,你腦海中的第一個解決方案應該是使用反射。反射是用於獲取已創建實例的方法或者屬性,並對其進行調用或者賦值。很明顯,在這里,反射解決不了問題。但是,再大膽一點,如果我們可以動態生成TimeProxy這個類,並且動態編譯。然后,再通過反射創建對象並加載到內存中,不就實現了對任意對象進行代理了嗎?為了防止你依然一頭霧水,我們用一張圖來描述接下來要做什么:

動態生成Java源文件並且排版是一個非常繁瑣的工作,為了簡化操作,我們使用 JavaPoet 這個第三方庫幫我們生成TimeProxy的源碼。希望 JavaPoet 不要成為你的負擔,不理解 JavaPoet 沒有關系,你只要把它當成一個Java源碼生成工具使用即可。
PS:你記住,任何工具庫的使用都不會太難,它是為了簡化某些操作而出現的,目標是簡化而不是繁瑣。因此,只要你適應它的規則就輕車熟路了。
第一步:生成TimeProxy源碼
public class Proxy { public static Object newProxyInstance() throws IOException { TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy") .addSuperinterface(Flyable.class); FieldSpec fieldSpec = FieldSpec.builder(Flyable.class, "flyable", Modifier.PRIVATE).build(); typeSpecBuilder.addField(fieldSpec); MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(Flyable.class, "flyable") .addStatement("this.flyable = flyable") .build(); typeSpecBuilder.addMethod(constructorMethodSpec); Method[] methods = Flyable.class.getDeclaredMethods(); for (Method method : methods) { MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(method.getReturnType()) .addStatement("long start = $T.currentTimeMillis()", System.class) .addCode("\n") .addStatement("this.flyable." + method.getName() + "()") .addCode("\n") .addStatement("long end = $T.currentTimeMillis()", System.class) .addStatement("$T.out.println(\"Fly Time =\" + (end - start))", System.class) .build(); typeSpecBuilder.addMethod(methodSpec); } JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build(); // 為了看的更清楚,我將源碼文件生成到桌面 javaFile.writeTo(new File("/Users/ouyangfeng/Desktop/")); return null; } }
在main方法中調用Proxy.newProxyInstance(),你將看到桌面已經生成了TimeProxy.java文件,生成的內容如下:
package com.youngfeng.proxy; import java.lang.Override; import java.lang.System; class TimeProxy implements Flyable { private Flyable flyable; public TimeProxy(Flyable flyable) { this.flyable = flyable; } @Override public void fly() { long start = System.currentTimeMillis(); this.flyable.fly(); long end = System.currentTimeMillis(); System.out.println("Fly Time =" + (end - start)); } }
第二步:編譯TimeProxy源碼
編譯TimeProxy源碼我們直接使用JDK提供的編譯工具即可,為了使你看起來更清晰,我使用一個新的輔助類來完成編譯操作:
public class JavaCompiler { public static void compile(File javaFile) throws IOException { javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null); Iterable iterable = fileManager.getJavaFileObjects(javaFile); javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, iterable); task.call(); fileManager.close(); } }
在Proxy->newProxyInstance()方法中調用該方法,編譯順利完成:
// 為了看的更清楚,我將源碼文件生成到桌面 String sourcePath = "/Users/ouyangfeng/Desktop/"; javaFile.writeTo(new File(sourcePath)); // 編譯 JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java"));

第三步:加載到內存中並創建對象
URL[] urls = new URL[] {new URL("file:/" + sourcePath)}; URLClassLoader classLoader = new URLClassLoader(urls); Class clazz = classLoader.loadClass("com.youngfeng.proxy.TimeProxy"); Constructor constructor = clazz.getConstructor(Flyable.class); Flyable flyable = (Flyable) constructor.newInstance(new Bird()); flyable.fly();
通過以上三個步驟,我們至少解決了下面兩個問題:
- 不再需要手動創建TimeProxy
- 可以代理任意實現了Flyable接口的類對象,並獲取接口方法的執行時間
可是,說好的任意對象呢?
第四步:增加InvocationHandler接口
查看Proxy->newProxyInstance()的源碼,代理類繼承的接口我們是寫死的,為了增加靈活性,我們將接口類型作為參數傳入:

接口的靈活性問題解決了,TimeProxy的局限性依然存在,它只能用於獲取方法的執行時間,而如果要在方法執行前后打印日志則需要重新創建一個代理類,顯然這是不妥的!
為了增加控制的靈活性,我們考慮針將代理的處理邏輯也抽離出來(這里的處理就是打印方法的執行時間)。新增InvocationHandler
接口,用於處理自定義邏輯:
public interface InvocationHandler { void invoke(Object proxy, Method method, Object[] args); }
想象一下,如果客戶程序員需要對代理類進行自定義的處理,只要實現該接口,並在invoke方法中進行相應的處理即可。這里我們在接口中設置了三個參數(其實也是為了和JDK源碼保持一致):
- proxy => 這個參數指定動態生成的代理類,這里是
TimeProxy
- method => 這個參數表示傳入接口中的所有Method對象
- args => 這個參數對應當前method方法中的參數
引入了InvocationHandler接口之后,我們的調用順序應該變成了這樣:
MyInvocationHandler handler = new MyInvocationHandler(); Flyable proxy = Proxy.newProxyInstance(Flyable.class, handler); proxy.fly(); 方法執行流:proxy.fly() => handler.invoke()
為此,我們需要在Proxy.newProxyInstance()方法中做如下改動:
- 在newProxyInstance方法中傳入InvocationHandler
- 在生成的代理類中增加成員變量handler
- 在生成的代理類方法中,調用invoke方法
public static Object newProxyInstance(Class inf, InvocationHandler handler) throws Exception { TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy") .addModifiers(Modifier.PUBLIC) .addSuperinterface(inf); FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build(); typeSpecBuilder.addField(fieldSpec); MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(InvocationHandler.class, "handler") .addStatement("this.handler = handler") .build(); typeSpecBuilder.addMethod(constructorMethodSpec); Method[] methods = inf.getDeclaredMethods(); for (Method method : methods) { MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(method.getReturnType()) .addCode("try {\n") .addStatement("\t$T method = " + inf.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class) // 為了簡單起見,這里參數直接寫死為空 .addStatement("\tthis.handler.invoke(this, method, null)") .addCode("} catch(Exception e) {\n") .addCode("\te.printStackTrace();\n") .addCode("}\n") .build(); typeSpecBuilder.addMethod(methodSpec); } JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build(); // 為了看的更清楚,我將源碼文件生成到桌面 String sourcePath = "/Users/ouyangfeng/Desktop/"; javaFile.writeTo(new File(sourcePath)); // 編譯 JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java")); // 使用反射load到內存 URL[] urls = new URL[] {new URL("file:" + sourcePath)}; URLClassLoader classLoader = new URLClassLoader(urls); Class clazz = classLoader.loadClass("com.youngfeng.proxy.TimeProxy"); Constructor constructor = clazz.getConstructor(InvocationHandler.class); Object obj = constructor.newInstance(handler); return obj; }
上面的代碼你可能看起來比較吃力,我們直接調用該方法,查看最后生成的源碼。在main方法中測試newProxyInstance查看生成的TimeProxy源碼:
測試代碼
Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird()));
生成的TimeProxy.java源碼
package com.youngfeng.proxy; import java.lang.Override; import java.lang.reflect.Method; public class TimeProxy implements Flyable { private InvocationHandler handler; public TimeProxy(InvocationHandler handler) { this.handler = handler; } @Override public void fly() { try { Method method = com.youngfeng.proxy.Flyable.class.getMethod("fly"); this.handler.invoke(this, method, null); } catch(Exception e) { e.printStackTrace(); } } }
MyInvocationHandler.java
public class MyInvocationHandler implements InvocationHandler { private Bird bird; public MyInvocationHandler(Bird bird) { this.bird = bird; } @Override public void invoke(Object proxy, Method method, Object[] args) { long start = System.currentTimeMillis(); try { method.invoke(bird, new Object[] {}); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); } }
至此,整個方法棧的調用棧變成了這樣:

看到這里,估計很多同學已經暈了,在靜態代理部分,我們在代理類中傳入了被代理對象。可是,使用newProxyInstance生成動態代理對象的時候,我們居然不再需要傳入被代理對象了。我們傳入了的實際對象是InvocationHandler實現類的實例,這看起來有點像生成了InvocationHandler的代理對象,在動態生成的代理類的任意方法中都會間接調用InvocationHandler->invoke(proxy, method, args)方法。
其實的確是這樣。TimeProxy真正代理的對象就是InvocationHandler,不過這里設計的巧妙之處在於,InvocationHandler是一個接口,真正的實現由用戶指定。另外,在每一個方法執行的時候,invoke方法都會被調用 ,這個時候如果你需要對某個方法進行自定義邏輯處理,可以根據method的特征信息進行判斷分別處理。
如何使用
上面這段解釋是告訴你在執行Proxy->newProxyInstance方法的時候真正發生的事情,而在實際使用過程中你完全可以忘掉上面的解釋。按照設計者的初衷,我們做如下簡單歸納:
- Proxy->newProxyInstance(infs, handler) 用於生成代理對象
- InvocationHandler:這個接口主要用於自定義代理邏輯處理
- 為了完成對被代理對象的方法攔截,我們需要在InvocationHandler對象中傳入被代理對象實例。
查看上面的代碼,你可以看到我將Bird實例已經傳入到了MyInvocationHandler中,原因就是第三點。
這樣設計有什么好處呢?有人說,我們大費周章,饒了一大圈,最終變成了這個樣子,到底圖什么呢?
想象一下,到此為止,如果我們還需要對其它任意對象進行代理,是否還需要改動newProxyInstance方法的源碼,答案是:完全不需要!
只要你在newProxyInstance方法中指定代理需要實現的接口,指定用於自定義處理的InvocationHandler對象,整個代理的邏輯處理都在你自定義的InvocationHandler實現類中進行處理。至此,而我們終於可以從不斷地寫代理類用於實現自定義邏輯的重復工作中解放出來了,從此需要做什么,交給InvocationHandler。
事實上,我們之前給自己定下的目標“使用同一個類來計算任意對象的任一方法的執行時間”已經實現了。嚴格來說,是我們超額完成了任務,TimeProxy不僅可以計算方法執行的時間,也可以打印方法執行日志,這完全取決於你的InvocationHandler接口實現。因此,這里取名為TimeProxy其實已經不合適了。我們可以修改為和JDK命名一致,即$Proxy0,感興趣的同學請自行實踐,本篇文章的代碼將放到我的Github倉庫,文章結尾會給出代碼地址。
JDK實現揭秘
通過上面的這些步驟,我們完成了一個簡易的仿JDK實現的動態代理邏輯。接下來,我們一起來看一看JDK實現的動態代理和我們到底有什么不同。
Proxy.java

InvocationHandler

可以看到,官方版本Proxy類提供的方法多一些,而我們主要使用的接口newProxyInstance參數也和我們設計的不太一樣。這里給大家簡單解釋一下,每個參數的意義:
- Classloader:類加載器,你可以使用自定義的類加載器,我們的實現版本為了簡化,直接在代碼中寫死了Classloader。
- Class<?>[]:第二個參數也和我們的實現版本不一致,這個其實很容易理解,我們應該允許我們自己實現的代理類同時實現多個接口。前面設計只傳入一個接口,只是為了簡化實現,讓你專注核心邏輯實現而已。
最后一個參數就不用說了,和我們實現的版本完全是一樣的。
仔細觀察官方版本的InvocationHandler,它和我們自己的實現的版本也有一個細微的差別:官方版本invoke方法有返回值,而我們的版本中是沒有返回值的。那么,返回值到底有什么作用呢?直接來看官方文檔:

核心思想:這里的返回值類型必須和傳入接口的返回值類型一致,或者與其封裝對象的類型一致。
遺憾的是,這里並沒有說明返回值的用途,其實這里稍微發揮一下想象力就知道了。在我們的版本實現中,Flyable接口的所有方法都是沒有返回值的,問題是,如果有返回值呢?是的,你沒有猜錯,這里的invoke方法對應的就是傳入接口中方法的返回值。
答疑解惑
invoke方法的第一個參數proxy到底有什么作用?
這個問題其實也好理解,如果你的接口中有方法需要返回自身,如果在invoke中沒有傳入這個參數,將導致實例無法正常返回。在這種場景中,proxy的用途就表現出來了。簡單來說,這其實就是最近非常火的鏈式編程的一種應用實現。
動態代理到底有什么用?
學習任何一門技術,一定要問一問自己,這到底有什么用。其實,在這篇文章的講解過程中,我們已經說出了它的主要用途。你發現沒,使用動態代理我們居然可以在不改變源碼的情況下,直接在方法中插入自定義邏輯。這有點不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個專業名稱叫 AOP。所謂的AOP,就像刀一樣,抓住時機,趁機插入。

基於這樣一種動態特性,我們可以用它做很多事情,例如:
- 事務提交或回退(Web開發中很常見)
- 權限管理
- 自定義緩存邏輯處理
- SDK Bug修復
...
如果你閱讀過 Android_Slide_To_Close 的源碼會發現,它也在某個地方使用了動態代理設計模式。
總結
到此為止,關於動態代理的所有講解已經結束了,原諒我使用了一個誘導性的標題“騙”你進來閱讀這篇文章。如果你不是一個久經沙場的“老司機”,10分鍾完全看懂動態代理設計模式還是有一定難度的。但即使沒有看懂也沒關系,如果你在第一次閱讀完這篇文章后依然一頭霧水,就不妨再仔細閱讀一次。在閱讀的過程中,一定要跟着文章思路去敲代碼。反反復復,一定會看懂的。我在剛剛學習動態代理設計模式的時候就反復看了不下5遍,並且親自敲代碼實踐了多次。
為了讓你少走彎路,我認為看懂這篇文章,你至少需要學習以下知識點:
- 至少已經理解了面向對象語言的多態特性
- 了解簡單的反射用法
- 會簡單使用 JavaPoet 生成Java源碼
如果你在閱讀文章的過程中,有任何不理解的問題或者建議,歡迎在文章下方留言告訴我!
作者:歐陽鋒
鏈接:https://www.jianshu.com/p/fc285d669bc5