java代理模式AOP(靜態(tài)代理,動態(tài)代理) (轉(zhuǎn))

所謂代理模式,是指客戶端(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)點

  1. 易于理解和實現(xiàn)

  2. 代理類和真實類的關(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)代理類圖如下

image

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贫贝,一起剝皮案震驚了整個濱河市秉犹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌稚晚,老刑警劉巖崇堵,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異客燕,居然都是意外死亡鸳劳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門也搓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赏廓,“玉大人涵紊,你說我怎么就攤上這事♂C” “怎么了摸柄?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長既忆。 經(jīng)常有香客問我驱负,道長,這世上最難降的妖魔是什么尿贫? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任电媳,我火速辦了婚禮,結(jié)果婚禮上庆亡,老公的妹妹穿的比我還像新娘匾乓。我一直安慰自己,他們只是感情好又谋,可當(dāng)我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布拼缝。 她就那樣靜靜地躺著,像睡著了一般彰亥。 火紅的嫁衣襯著肌膚如雪咧七。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天任斋,我揣著相機與錄音继阻,去河邊找鬼。 笑死废酷,一個胖子當(dāng)著我的面吹牛瘟檩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播澈蟆,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼墨辛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了趴俘?” 一聲冷哼從身側(cè)響起睹簇,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寥闪,沒想到半個月后太惠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡疲憋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年凿渊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡嗽元,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出喂击,到底是詐尸還是另有隱情剂癌,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布翰绊,位于F島的核電站佩谷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏监嗜。R本人自食惡果不足惜谐檀,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望裁奇。 院中可真熱鬧桐猬,春花似錦、人聲如沸刽肠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽音五。三九已至惫撰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躺涝,已是汗流浹背厨钻。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坚嗜,地道東北人夯膀。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像惶傻,于是被迫代替她去往敵國和親棍郎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內(nèi)容