在上一篇文章我們介紹了代理模式逗嫡,靜態(tài)的伙单,本期我們介紹動態(tài)代理庄吼,動態(tài)代理的應(yīng)用也非常廣泛,也是在很多面試場合中必問的一個點颤芬,希望讀完本文,你將有所收獲巷挥。
原創(chuàng)聲明:未經(jīng)授權(quán)初厚,不得轉(zhuǎn)載衫生,侵權(quán)必究毡惜,轉(zhuǎn)載前請與作者取得聯(lián)系泳叠。
何謂動態(tài)代理
普通代理模式乌庶,代理類Proxy的Java代碼在JVM運行時就已經(jīng)確定了透敌,也就是在編碼編譯階段就確定了Proxy類的代碼背率。而動態(tài)代理是指在JVM運行過程中,動態(tài)的創(chuàng)建一個類的代理類桨仿,并實例化代理對象钞支。因為實際的代理類是在運行時創(chuàng)建的茫蛹,所以我們稱這個Java技術(shù)為:動態(tài)代理。
動態(tài)代理的應(yīng)用場景
動態(tài)代理的應(yīng)用場景有很多烁挟,例如久負盛名的RPC框架婴洼,在客戶端都會利用動態(tài)代理生成一個服務(wù)端接口的代理類來代理接口的調(diào)用執(zhí)行。同時Spring中的AOP就是一個動態(tài)代理的典型應(yīng)用信夫,有了動態(tài)代理窃蹋,媽媽再也不用擔心我的切面編程了。
實現(xiàn)動態(tài)代理的兩種方式
Java中有兩種生成動態(tài)代理的方式:
- 基于JDK實現(xiàn)的動態(tài)代理静稻。
- 基于CGLIB類庫實現(xiàn)的動態(tài)代理警没。
兩者的區(qū)別將在后文介紹。
JDK動態(tài)代理
在java的類庫中振湾,java.util.reflect.Proxy類就是其用來實現(xiàn)動態(tài)代理的頂層類杀迹。可以通過Proxy類的靜態(tài)方法Proxy.newProxyInstance()方法動態(tài)的創(chuàng)建一個類的代理類押搪,并實例化树酪。由它創(chuàng)建的代理類都是Proxy類的子類浅碾。
在看JDK動態(tài)代理的示例代碼之前,讓我們先來看看其UML類圖续语,從全局上進行一個理解垂谢。
JDK動態(tài)代理UML類圖
因為Proxy類是JDK為你創(chuàng)建的,所以你需要有辦法告訴Proxy類你要做什么疮茄,讓Proxy類代理你滥朱。但你不能像普通代理模式那樣,將被代理類通過組合的方式放到Proxy類中力试,那么要放到哪呢徙邻?放到InvocationHandler的實現(xiàn)類中,讓InvocationHandler的實現(xiàn)類來響應(yīng)代理的方法調(diào)用畸裳。
JDK動態(tài)代理實現(xiàn)步驟
(1)創(chuàng)建被代理對象的接口類缰犁。
(2)創(chuàng)建具體被代理對象接口的實現(xiàn)類。
(3)創(chuàng)建一個InvocationHandler的實現(xiàn)類怖糊,并持有被代理對象的引用帅容。然后在invoke方法中利用反射調(diào)用被代理對象的方法。
(4)利用Proxy.newProxyInstance方法創(chuàng)建代理對象蓬抄,利用代理對象實現(xiàn)真實對象方法的調(diào)用丰嘉。
JDK動態(tài)代理實現(xiàn)示例代碼
創(chuàng)建被代理對象的接口類Subject
public interface Subject {
void request();
}
創(chuàng)建Subject接口的實現(xiàn)類:簡單打印一句輸出
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("request invoke");
}
}
創(chuàng)建一個InvocationHandler的實現(xiàn)類
public class ConcreteInvocationHandler implements InvocationHandler {
private Subject subject;
public ConcreteInvocationHandler(Subject subject) {
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return method.invoke(subject, args);
}
}
客戶端測試類
public class JDKDynamicProxyTest {
public static void main(String[] args) {
Subject subject = new RealSubject();
InvocationHandler handler = new ConcreteInvocationHandler(subject);
Subject proxy = (Subject)Proxy.newProxyInstance(RealSubject.class.getClassLoader(),
RealSubject.class.getInterfaces(), handler);
proxy.request();
}
}
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法有三個入?yún)ⅲ?/p>
- ClassLoader:用來創(chuàng)建并加載Proxy類的ClassLoader。
- interfaces:被代理對象的接口列表嚷缭,這是JDK強制要求的饮亏,被代理的對象必須實現(xiàn)某一個接口。其目的是在生成Proxy類時也實現(xiàn)這些接口阅爽,做到能夠替代被代理類路幸。
- invoactionhandler對象:用來提供Proxy轉(zhuǎn)發(fā)處理的幫助實現(xiàn)類,所有代理方法的調(diào)用都交由它調(diào)用付翁,再由它內(nèi)部實現(xiàn)對真正對象方法的調(diào)用简肴。
輸出結(jié)果:
proxy class name : com.sun.proxy.$Proxy0
request invoke
從輸出結(jié)果可以看到,生成代理類的類名為$Proxy0
這是JDK在運行時生成的字節(jié)碼類百侧。JDK生成Proxy類的字節(jié)碼是通過ProxyGenerator.generateProxyClass生成的砰识,筆者寫了個小工具生成字節(jié)碼,并輸出到文件佣渴,代碼如下:
byte[] proxyBytes = ProxyGenerator.generateProxyClass("$Proxy0",
RealSubject.class.getInterfaces());
InputStream inputStream = new ByteArrayInputStream(proxyBytes);
FileOutputStream outputStream = new FileOutputStream("C:/$Proxy0.class");
byte[] buff = new byte[1024];
int len = 0;
while((len=inputStream.read(buff))!=-1){
outputStream.write(buff, 0, len);
}
inputStream.close();
outputStream.close();
生成的字節(jié)碼文件辫狼,用反編譯工具看下$Proxy0的實現(xiàn):
public final class $Proxy0 extends Proxy implements Subject {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler paramInvocationHandler) {
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject) {
try {
return ((Boolean) this.h.invoke(this, m1,
new Object[]{paramObject})).booleanValue();
} catch (Error | RuntimeException localError) {
throw localError;
} catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}
public final String toString() {
try {
return (String) this.h.invoke(this, m2, null);
} catch (Error | RuntimeException localError) {
throw localError;
} catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}
public final void request() {
try {
this.h.invoke(this, m3, null);
return;
} catch (Error | RuntimeException localError) {
throw localError;
} catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}
public final int hashCode() {
try {
return ((Integer) this.h.invoke(this, m0, null)).intValue();
} catch (Error | RuntimeException localError) {
throw localError;
} catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.misout.designpattern.subject.Subject").getMethod("request", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
} catch (NoSuchMethodException localNoSuchMethodException) {
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
} catch (ClassNotFoundException localClassNotFoundException) {
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}
從生成的類可以看出,$Proxy0
實現(xiàn)了Subject接口辛润,這個接口正是我們傳遞進去的被代理對象的接口列表膨处。同時還繼承了Proxy類,這驗證了前文所說生成的代理類是Proxy子類的說明。
從類的結(jié)構(gòu)可以看出真椿,JDK動態(tài)生成代理類鹃答,一定要被代理類實現(xiàn)了某個接口,否則就無法生成代理類突硝,這也就是JDK動態(tài)代理的缺陷之一测摔。
另外,被代理類可以實現(xiàn)多個接口狞换。從代理類代碼中可以看到避咆,代理類是通過InvocationHandler的invoke方法去實現(xiàn)被代理接口方法調(diào)用的舟肉。所以被代理對象實現(xiàn)了多個接口并且希望對不同接口實施不同的代理行為時修噪,應(yīng)該在ConcreteInvocationHandler類的invoke方法中,通過判斷方法名來實現(xiàn)不同的接口的代理行為路媚。
CGLIB實現(xiàn)動態(tài)代理
CGLIB是一個高性能的代碼生成類庫黄琼,被Spring廣泛應(yīng)用。其底層是通過ASM字節(jié)碼框架生成類的字節(jié)碼整慎,達到動態(tài)創(chuàng)建類的目的脏款。
CGLIB實現(xiàn)的動態(tài)代理UML類圖:
我們知道,實現(xiàn)代理有兩種方式:
- 要么通過繼承父類裤园,并改寫父類的方法撤师,在父類方法邏輯前后增加控制邏輯實現(xiàn)代理。
- 要么實現(xiàn)同一接口拧揽,并利用組合的方式剃盾,持有被代理的引用,然后在代理方法前后增加控制邏輯實現(xiàn)代理淤袜。
那么從CGLIB實現(xiàn)的動態(tài)代理UML類圖來看痒谴,顯然是通過繼承父類的方式進行實現(xiàn)的。這樣在父類可以代替子類铡羡,代理子類可以直接調(diào)用父類的方法進行訪問积蔚。巧妙的是,如果想對真實類增強業(yè)務(wù)邏輯烦周,進行切面編程尽爆,則可以創(chuàng)建一個方法攔截器,在其中編寫自己增強的業(yè)務(wù)邏輯代碼或訪問控制代碼读慎,然后交給代理類進行調(diào)用訪問漱贱,達到AOP的效果。
下面贪壳,我們看看通過CGLIB實現(xiàn)動態(tài)代理的步驟:
(1)創(chuàng)建被代理的目標類饱亿。
(2)創(chuàng)建一個方法攔截器類,并實現(xiàn)CGLIB的MethodInterceptor接口的intercept()方法。
(3)通過Enhancer類增強工具彪笼,創(chuàng)建目標類的代理類钻注。
(4)利用代理類進行方法調(diào)用,就像調(diào)用真實的目標類方法一樣配猫。
CGLIB動態(tài)代理實現(xiàn)示例代碼
創(chuàng)建目標類:Target:方法簡單輸出一句話
public class Target {
public void request() {
System.out.println("執(zhí)行目標類的方法");
}
}
創(chuàng)建目標類的方法增強攔截器:TargetMethodInterceptor:在攔截器內(nèi)部幅恋,調(diào)用目標方法前進行前置和后置增強處理。
public class TargetMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("方法攔截增強邏輯-前置處理執(zhí)行");
Object result = proxy.invokeSuper(obj, args);
System.out.println("方法攔截增強邏輯-后置處理執(zhí)行");
return result;
}
}
生成代理類泵肄,并測試
public class CglibDynamicProxyTest {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
// 設(shè)置生成代理類的父類class對象
enhancer.setSuperclass(Target.class);
// 設(shè)置增強目標類的方法攔截器
MethodInterceptor methodInterceptor = new TargetMethodInterceptor();
enhancer.setCallback(methodInterceptor);
// 生成代理類并實例化
Target proxy = (Target) enhancer.create();
// 用代理類調(diào)用方法
proxy.request();
}
}
測試輸出:可以看到成功進行了業(yè)務(wù)增強的處理捆交。
方法攔截增強邏輯-前置處理執(zhí)行
執(zhí)行目標類的方法
方法攔截增強邏輯-后置處理執(zhí)行
JDK動態(tài)代理 VS CGLIB 對比
- 字節(jié)碼創(chuàng)建方式:JDK動態(tài)代理通過JVM實現(xiàn)代理類字節(jié)碼的創(chuàng)建,cglib通過ASM創(chuàng)建字節(jié)碼腐巢。
- JDK動態(tài)代理強制要求目標類必須實現(xiàn)了某一接口品追,否則無法進行代理。而CGLIB則要求目標類和目標方法不能是final的冯丙,因為CGLIB通過繼承的方式實現(xiàn)代理肉瓦。
- CGLib不能對聲明為final的方法進行代理,因為是通過繼承父類的方式實現(xiàn)胃惜,如果父類是final的泞莉,那么無法繼承父類。
性能對比
性能的對比船殉,不是一個簡單的答案鲫趁,要區(qū)分JDK版本來區(qū)分,這里得出的答案是基于其他作者的測試結(jié)果得出的利虫。
JDK1.6/1.7上的對比
- 類的創(chuàng)建速度:JDK快于CGLIB挨厚。
- 執(zhí)行速度:JDK慢于CGLIB,大概慢2倍的關(guān)系列吼。
JDK1.8上的對比
- 類的創(chuàng)建速度:JDK快于CGLIB幽崩。
- 執(zhí)行速度:JDK快于CGLIB,經(jīng)過努力寞钥,JDK1.8作了性能上的優(yōu)化慌申,速度明顯比1.7提升了很多。1.8上JDK全面優(yōu)于CGLIB理郑,是不是說以后都不要用CGLIB蹄溉,這還得看具體的類情況和場景,如果沒有實現(xiàn)接口您炉,就用CGLIB柒爵,使用的場景不同。
推薦閱讀
設(shè)計模式(一)策略模式
設(shè)計模式(二)觀察者模式
設(shè)計模式(三)裝飾器模式
設(shè)計模式(四)簡單工廠模式
設(shè)計模式(五)工廠方法模式
設(shè)計模式(六)抽象工廠模式
設(shè)計模式(七)單例模式你用對了嗎
設(shè)計模式(八)適配器模式
設(shè)計模式(九)模板方法
設(shè)計模式(十)代理模式
參考
深入剖析動態(tài)代理--性能比較
Spring AOP中的JDK和CGLib動態(tài)代理哪個效率更高赚爵?