摘要:動(dòng)態(tài)代理是語(yǔ)言中非常經(jīng)典的一種設(shè)計(jì)模式,也是所有設(shè)計(jì)模式中最難理解的一種。本文將通過(guò)一個(gè)簡(jiǎn)單的例子模擬動(dòng)態(tài)代理實(shí)現(xiàn),讓你徹底明白動(dòng)態(tài)代理設(shè)計(jì)模式的本質(zhì),文章中可能會(huì)涉及到一些你沒(méi)有學(xué)習(xí)過(guò)的知識(shí)點(diǎn)或概念。
動(dòng)態(tài)代理是Java語(yǔ)言中非常經(jīng)典的一種設(shè)計(jì)模式,也是所有設(shè)計(jì)模式中最難理解的一種。本文將通過(guò)一個(gè)簡(jiǎn)單的例子模擬JDK動(dòng)態(tài)代理實(shí)現(xiàn),讓你徹底明白動(dòng)態(tài)代理設(shè)計(jì)模式的本質(zhì),文章中可能會(huì)涉及到一些你沒(méi)有學(xué)習(xí)過(guò)的知識(shí)點(diǎn)或概念。如果恰好遇到了這些知識(shí)盲點(diǎn),請(qǐng)先去學(xué)習(xí)這部分知識(shí),再來(lái)閱讀這篇文章。什么是代理
從字面意思來(lái)看,代理比較好理解,無(wú)非就是代為處理的意思。舉個(gè)例子,你在上大學(xué)的時(shí)候,總是喜歡逃課。因此,你拜托你的同學(xué)幫你答到,而自己卻窩在宿舍玩游戲... 你的這個(gè)同學(xué)恰好就充當(dāng)了代理的作用,代替你去上課。
是的,你沒(méi)有看錯(cuò),代理就是這么簡(jiǎn)單!
理解了代理的意思,你腦海中恐怕還有兩個(gè)巨大的疑問(wèn):
怎么實(shí)現(xiàn)代理模式
代理模式有什么實(shí)際用途
要理解這兩個(gè)問(wèn)題,看一個(gè)簡(jiǎn)單的例子:
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(); } } }
很簡(jiǎn)單的一個(gè)例子,用一個(gè)隨機(jī)睡眠時(shí)間模擬小鳥在空中的飛行時(shí)間。接下來(lái)問(wèn)題來(lái)了,如果我要知道小鳥在天空中飛行了多久,怎么辦?
有人說(shuō),很簡(jiǎn)單,在Bird->fly()方法的開頭記錄起始時(shí)間,在方法結(jié)束記錄完成時(shí)間,兩個(gè)時(shí)間相減就得到了飛行時(shí)間。
@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)); }
的確,這個(gè)方法沒(méi)有任何問(wèn)題,接下來(lái)加大問(wèn)題的難度。如果Bird這個(gè)類來(lái)自于某個(gè)SDK(或者說(shuō)Jar包)提供,你無(wú)法改動(dòng)源碼,怎么辦?
一定會(huì)有人說(shuō),我可以在調(diào)用的地方這樣寫:
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)); }
這個(gè)方案看起來(lái)似乎沒(méi)有問(wèn)題,但其實(shí)你忽略了準(zhǔn)備這些方法所需要的時(shí)間,執(zhí)行一個(gè)方法,需要開辟棧內(nèi)存、壓棧、出棧等操作,這部分時(shí)間也是不可以忽略的。因此,這個(gè)解決方案不可行。那么,還有什么方法可以做到呢?
繼承是最直觀的解決方案,相信你已經(jīng)想到了,至少我最開始想到的解決方案就是繼承。
為此,我們重新創(chuàng)建一個(gè)類Bird2,在Bird2中我們只做一件事情,就是調(diào)用父類的fly方法,在前后記錄時(shí)間,并打印時(shí)間差:
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)); } }
這是一種解決方案,還有一種解決方案叫做:聚合,其實(shí)也是比較容易想到的。
我們?cè)俅蝿?chuàng)建新類Bird3,在Bird3的構(gòu)造方法中傳入Bird實(shí)例。同時(shí),讓Bird3也實(shí)現(xiàn)Flyable接口,并在fly方法中調(diào)用傳入的Bird實(shí)例的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()方法的執(zhí)行時(shí)間,我們?cè)谇昂筇砑恿擞涗洉r(shí)間的代碼。同樣地,通過(guò)這種方法我們也可以獲得小鳥的飛行時(shí)間。那么,這兩種方法孰優(yōu)孰劣呢?咋一看,不好評(píng)判!
繼續(xù)深入思考,用問(wèn)題推導(dǎo)來(lái)解答這個(gè)問(wèn)題:
問(wèn)題一:如果我還需要在fly方法前后打印日志,記錄飛行開始和飛行結(jié)束,怎么辦?
有人說(shuō),很簡(jiǎn)單!繼承Bird2并在在前后添加打印語(yǔ)句即可。那么,問(wèn)題來(lái)了,請(qǐng)看問(wèn)題二。
問(wèn)題二:如果我需要調(diào)換執(zhí)行順序,先打印日志,再獲取飛行時(shí)間,怎么辦?
有人說(shuō),再新建一個(gè)類Bird4繼承Bird,打印日志。再新建一個(gè)類Bird5繼承Bird4,獲取方法執(zhí)行時(shí)間。
問(wèn)題顯而易見(jiàn):使用繼承將導(dǎo)致類無(wú)限制擴(kuò)展,同時(shí)靈活性也無(wú)法獲得保障。那么,使用 聚合 是否可以避免這個(gè)問(wèn)題呢?
答案是:可以!但我們的類需要稍微改造一下。修改Bird3類,將聚合對(duì)象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,即用于獲取方法執(zhí)行時(shí)間的代理的意思。同時(shí)我們新建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..."); } }
接下來(lái)神奇的事情發(fā)生了,如果我們需要先記錄日志,再獲取飛行時(shí)間,可以在調(diào)用的地方這么做:
public static void main(String[] args) { Bird bird = new Bird(); BirdLogProxy p1 = new BirdLogProxy(bird); BirdTimeProxy p2 = new BirdTimeProxy(p1); p2.fly(); }
反過(guò)來(lái),可以這么做:
public static void main(String[] args) { Bird bird = new Bird(); BirdTimeProxy p2 = new BirdTimeProxy(bird); BirdLogProxy p1 = new BirdLogProxy(p2); p1.fly(); }
看到這里,有同學(xué)可能會(huì)有疑問(wèn)了。雖然現(xiàn)象看起來(lái),聚合可以靈活調(diào)換執(zhí)行順序。可是,為什么 聚合 可以做到,而繼承不行呢。我們用一張圖來(lái)解釋一下:
接下來(lái),觀察上面的類BirdTimeProxy,在它的fly方法中我們直接調(diào)用了flyable->fly()方法。換而言之,BirdTimeProxy其實(shí)代理了傳入的Flyable對(duì)象,這就是典型的靜態(tài)代理實(shí)現(xiàn)。
從表面上看,靜態(tài)代理已經(jīng)完美解決了我們的問(wèn)題。可是,試想一下,如果我們需要計(jì)算SDK中100個(gè)方法的運(yùn)行時(shí)間,同樣的代碼至少需要重復(fù)100次,并且創(chuàng)建至少100個(gè)代理類。往小了說(shuō),如果Bird類有多個(gè)方法,我們需要知道其他方法的運(yùn)行時(shí)間,同樣的代碼也至少需要重復(fù)多次。因此,靜態(tài)代理至少有以下兩個(gè)局限性問(wèn)題:
如果同時(shí)代理多個(gè)類,依然會(huì)導(dǎo)致類無(wú)限制擴(kuò)展
如果類中有多個(gè)方法,同樣的邏輯需要反復(fù)實(shí)現(xiàn)
那么,我們是否可以使用同一個(gè)代理類來(lái)代理任意對(duì)象呢?我們以獲取方法運(yùn)行時(shí)間為例,是否可以使用同一個(gè)類(例如:TimeProxy)來(lái)計(jì)算任意對(duì)象的任一方法的執(zhí)行時(shí)間呢?甚至再大膽一點(diǎn),代理的邏輯也可以自己指定。比如,獲取方法的執(zhí)行時(shí)間,打印日志,這類邏輯都可以自己指定。這就是本文重點(diǎn)探討的問(wèn)題,也是最難理解的部分:動(dòng)態(tài)代理。
動(dòng)態(tài)代理繼續(xù)回到上面這個(gè)問(wèn)題:是否可以使用同一個(gè)類(例如:TimeProxy)來(lái)計(jì)算任意對(duì)象的任一方法的執(zhí)行時(shí)間呢。
這個(gè)部分需要一定的抽象思維,我想,你腦海中的第一個(gè)解決方案應(yīng)該是使用反射。反射是用于獲取已創(chuàng)建實(shí)例的方法或者屬性,并對(duì)其進(jìn)行調(diào)用或者賦值。很明顯,在這里,反射解決不了問(wèn)題。但是,再大膽一點(diǎn),如果我們可以動(dòng)態(tài)生成TimeProxy這個(gè)類,并且動(dòng)態(tài)編譯。然后,再通過(guò)反射創(chuàng)建對(duì)象并加載到內(nèi)存中,不就實(shí)現(xiàn)了對(duì)任意對(duì)象進(jìn)行代理了嗎?為了防止你依然一頭霧水,我們用一張圖來(lái)描述接下來(lái)要做什么:
動(dòng)態(tài)生成Java源文件并且排版是一個(gè)非常繁瑣的工作,為了簡(jiǎn)化操作,我們使用 JavaPoet 這個(gè)第三方庫(kù)幫我們生成TimeProxy的源碼。希望 JavaPoet 不要成為你的負(fù)擔(dān),不理解 JavaPoet 沒(méi)有關(guān)系,你只要把它當(dāng)成一個(gè)Java源碼生成工具使用即可。
PS:你記住,任何工具庫(kù)的使用都不會(huì)太難,它是為了簡(jiǎn)化某些操作而出現(xiàn)的,目標(biāo)是簡(jiǎn)化而不是繁瑣。因此,只要你適應(yīng)它的規(guī)則就輕車熟路了。
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(" ") .addStatement("this.flyable." + method.getName() + "()") .addCode(" ") .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方法中調(diào)用Proxy.newProxyInstance(),你將看到桌面已經(jīng)生成了TimeProxy.java文件,生成的內(nèi)容如下:
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提供的編譯工具即可,為了使你看起來(lái)更清晰,我使用一個(gè)新的輔助類來(lái)完成編譯操作:
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()方法中調(diào)用該方法,編譯順利完成:
// 為了看的更清楚,我將源碼文件生成到桌面 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();
通過(guò)以上三個(gè)步驟,我們至少解決了下面兩個(gè)問(wèn)題:
不再需要手動(dòng)創(chuàng)建TimeProxy
可以代理任意實(shí)現(xiàn)了Flyable接口的類對(duì)象,并獲取接口方法的執(zhí)行時(shí)間
可是,說(shuō)好的任意對(duì)象呢?
查看Proxy->newProxyInstance()的源碼,代理類繼承的接口我們是寫死的,為了增加靈活性,我們將接口類型作為參數(shù)傳入:
接口的靈活性問(wèn)題解決了,TimeProxy的局限性依然存在,它只能用于獲取方法的執(zhí)行時(shí)間,而如果要在方法執(zhí)行前后打印日志則需要重新創(chuàng)建一個(gè)代理類,顯然這是不妥的!
為了增加控制的靈活性,我們考慮針將代理的處理邏輯也抽離出來(lái)(這里的處理就是打印方法的執(zhí)行時(shí)間)。新增InvocationHandler接口,用于處理自定義邏輯:
public interface InvocationHandler { void invoke(Object proxy, Method method, Object[] args); }
想象一下,如果客戶程序員需要對(duì)代理類進(jìn)行自定義的處理,只要實(shí)現(xiàn)該接口,并在invoke方法中進(jìn)行相應(yīng)的處理即可。這里我們?cè)诮涌谥性O(shè)置了三個(gè)參數(shù)(其實(shí)也是為了和JDK源碼保持一致):
proxy => 這個(gè)參數(shù)指定動(dòng)態(tài)生成的代理類,這里是TimeProxy
method => 這個(gè)參數(shù)表示傳入接口中的所有Method對(duì)象
args => 這個(gè)參數(shù)對(duì)應(yīng)當(dāng)前method方法中的參數(shù)
引入了InvocationHandler接口之后,我們的調(diào)用順序應(yīng)該變成了這樣:
MyInvocationHandler handler = new MyInvocationHandler(); Flyable proxy = Proxy.newProxyInstance(Flyable.class, handler); proxy.fly(); 方法執(zhí)行流:proxy.fly() => handler.invoke()
為此,我們需要在Proxy.newProxyInstance()方法中做如下改動(dòng):
在newProxyInstance方法中傳入InvocationHandler
在生成的代理類中增加成員變量handler
在生成的代理類方法中,調(diào)用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 { ") .addStatement(" $T method = " + inf.getName() + ".class.getMethod("" + method.getName() + "")", Method.class) // 為了簡(jiǎn)單起見(jiàn),這里參數(shù)直接寫死為空 .addStatement(" this.handler.invoke(this, method, null)") .addCode("} catch(Exception e) { ") .addCode(" e.printStackTrace(); ") .addCode("} ") .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到內(nèi)存 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; }
上面的代碼你可能看起來(lái)比較吃力,我們直接調(diào)用該方法,查看最后生成的源碼。在main方法中測(cè)試newProxyInstance查看生成的TimeProxy源碼:
測(cè)試代碼
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)); } }
至此,整個(gè)方法棧的調(diào)用棧變成了這樣:
看到這里,估計(jì)很多同學(xué)已經(jīng)暈了,在靜態(tài)代理部分,我們?cè)诖眍愔袀魅肓吮淮韺?duì)象。可是,使用newProxyInstance生成動(dòng)態(tài)代理對(duì)象的時(shí)候,我們居然不再需要傳入被代理對(duì)象了。我們傳入了的實(shí)際對(duì)象是InvocationHandler實(shí)現(xiàn)類的實(shí)例,這看起來(lái)有點(diǎn)像生成了InvocationHandler的代理對(duì)象,在動(dòng)態(tài)生成的代理類的任意方法中都會(huì)間接調(diào)用InvocationHandler->invoke(proxy, method, args)方法。
其實(shí)的確是這樣。TimeProxy真正代理的對(duì)象就是InvocationHandler,不過(guò)這里設(shè)計(jì)的巧妙之處在于,InvocationHandler是一個(gè)接口,真正的實(shí)現(xiàn)由用戶指定。另外,在每一個(gè)方法執(zhí)行的時(shí)候,invoke方法都會(huì)被調(diào)用 ,這個(gè)時(shí)候如果你需要對(duì)某個(gè)方法進(jìn)行自定義邏輯處理,可以根據(jù)method的特征信息進(jìn)行判斷分別處理。
如何使用上面這段解釋是告訴你在執(zhí)行Proxy->newProxyInstance方法的時(shí)候真正發(fā)生的事情,而在實(shí)際使用過(guò)程中你完全可以忘掉上面的解釋。按照設(shè)計(jì)者的初衷,我們做如下簡(jiǎn)單歸納:
Proxy->newProxyInstance(infs, handler) 用于生成代理對(duì)象
InvocationHandler:這個(gè)接口主要用于自定義代理邏輯處理
為了完成對(duì)被代理對(duì)象的方法攔截,我們需要在InvocationHandler對(duì)象中傳入被代理對(duì)象實(shí)例。
查看上面的代碼,你可以看到我將Bird實(shí)例已經(jīng)傳入到了MyInvocationHandler中,原因就是第三點(diǎn)。
這樣設(shè)計(jì)有什么好處呢?有人說(shuō),我們大費(fèi)周章,饒了一大圈,最終變成了這個(gè)樣子,到底圖什么呢?
想象一下,到此為止,如果我們還需要對(duì)其它任意對(duì)象進(jìn)行代理,是否還需要改動(dòng)newProxyInstance方法的源碼,答案是:完全不需要!
只要你在newProxyInstance方法中指定代理需要實(shí)現(xiàn)的接口,指定用于自定義處理的InvocationHandler對(duì)象,整個(gè)代理的邏輯處理都在你自定義的InvocationHandler實(shí)現(xiàn)類中進(jìn)行處理。至此,而我們終于可以從不斷地寫代理類用于實(shí)現(xiàn)自定義邏輯的重復(fù)工作中解放出來(lái)了,從此需要做什么,交給InvocationHandler。
事實(shí)上,我們之前給自己定下的目標(biāo)“使用同一個(gè)類來(lái)計(jì)算任意對(duì)象的任一方法的執(zhí)行時(shí)間”已經(jīng)實(shí)現(xiàn)了。嚴(yán)格來(lái)說(shuō),是我們超額完成了任務(wù),TimeProxy不僅可以計(jì)算方法執(zhí)行的時(shí)間,也可以打印方法執(zhí)行日志,這完全取決于你的InvocationHandler接口實(shí)現(xiàn)。因此,這里取名為TimeProxy其實(shí)已經(jīng)不合適了。我們可以修改為和JDK命名一致,即$Proxy0,感興趣的同學(xué)請(qǐng)自行實(shí)踐,本篇文章的代碼將放到我的Github倉(cāng)庫(kù),文章結(jié)尾會(huì)給出代碼地址。
JDK實(shí)現(xiàn)揭秘通過(guò)上面的這些步驟,我們完成了一個(gè)簡(jiǎn)易的仿JDK實(shí)現(xiàn)的動(dòng)態(tài)代理邏輯。接下來(lái),我們一起來(lái)看一看JDK實(shí)現(xiàn)的動(dòng)態(tài)代理和我們到底有什么不同。
Proxy.java
InvocationHandler
可以看到,官方版本Proxy類提供的方法多一些,而我們主要使用的接口newProxyInstance參數(shù)也和我們?cè)O(shè)計(jì)的不太一樣。這里給大家簡(jiǎn)單解釋一下,每個(gè)參數(shù)的意義:
Classloader:類加載器,你可以使用自定義的類加載器,我們的實(shí)現(xiàn)版本為了簡(jiǎn)化,直接在代碼中寫死了Classloader。
Class>[]:第二個(gè)參數(shù)也和我們的實(shí)現(xiàn)版本不一致,這個(gè)其實(shí)很容易理解,我們應(yīng)該允許我們自己實(shí)現(xiàn)的代理類同時(shí)實(shí)現(xiàn)多個(gè)接口。前面設(shè)計(jì)只傳入一個(gè)接口,只是為了簡(jiǎn)化實(shí)現(xiàn),讓你專注核心邏輯實(shí)現(xiàn)而已。
最后一個(gè)參數(shù)就不用說(shuō)了,和我們實(shí)現(xiàn)的版本完全是一樣的。
仔細(xì)觀察官方版本的InvocationHandler,它和我們自己的實(shí)現(xiàn)的版本也有一個(gè)細(xì)微的差別:官方版本invoke方法有返回值,而我們的版本中是沒(méi)有返回值的。那么,返回值到底有什么作用呢?直接來(lái)看官方文檔:
核心思想:這里的返回值類型必須和傳入接口的返回值類型一致,或者與其封裝對(duì)象的類型一致。
遺憾的是,這里并沒(méi)有說(shuō)明返回值的用途,其實(shí)這里稍微發(fā)揮一下想象力就知道了。在我們的版本實(shí)現(xiàn)中,F(xiàn)lyable接口的所有方法都是沒(méi)有返回值的,問(wèn)題是,如果有返回值呢?是的,你沒(méi)有猜錯(cuò),這里的invoke方法對(duì)應(yīng)的就是傳入接口中方法的返回值。
答疑解惑這個(gè)問(wèn)題其實(shí)也好理解,如果你的接口中有方法需要返回自身,如果在invoke中沒(méi)有傳入這個(gè)參數(shù),將導(dǎo)致實(shí)例無(wú)法正常返回。在這種場(chǎng)景中,proxy的用途就表現(xiàn)出來(lái)了。簡(jiǎn)單來(lái)說(shuō),這其實(shí)就是最近非常火的鏈?zhǔn)骄幊痰囊环N應(yīng)用實(shí)現(xiàn)。
學(xué)習(xí)任何一門技術(shù),一定要問(wèn)一問(wèn)自己,這到底有什么用。其實(shí),在這篇文章的講解過(guò)程中,我們已經(jīng)說(shuō)出了它的主要用途。你發(fā)現(xiàn)沒(méi),使用動(dòng)態(tài)代理我們居然可以在不改變?cè)创a的情況下,直接在方法中插入自定義邏輯。這有點(diǎn)不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個(gè)專業(yè)名稱叫 AOP。所謂的AOP,就像刀一樣,抓住時(shí)機(jī),趁機(jī)插入。
基于這樣一種動(dòng)態(tài)特性,我們可以用它做很多事情,例如:
事務(wù)提交或回退(Web開發(fā)中很常見(jiàn))
權(quán)限管理
自定義緩存邏輯處理
SDK Bug修復(fù)
...
如果你閱讀過(guò) Android_Slide_To_Close 的源碼會(huì)發(fā)現(xiàn),它也在某個(gè)地方使用了動(dòng)態(tài)代理設(shè)計(jì)模式。
總結(jié)到此為止,關(guān)于動(dòng)態(tài)代理的所有講解已經(jīng)結(jié)束了,原諒我使用了一個(gè)誘導(dǎo)性的標(biāo)題“騙”你進(jìn)來(lái)閱讀這篇文章。如果你不是一個(gè)久經(jīng)沙場(chǎng)的“老司機(jī)”,10分鐘完全看懂動(dòng)態(tài)代理設(shè)計(jì)模式還是有一定難度的。但即使沒(méi)有看懂也沒(méi)關(guān)系,如果你在第一次閱讀完這篇文章后依然一頭霧水,就不妨再仔細(xì)閱讀一次。在閱讀的過(guò)程中,一定要跟著文章思路去敲代碼。反反復(fù)復(fù),一定會(huì)看懂的。我在剛剛學(xué)習(xí)動(dòng)態(tài)代理設(shè)計(jì)模式的時(shí)候就反復(fù)看了不下5遍,并且親自敲代碼實(shí)踐了多次。
為了讓你少走彎路,我認(rèn)為看懂這篇文章,你至少需要學(xué)習(xí)以下知識(shí)點(diǎn):
至少已經(jīng)理解了面向?qū)ο笳Z(yǔ)言的多態(tài)特性
了解簡(jiǎn)單的反射用法
會(huì)簡(jiǎn)單使用 JavaPoet 生成Java源碼
如果你在閱讀文章的過(guò)程中,有任何不理解的問(wèn)題或者建議,歡迎在文章下方留言告訴我!
本篇文章例子代碼:https://github.com/yuanhoujun/java-dynamic-proxy
我是歐陽(yáng)鋒,設(shè)計(jì)模式是一種非常好的編程指導(dǎo)模型,它在所有編程語(yǔ)言中是通用的,并且是亙古不變的。我建議你在這個(gè)方面多下苦功,不要糾結(jié)在一些重復(fù)的勞動(dòng)中,活用設(shè)計(jì)模式會(huì)讓你的代碼更顯靈動(dòng)。想要了解我嗎?看這里:歐陽(yáng)鋒檔案館。
編程,我們是認(rèn)真的! 關(guān)注歐陽(yáng)鋒工作室公眾號(hào),你想要的都在這里:文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/68681.html
摘要:本文從定義,作用,技術(shù)架構(gòu),安裝和使用等全方位帶你看懂。如圖中左邊紅框中和右邊的紅框中都唯一表示為同一個(gè)鏡像。最后,于開發(fā)者而言提供了一種開發(fā)環(huán)境的管理辦法,與測(cè)試人員而言保證了環(huán)境的同步,于運(yùn)維人員提供了可移植的標(biāo)準(zhǔn)化部署流程。 作者丨唐文廣:騰訊工程師,負(fù)責(zé)無(wú)線研發(fā)部地圖測(cè)試。 導(dǎo)語(yǔ):Docker,近兩年才流行起來(lái)的超輕量級(jí)虛擬機(jī),它可以讓你輕松完成持續(xù)集成、自動(dòng)交付、自動(dòng)部署...
摘要:本文從定義,作用,技術(shù)架構(gòu),安裝和使用等全方位帶你看懂。最后,于開發(fā)者而言提供了一種開發(fā)環(huán)境的管理辦法,與測(cè)試人員而言保證了環(huán)境的同步,于運(yùn)維人員提供了可移植的標(biāo)準(zhǔn)化部署流程。顯示上圖內(nèi)容就表明安裝完成。 作者丨唐文廣:騰訊工程師,負(fù)責(zé)無(wú)線研發(fā)部地圖測(cè)試。 導(dǎo)語(yǔ):Docker,近兩年才流行起來(lái)的超輕量級(jí)虛擬機(jī),它可以讓你輕松完成持續(xù)集成、自動(dòng)交付、自動(dòng)部署,并且實(shí)現(xiàn)開發(fā)環(huán)境、測(cè)試環(huán)...
摘要:在結(jié)合下面簡(jiǎn)要的分析,分鐘內(nèi)你就能搞明白的啟動(dòng)流程。關(guān)于的啟動(dòng),我在驚天秘密從開始,揭露線程通訊的詭計(jì)和主線程的陰謀一文中有提到過(guò)。從上圖可以看到,方法中主要做的事情有初始化主線程的主。并使主線程進(jìn)入等待接收消息的無(wú)限循環(huán)狀態(tài)。 showImg(https://segmentfault.com/img/remote/1460000009912818); 背景介紹 從事開發(fā)到了一定階段,...
閱讀 861·2023-04-26 00:11
閱讀 2658·2021-11-04 16:13
閱讀 2106·2021-09-09 09:33
閱讀 1477·2021-08-20 09:35
閱讀 3823·2021-08-09 13:42
閱讀 3611·2019-08-30 15:55
閱讀 1056·2019-08-30 15:55
閱讀 2221·2019-08-30 13:55