所謂代理模式,是指客戶端(Client)并不直接調(diào)用實際的對象(下圖右下角的RealSubject)爽航,而是通過調(diào)用代理(Proxy)鹦筹,來間接的調(diào)用實際的對象沉删。
代理模式的使用場合彤守,一般是由于客戶端不想直接訪問實際對象毁菱,或者訪問實際的對象存在技術(shù)上的障礙,因而通過代理對象作為橋梁衷佃,來完成間接訪問趟卸。
實現(xiàn)方式一:靜態(tài)代理
開發(fā)一個接口IDeveloper,該接口包含一個方法writeCode,寫代碼锄列。
public interface IDeveloper {
public void writeCode();
}
創(chuàng)建一個Developer類图云,實現(xiàn)該接口。
public class Developer implements IDeveloper{
private String name;
public Developer(String name){
this.name = name;
}
@Override
public void writeCode() {
System.out.println("Developer " + name + " writes code");
}
}
測試代碼:創(chuàng)建一個Developer實例邻邮,名叫Jerry竣况,去寫代碼!
public class DeveloperTest {
public static void main(String[] args) {
IDeveloper jerry = new Developer("Jerry");
jerry.writeCode();
}
}
現(xiàn)在問題來了筒严。Jerry的項目經(jīng)理對Jerry光寫代碼丹泉,而不維護任何的文檔很不滿。假設(shè)哪天Jerry休假去了鸭蛙,其他的程序員來接替Jerry的工作摹恨,對著陌生的代碼一臉問號。經(jīng)全組討論決定娶视,每個開發(fā)人員寫代碼時晒哄,必須同步更新文檔。
為了強迫每個程序員在開發(fā)時記著寫文檔肪获,而又不影響大家寫代碼這個動作本身, 我們不修改原來的Developer類揩晴,而是創(chuàng)建了一個新的類,同樣實現(xiàn)IDeveloper接口贪磺。這個新類DeveloperProxy內(nèi)部維護了一個成員變量,指向原始的IDeveloper實例:
public class DeveloperProxy implements IDeveloper{
private IDeveloper developer;
public DeveloperProxy(IDeveloper developer){
this.developer = developer;
}
@Override
public void writeCode() {
System.out.println("Write documentation...");
this.developer.writeCode();
}
}
這個代理類實現(xiàn)的writeCode方法里诅愚,在調(diào)用實際程序員writeCode方法之前寒锚,加上一個寫文檔的調(diào)用,這樣就確保了程序員寫代碼時都伴隨著文檔更新违孝。
靜態(tài)代理方式的優(yōu)點
易于理解和實現(xiàn)
代理類和真實類的關(guān)系是編譯期靜態(tài)決定的刹前,和下文馬上要介紹的動態(tài)代理比較起來,執(zhí)行時沒有任何額外開銷雌桑。
靜態(tài)代理方式的缺點
每一個真實類都需要一個創(chuàng)建新的代理類喇喉。還是以上述文檔更新為例,假設(shè)老板對測試工程師也提出了新的要求校坑,讓測試工程師每次測出bug時拣技,也要及時更新對應(yīng)的測試文檔。那么采用靜態(tài)代理的方式耍目,測試工程師的實現(xiàn)類ITester也得創(chuàng)建一個對應(yīng)的ITesterProxy類膏斤。
public interface ITester {
public void doTesting();
}
Original tester implementation class:
public class Tester implements ITester {
private String name;
public Tester(String name){
this.name = name;
}
@Override
public void doTesting() {
System.out.println("Tester " + name + " is testing code");
}
}
public class TesterProxy implements ITester{
private ITester tester;
public TesterProxy(ITester tester){
this.tester = tester;
}
@Override
public void doTesting() {
System.out.println("Tester is preparing test documentation...");
tester.doTesting();
}
}
正是因為有了靜態(tài)代碼方式的這個缺點,才誕生了Java的動態(tài)代理實現(xiàn)方式邪驮。
Java動態(tài)代理實現(xiàn)方式一:InvocationHandler
jdk動態(tài)代理是jre提供給我們的類庫莫辨,可以直接使用,不依賴第三方。先看下jdk動態(tài)代理的使用代碼沮榜,再理解原理盘榨。
首先有個“明星”接口類,有唱蟆融、跳兩個功能:
package proxy;
public interface Star
{
String sing(String name);
String dance(String name);
}
再有個明星實現(xiàn)類“劉德華”:
package proxy;
public class LiuDeHua implements Star{
@Override
public String sing(String name){
System.out.println("給我一杯忘情水");
return "唱完" ;
}
@Override
public String dance(String name){
System.out.println("開心的馬騮");
return "跳完" ;
}
}
明星演出前需要有人收錢草巡,由于要準備演出,自己不做這個工作振愿,一般交給一個經(jīng)紀人捷犹。便于理解,它的名字以Proxy結(jié)尾冕末,但他不是代理類萍歉,原因是它沒有實現(xiàn)我們的明星接口,無法對外服務(wù)档桃,它僅僅是一個wrapper枪孩。
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class StarProxy implements InvocationHandler{ // 目標(biāo)類,也就是被代理對象
private Object target;
public void setTarget(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ // 這里可以做增強
System.out.println("收錢");
Object result = method.invoke(target, args);
return result;
} // 生成代理類
public Object CreatProxyedObj(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
}
上述例子中藻肄,方法CreatProxyedObj返回的對象才是我們的代理類蔑舞,它需要三個參數(shù),前兩個參數(shù)的意思是在同一個classloader下通過接口創(chuàng)建出一個對象嘹屯,該對象需要一個屬性攻询,也就是第三個參數(shù),它是一個InvocationHandler州弟。需要注意的是這個CreatProxyedObj方法不一定非得在我們的StarProxy類中钧栖,往往放在一個工廠類中。上述代理的代碼使用過程一般如下:
1婆翔、new一個目標(biāo)對象
2拯杠、new一個InvocationHandler,將目標(biāo)對象set進去
3啃奴、通過CreatProxyedObj創(chuàng)建代理對象潭陪,強轉(zhuǎn)為目標(biāo)對象的接口類型即可使用,實際上生成的代理對象實現(xiàn)了目標(biāo)接口最蕾。
Star ldh = new LiuDeHua();
StarProxy proxy = new StarProxy();
proxy.setTarget(ldh);
Object obj = proxy.CreatProxyedObj();
Star star = (Star)obj;
Proxy(jdk類庫提供)根據(jù)B的接口生成一個實現(xiàn)類依溯,我們成為C,它就是動態(tài)代理類(該類型是 $Proxy+數(shù)字 的“新的類型”)瘟则。生成過程是:由于拿到了接口誓沸,便可以獲知接口的所有信息(主要是方法的定義),也就能聲明一個新的類型去實現(xiàn)該接口的所有方法壹粟,這些方法顯然都是“虛”的拜隧,它調(diào)用另一個對象的方法宿百。當(dāng)然這個被調(diào)用的對象不能是對象B,如果是對象B洪添,我們就沒法增強了垦页,等于饒了一圈又回來了。
所以它調(diào)用的是B的包裝類干奢,這個包裝類需要我們來實現(xiàn)痊焊,但是jdk給出了約束,它必須實現(xiàn)InvocationHandler忿峻,上述例子中就是StarProxy薄啥, 這個接口里面有個方法,它是所有Target的所有方法的調(diào)用入口(invoke)逛尚,調(diào)用之前我們可以加自己的代碼增強垄惧。
看下我們的實現(xiàn),我們在InvocationHandler里調(diào)用了對象B(target)的方法绰寞,調(diào)用之前增強了B的方法到逊。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
// 這里增強 System.out.println("收錢");
Object result = method.invoke(target, args);
return result;
}
所以可以這么認為C代理了InvocationHandler,InvocationHandler代理了我們的類B滤钱,兩級代理觉壶。
整個JDK動態(tài)代理的秘密也就這些,簡單一句話件缸,動態(tài)代理就是要生成一個包裝類對象铜靶,由于代理的對象是動態(tài)的,所以叫動態(tài)代理他炊。由于我們需要增強旷坦,這個增強是需要留給開發(fā)人員開發(fā)代碼的,因此代理類不能直接包含被代理對象佑稠,而是一個InvocationHandler,該InvocationHandler包含被代理對象旗芬,并負責(zé)分發(fā)請求給被代理對象舌胶,分發(fā)前后均可以做增強。從原理可以看出疮丛,JDK動態(tài)代理是“對象”的代理幔嫂。
下面看下動態(tài)代理類到底如何調(diào)用的InvocationHandler的,為什么InvocationHandler的一個invoke方法能為分發(fā)target的所有方法誊薄。C中的部分代碼示例如下履恩,通過反編譯生成后的代碼查看,摘自鏈接地址呢蔫。Proxy創(chuàng)造的C是自己(Proxy)的子類切心,且實現(xiàn)了B的接口飒筑,一般都是這么修飾的:
public final class XXX extends Proxy implements XXX
一個方法代碼如下:
public final String SayHello(String paramString){
try {
return (String)this.h.invoke(this, m4, new Object[] { paramString });
} catch (Error|RuntimeException localError) {
throw localError;
} catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}
可以看到,C中的方法全部通過調(diào)用h實現(xiàn)绽昏,其中h就是InvocationHandler协屡,是我們在生成C時傳遞的第三個參數(shù)。這里還有個關(guān)鍵就是SayHello方法(業(yè)務(wù)方法)跟調(diào)用invoke方法時傳遞的參數(shù)m4一定要是一一對應(yīng)的全谤,但是這些對我們來說都是透明的肤晓,由Proxy在newProxyInstance時保證的。留心看到C在invoke時把自己this傳遞了過去认然,InvocationHandler的invoke的第一個方法也就是我們的動態(tài)代理實例類补憾,業(yè)務(wù)上有需要就可以使用它。(所以千萬不要在invoke方法里把請求分發(fā)給第一個參數(shù)卷员,否則很明顯就死循環(huán)了)
C類中有B中所有方法的成員變量
private static Method m1;
private static Method m3;
private static Method m4;
private static Method m2;
private static Method m0;
這些變量在static靜態(tài)代碼塊初始化盈匾,這些變量是在調(diào)用invocationhander時必要的入?yún)ⅲ沧屛覀円老】吹絇roxy在生成C時留下的痕跡子刮。
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("jiankunking.Subject").getMethod("SayGoodBye", new Class[0]);
m4 = Class.forName("jiankunking.Subject").getMethod("SayHello", new Class[] { Class.forName("java.lang.String")});
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; }
catch (NoSuchMethodException localNoSuchMethodException) {
throw new NoSuchMethodError(localNoSuchMethodException.getMessage()); }
catch (ClassNotFoundException localClassNotFoundException) {
throw new NoClassDefFoundError(localClassNotFoundException.getMessage()); }
}
從以上分析來看威酒,要想徹底理解一個東西,再多的理論不如看源碼挺峡,底層的原理非常重要葵孤。
jdk動態(tài)代理類圖如下
Java動態(tài)代理實現(xiàn)方式二:cglib動態(tài)代理
我們了解到,“代理”的目的是構(gòu)造一個和被代理的對象有同樣行為的對象橱赠,一個對象的行為是在類中定義的尤仍,對象只是類的實例。所以構(gòu)造代理狭姨,不一定非得通過持有宰啦、包裝對象這一種方式。
通過“繼承”可以繼承父類所有的公開方法饼拍,然后可以重寫這些方法赡模,在重寫時對這些方法增強,這就是cglib的思想师抄。根據(jù)里氏代換原則(LSP)漓柑,父類需要出現(xiàn)的地方,子類可以出現(xiàn)叨吮,所以cglib實現(xiàn)的代理也是可以被正常使用的辆布。
先看下代碼
package proxy;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor{
// 根據(jù)一個類型產(chǎn)生代理類,此方法不要求一定放在MethodInterceptor中
public Object CreatProxyedObj(Class<?> clazz){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create(); }
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable{
// 這里增強
System.out.println("收錢");
return arg3.invokeSuper(arg0, arg2);
}
}
從代碼可以看出茶鉴,它和jdk動態(tài)代理有所不同锋玲,對外表現(xiàn)上看CreatProxyedObj,它只需要一個類型clazz就可以產(chǎn)生一個代理對象涵叮, 所以說是“類的代理”惭蹂,且創(chuàng)造的對象通過打印類型發(fā)現(xiàn)也是一個新的類型伞插。不同于jdk動態(tài)代理,jdk動態(tài)代理要求對象必須實現(xiàn)接口(三個參數(shù)的第二個參數(shù))剿干,cglib對此沒有要求蜂怎。
cglib的原理是這樣,它生成一個繼承B的類型C(代理類)置尔,這個代理類持有一個MethodInterceptor杠步,我們setCallback時傳入的。 C重寫所有B中的方法(方法名一致)榜轿,然后在C中幽歼,構(gòu)建名叫“CGLIB”+“”的方法(下面叫cglib方法,所有非private的方法都會被構(gòu)建)谬盐,方法體里只有一句話super.方法名()甸私,可以簡單的認為保持了對父類方法的一個引用,方便調(diào)用飞傀。
這樣的話皇型,C中就有了重寫方法、cglib方法砸烦、父類方法(不可見)弃鸦,還有一個統(tǒng)一的攔截方法(增強方法intercept)。其中重寫方法和cglib方法肯定是有映射關(guān)系的幢痘。
C的重寫方法是外界調(diào)用的入口(LSP原則)唬格,它調(diào)用MethodInterceptor的intercept方法,調(diào)用時會傳遞四個參數(shù)颜说,第一個參數(shù)傳遞的是this购岗,代表代理類本身,第二個參數(shù)標(biāo)示攔截的方法门粪,第三個參數(shù)是入?yún)⒑盎谒膫€參數(shù)是cglib方法,intercept方法完成增強后玄妈,我們調(diào)用cglib方法間接調(diào)用父類方法完成整個方法鏈的調(diào)用乾吻。
這里有個疑問就是intercept的四個參數(shù),為什么我們使用的是arg3而不是arg1?
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable{
System.out.println("收錢");
return arg3.invokeSuper(arg0, arg2);
}
因為如果我們通過反射 arg1.invoke(arg0, ...)這種方式是無法調(diào)用到父類的方法的措近,子類有方法重寫,隱藏了父類的方法女淑,父類的方法已經(jīng)不可見瞭郑,如果硬調(diào)arg1.invoke(arg0, ...)很明顯會死循環(huán)。
所以調(diào)用的是cglib開頭的方法鸭你,但是屈张,我們使用arg3也不是簡單的invoke擒权,而是用的invokeSuper方法,這是因為cglib采用了fastclass機制阁谆,不僅巧妙的避開了調(diào)不到父類方法的問題碳抄,還加速了方法的調(diào)用。
fastclass基本原理是场绿,給每個方法編號剖效,通過編號找到方法執(zhí)行避免了通過反射調(diào)用。
對比JDK動態(tài)代理焰盗,cglib依然需要一個第三者分發(fā)請求璧尸,只不過jdk動態(tài)代理分發(fā)給了目標(biāo)對象,cglib最終分發(fā)給了自己熬拒,通過給method編號完成調(diào)用爷光。cglib是繼承的極致發(fā)揮,本身還是很簡單的澎粟,只是fastclass需要另行理解蛀序。
測試
public static void main(String[] args){
int times = 1000000;
Star ldh = new LiuDeHua();
StarProxy proxy = new StarProxy();
proxy.setTarget(ldh);
long time1 = System.currentTimeMillis();
Star star = (Star)proxy.CreatProxyedObj();
long time2 = System.currentTimeMillis();
System.out.println("jdk創(chuàng)建時間:" + (time2 - time1));
CglibProxy proxy2 = new CglibProxy();
long time5 = System.currentTimeMillis();
Star star2 = (Star)proxy2.CreatProxyedObj(LiuDeHua.class);
long time6 = System.currentTimeMillis();
System.out.println("cglib創(chuàng)建時間:" + (time6 - time5));
long time3 = System.currentTimeMillis();
for (int i = 1; i <= times; i++) {
star.sing("ss"); star.dance("ss");
}
long time4 = System.currentTimeMillis();
System.out.println("jdk執(zhí)行時間" + (time4 - time3));
long time7 = System.currentTimeMillis();
for (int i = 1; i <= times; i++) {
star2.sing("ss");
star2.dance("ss");
}
long time8 = System.currentTimeMillis();
System.out.println("cglib執(zhí)行時間" + (time8 - time7));
}
經(jīng)測試,jdk創(chuàng)建對象的速度遠大于cglib活烙,這是由于cglib創(chuàng)建對象時需要操作字節(jié)碼徐裸。cglib執(zhí)行速度略大于jdk,所以比較適合單例模式瓣颅。另外由于CGLIB的大部分類是直接對Java字節(jié)碼進行操作倦逐,這樣生成的類會在Java的永久堆中。如果動態(tài)代理操作過多宫补,容易造成永久堆滿檬姥,觸發(fā)OutOfMemory異常。spring默認使用jdk動態(tài)代理粉怕,如果類沒有接口健民,則使用cglib。
原博客:https://my.oschina.net/u/3771578/blog/2249801
作者:遠舉高飛
原博客:https://blog.csdn.net/flyfeifei66/article/details/81481222