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{ // 目標類,也就是被代理對象 
    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一個目標對象

2、new一個InvocationHandler,將目標對象set進去

3、通過CreatProxyedObj創(chuàng)建代理對象,強轉(zhuǎn)為目標對象的接口類型即可使用,實際上生成的代理對象實現(xiàn)了目標接口。

 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)用另一個對象的方法。當然這個被調(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包含被代理對象,并負責分發(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)?,也讓我們依稀看到Proxy在生成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ù)標示攔截的方法,第三個參數(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ā)給了目標對象,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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容