動態代理是Java語言中很是經典的一種設計模式,也是全部設計模式中最難理解的一種。本文將經過一個簡單的例子模擬JDK動態代理實現,讓你完全明白動態代理設計模式的本質,文章中可能會涉及到一些你沒有學習過的知識點或概念。若是剛好遇到了這些知識盲點,請先去學習這部分知識,再來閱讀這篇文章。java
從字面意思來看,代理比較好理解,無非就是代爲處理的意思。舉個例子,你在上大學的時候,老是喜歡逃課。所以,你拜託你的同窗幫你答到,而本身卻窩在宿舍玩遊戲... 你的這個同窗剛好就充當了代理的做用,代替你去上課。git
是的,你沒有看錯,代理就是這麼簡單!程序員
理解了代理的意思,你腦海中恐怕還有兩個巨大的疑問:github
要理解這兩個問題,看一個簡單的例子:編程
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包)提供,你沒法改動源碼,怎麼辦?bash
必定會有人說,我能夠在調用的地方這樣寫:dom
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));
}
複製代碼
這個方案看起來彷佛沒有問題,但其實你忽略了準備這些方法所須要的時間,執行一個方法,須要開闢棧內存、壓棧、出棧等操做,這部分時間也是不能夠忽略的。所以,這個解決方案不可行。那麼,還有什麼方法能夠作到呢?編程語言
繼承是最直觀的解決方案,相信你已經想到了,至少我最開始想到的解決方案就是繼承。 爲此,咱們從新建立一個類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:你記住,任何工具庫的使用都不會太難,它是爲了簡化某些操做而出現的,目標是簡化而不是繁瑣。所以,只要你適應它的規則就輕車熟路了。
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源碼咱們直接使用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();
複製代碼
經過以上三個步驟,咱們至少解決了下面兩個問題:
但是,說好的任意對象呢?
查看Proxy->newProxyInstance()的源碼,代理類繼承的接口咱們是寫死的,爲了增長靈活性,咱們將接口類型做爲參數傳入:
接口的靈活性問題解決了,TimeProxy的侷限性依然存在,它只能用於獲取方法的執行時間,而若是要在方法執行先後打印日誌則須要從新建立一個代理類,顯然這是不妥的!
爲了增長控制的靈活性,咱們考慮針將代理的處理邏輯也抽離出來(這裏的處理就是打印方法的執行時間)。新增InvocationHandler
接口,用於處理自定義邏輯:
public interface InvocationHandler {
void invoke(Object proxy, Method method, Object[] args);
}
複製代碼
想象一下,若是客戶程序員須要對代理類進行自定義的處理,只要實現該接口,並在invoke方法中進行相應的處理便可。這裏咱們在接口中設置了三個參數(其實也是爲了和JDK源碼保持一致):
TimeProxy
引入了InvocationHandler接口以後,咱們的調用順序應該變成了這樣:
MyInvocationHandler handler = new MyInvocationHandler();
Flyable proxy = Proxy.newProxyInstance(Flyable.class, handler);
proxy.fly();
方法執行流:proxy.fly() => handler.invoke()
複製代碼
爲此,咱們須要在Proxy.newProxyInstance()方法中作以下改動:
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方法的時候真正發生的事情,而在實際使用過程當中你徹底能夠忘掉上面的解釋。按照設計者的初衷,咱們作以下簡單概括:
查看上面的代碼,你能夠看到我將Bird實例已經傳入到了MyInvocationHandler中,緣由就是第三點。
這樣設計有什麼好處呢?有人說,咱們大費周章,饒了一大圈,最終變成了這個樣子,到底圖什麼呢?
想象一下,到此爲止,若是咱們還須要對其它任意對象進行代理,是否還須要改動newProxyInstance方法的源碼,答案是:徹底不須要!
只要你在newProxyInstance方法中指定代理須要實現的接口,指定用於自定義處理的InvocationHandler對象,整個代理的邏輯處理都在你自定義的InvocationHandler實現類中進行處理。至此,而咱們終於能夠從不斷地寫代理類用於實現自定義邏輯的重複工做中解放出來了,今後須要作什麼,交給InvocationHandler。
事實上,咱們以前給本身定下的目標「使用同一個類來計算任意對象的任一方法的執行時間」已經實現了。嚴格來講,是咱們超額完成了任務,TimeProxy不只能夠計算方法執行的時間,也能夠打印方法執行日誌,這徹底取決於你的InvocationHandler接口實現。所以,這裏取名爲TimeProxy其實已經不合適了。咱們能夠修改成和JDK命名一致,即$Proxy0,感興趣的同窗請自行實踐,本篇文章的代碼將放到個人Github倉庫,文章結尾會給出代碼地址。
經過上面的這些步驟,咱們完成了一個簡易的仿JDK實現的動態代理邏輯。接下來,咱們一塊兒來看一看JDK實現的動態代理和咱們到底有什麼不一樣。
Proxy.java
InvocationHandler
能夠看到,官方版本Proxy類提供的方法多一些,而咱們主要使用的接口newProxyInstance參數也和咱們設計的不太同樣。這裏給你們簡單解釋一下,每一個參數的意義:
最後一個參數就不用說了,和咱們實現的版本徹底是同樣的。
仔細觀察官方版本的InvocationHandler,它和咱們本身的實現的版本也有一個細微的差異:官方版本invoke方法有返回值,而咱們的版本中是沒有返回值的。那麼,返回值到底有什麼做用呢?直接來看官方文檔:
核心思想:這裏的返回值類型必須和傳入接口的返回值類型一致,或者與其封裝對象的類型一致。
遺憾的是,這裏並無說明返回值的用途,其實這裏稍微發揮一下想象力就知道了。在咱們的版本實現中,Flyable接口的全部方法都是沒有返回值的,問題是,若是有返回值呢?是的,你沒有猜錯,這裏的invoke方法對應的就是傳入接口中方法的返回值。
這個問題其實也好理解,若是你的接口中有方法須要返回自身,若是在invoke中沒有傳入這個參數,將致使實例沒法正常返回。在這種場景中,proxy的用途就表現出來了。簡單來講,這其實就是最近很是火的鏈式編程的一種應用實現。
學習任何一門技術,必定要問一問本身,這到底有什麼用。其實,在這篇文章的講解過程當中,咱們已經說出了它的主要用途。你發現沒,使用動態代理咱們竟然能夠在不改變源碼的狀況下,直接在方法中插入自定義邏輯。這有點不太符合咱們的一條線走到底的編程邏輯,這種編程模型有一個專業名稱叫 AOP。所謂的AOP,就像刀同樣,抓住時機,趁機插入。
基於這樣一種動態特性,咱們能夠用它作不少事情,例如:
若是你閱讀過 Android_Slide_To_Close 的源碼會發現,它也在某個地方使用了動態代理設計模式。
到此爲止,關於動態代理的全部講解已經結束了,原諒我使用了一個誘導性的標題「騙」你進來閱讀這篇文章。若是你不是一個久經沙場的「老司機」,10分鐘徹底看懂動態代理設計模式仍是有必定難度的。但即便沒有看懂也不要緊,若是你在第一次閱讀完這篇文章後依然一頭霧水,就不妨再仔細閱讀一次。在閱讀的過程當中,必定要跟着文章思路去敲代碼。反反覆覆,必定會看懂的。我在剛剛學習動態代理設計模式的時候就反覆看了不下5遍,而且親自敲代碼實踐了屢次。
爲了讓你少走彎路,我認爲看懂這篇文章,你至少須要學習如下知識點:
若是你在閱讀文章的過程當中,有任何不理解的問題或者建議,歡迎在文章下方留言告訴我!
本篇文章例子代碼:github.com/yuanhoujun/…
我是歐陽鋒,設計模式是一種很是好的編程指導模型,它在全部編程語言中是通用的,而且是亙古不變的。我建議你在這個方面多下苦功,不要糾結在一些重複的勞動中,活用設計模式會讓你的代碼更顯靈動。想要了解我嗎?看這裏:歐陽鋒檔案館。