設計模式-靜態(tài)代理與動態(tài)代理

  • 靜態(tài)代理: 由程序員創(chuàng)建或工具生成代理類的源碼榨汤,再編譯代理類。所謂靜態(tài)也就是在程序運行前就已經存在代理類的字節(jié)碼文件馏艾,代理類和委托類的關系在運行前就確定了举瑰。
  • 動態(tài)代理: 在實現階段不用關心代理類捣辆,而在運行階段才指定哪一個對象。

情境

假設此迅,有個汽車類具有移動停止兩個方法汽畴,我們要怎么在不改動源碼的情況下:

1.添加日志

2.添加事務

IMovable.java

public interface IMovable {
    void move();

    void stop();
}

Car.java

public class Car implements IMovable {

    @Override
    public void move() {
        System.out.println("汽車移動");
    }

    @Override
    public void stop() {
        System.out.println("汽車停止");
    }
}

靜態(tài)代理

繼承

1.添加日志

CarLog.java

public class CarLog extends Car {
    @Override
    public void move() {
        System.out.println("開始執(zhí)行move");
        super.move();
        System.out.println("執(zhí)行move完成");
    }

    @Override
    public void stop() {
        System.out.println("開始執(zhí)行stop");
        super.stop();
        System.out.println("執(zhí)行stop完成");
    }
}

Client.java

public class Client {
    public static void main(String[] args) {
        IMovable log = new CarLog();
        log.move();
        log.stop();
    }
}
  • 從上面的代碼可以看出旧巾,我們定義了一個類并且繼承于Car
  • 重寫父類中的方法,在super(調用父類中的方法)前后加入打印日志的代碼

運行截圖:

靜態(tài)代理_繼承_日志.png

2.添加事務

CarTransaction.java

public class CarTransaction extends Car {
    @Override
    public void move() {
        System.out.println("move事務開始");
        super.move();
        System.out.println("move事務提交");
    }

    @Override
    public void stop() {
        System.out.println("stop事務開始");
        super.stop();
        System.out.println("stop事務提交");
    }
}

運行結果:

靜態(tài)代理_繼承_事務.png
  • 很明顯忍些,對于事務的做法與日志的做法一致

3.先添加日志再開啟事務

CarLog2Trans.java

public class CarLog2Trans extends CarTransaction{
    @Override
    public void move() {
        System.out.println("開始執(zhí)行move");
        super.move();
        System.out.println("執(zhí)行move完成");
    }

    @Override
    public void stop() {
        System.out.println("開始執(zhí)行stop");
        super.stop();
        System.out.println("執(zhí)行stop完成");
    }
}

運行結果:

靜態(tài)代理_繼承_先日志后事務.png

4.先開啟事務再添加日志

CarTrans2Log.java

public class CarTrans2Log extends CarLog {
    @Override
    public void move() {
        System.out.println("move事務開始");
        super.move();
        System.out.println("move事務提交");
    }

    @Override
    public void stop() {
        System.out.println("stop事務開始");
        super.stop();
        System.out.println("stop事務提交");
    }
}

運行結果:

靜態(tài)代理_繼承_先事務后日志.png
  • 從上面代碼可以看出如果我們添加功能的話鲁猩,就要創(chuàng)建新的類

情境: 有四輛汽車A,B,C,D,A汽車要做到先添加日志再開啟事務罢坝,B汽車要做到先開啟事務再添加日志,C汽車只需要添加日志廓握,D汽車只需要開啟事務

顯然為了完成這樣的功能使用繼承的方式,我們必須要有四個類才能完成嘁酿,哪有沒有更好的方式呢隙券?

接口(聚合)

1.添加日志

CarLogProxy.java

public class CarLogProxy implements IMovable {
    private IMovable movable;

    public CarLogProxy(IMovable movable) {
        this.movable = movable;
    }

    @Override
    public void move() {
        System.out.println("開始執(zhí)行move");
        movable.move();
        System.out.println("執(zhí)行move完成");
    }

    @Override
    public void stop() {
        System.out.println("開始執(zhí)行stop");
        movable.stop();
        System.out.println("執(zhí)行stop完成");
    }
}

Client.java

public class Client {
    public static void main(String[] args) {
        IMovable movable = new Car();
        IMovable log = new CarLogProxy(movable);
        log.move();
        log.stop();
    }
}
  • 從上面的代碼可以看出,我們實現了IMovable接口(目標接口)闹司,并傳入了需要被代理的對象

2.添加事務

CarTransactionProxy.java

public class CarTransactionProxy implements IMovable {
private IMovable movable;

public CarTransactionProxy(IMovable movable) {
    this.movable = movable;
}

@Override
public void move() {
    System.out.println("move事務開始");
    movable.move();
    System.out.println("move事務提交");
}

@Override
public void stop() {
    System.out.println("stop事務開始");
    movable.stop();
    System.out.println("stop事務提交");
}

}

Client.java

public class Client {
    public static void main(String[] args) {
        IMovable movable = new Car();
        IMovable transaction = new CarTransactionProxy(movable);
        transaction.move();
        transaction.stop();
    }
}

3.先添加日志再開啟事務

Client.java

public class Client {
    public static void main(String[] args) {
        IMovable movable = new Car();
        IMovable transaction = new CarTransactionProxy(movable);
        IMovable log = new CarLogProxy(transaction);
        log.move();
        log.stop();
    }
}

4.先開啟事務再添加日志

Client.java

public class Client {
    public static void main(String[] args) {
        IMovable movable = new Car();
        IMovable log = new CarLogProxy(movable);
        IMovable transaction = new CarTransactionProxy(log);
        transaction.move();
        transaction.stop();
    }
}
  • 從3與4的Client可以看出娱仔,使用聚合的辦法就只要用兩個類就能實現需求
  • 顯然,使用實現目標接口的方式進行代理游桩,讓代理和被代理對象之間都可以相互靈活轉換
  • 所以一般靜態(tài)代理使用聚合的方式進行實現牲迫,使用繼承的方式多多少少有些過于笨重

想必認真的人都看的出來,靜態(tài)代理的方式隨著功能的增多借卧,必然要生成更多的代理對象盹憎,這樣不利于維護。而且谓娃,就目前的要求來看脚乡,對 move()stop() 兩個方法添加日志,其中代碼出現了冗余的情況滨达,無法復用。那么有什么方式可以解決呢俯艰?

動態(tài)代理

Client.java

public class Client {
    public static void main(String[] args) {
        IMovable movable = new Car();
        IMovable logProxy = (IMovable) Proxy.newProxyInstance(Car.class.getClassLoader(), Car.class.getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("開始執(zhí)行" + method.getName());
                        Object invoke =method.invoke(movable, args);
                        System.out.println("執(zhí)行" + method.getName() + "完成");
                        return invoke;
                    }
                });
        logProxy.move();
        logProxy.stop();
        
        System.out.println();

        IMovable transProxy = (IMovable) Proxy.newProxyInstance(Car.class.getClassLoader(), Car.class.getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println(method.getName() + "開啟事務");
                        Object invoke = method.invoke(logProxy, args);
                        System.out.println(method.getName() + "事務提交");
                        return invoke;
                    }
                });
        transProxy.move();
        transProxy.stop();
    }
}

運行結果:

動態(tài)代理.PNG
  • 從運行結果來看捡遍,我們使用動態(tài)代理實現了上面靜態(tài)代理的例子,且沒有編寫多余的類
  • 從上面的代碼可以看出竹握,要使用動態(tài)代理就必須要有目標接口
  • InvocationHandler 的方法中可以獲取要執(zhí)行的 Method 實例
  • 通過 Method 的實例可以通過反射來執(zhí)行画株,不過要傳入被代理對象
  • 在反射前后可以進行添加日志和事務的操作
  • 而且也可以靈活的讓進行代理對象與被代理對象之間的轉換
  • 由于使用了反射,對性能有一定的損耗

動態(tài)代理源碼解析

對于動態(tài)代理的源碼其實最重要的就是下面兩個方法啦辐,我們下面開始對他們進行深入分析谓传,做到知其然知其所以然。

Proxy.newProxyInstance 部分代碼

 public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
     ...
     
     Class<?> cl = getProxyClass0(loader, intfs);
     try {
        ...
        
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        ...
        

            return cons.newInstance(new Object[]{h});
        }catch (IllegalAccessException|InstantiationException e) {
        throw new InternalError(e.toString(), e);
        }
        ...
        
}
  • loader 定義代理類的類加載器
  • interfaces 代理類要實現的接口列表
  • h 指派方法調用的調用處理程序(注:動態(tài)代理的關鍵)
  • 將其他多余的部分代碼忽略芹关,找核心的代碼(因為有些偏底層我也看不懂 -.- )
  • getProxyClass0(loader, intfs); 獲得代理類
  • cl.getConstructor(constructorParams); 獲得代理類的構造方法
  • cons.newInstance(new Object[]{h}); 反射生成代理對象续挟,并傳入 InvocationHandler

所以我們往下看看它是如何得到代理對象的

Proxy.getProxyClass0 代碼

    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

    
    private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
  • 注釋翻譯: 如果存在給定接口的給定裝入器定義的代理類存在,則只返回緩存的副本侥衬;否則诗祸,它將通過proxyclassfactory創(chuàng)建代理類

所以我們就要進一步分析 (proxyClassCache)WeakCache 類是怎么進行緩存的跑芳。(個人能力有限對于WeakCache還有較多疑惑,之后會進行總結更新)

學習資料:

https://www.cnblogs.com/liuyun1995/p/8144676.html

http://www.reibang.com/p/9f5566b5e7fb


知道了是得到創(chuàng)建代理類直颅,我們繼續(xù)往下分析

InvocationHandler.java

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
  • proxy 在其上調用方法的代理實例
  • method 對應于在代理實例上調用的接口方法的 Method 實例博个,目標對象被調用的方法
  • args 包含傳入代理實例上方法調用的參數值的對象數組

InvocationHandler用來連接代理對象與目標對象

分析代理類對象
我們可以使用如下代碼獲取代理類$Proxy0.class文件

        public static void main(String[] args) {
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    System.out.println("$Proxy0.class: "+Proxy.getProxyClass(Inter.class.getClassLoader(), Inter.class));   
    //
    IMovable movable = new Car();
    IMovable logProxy = (IMovable) Proxy.newProxyInstance(Car.class.getClassLoader(), Car.class.getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("開始執(zhí)行" + method.getName());
                    Object invoke =method.invoke(movable, args);
                    System.out.println("執(zhí)行" + method.getName() + "完成");
                    return invoke;
                }
            });
    logProxy.move();
    logProxy.stop();
}

運行結果:

獲取$Proxy0.class文件運行截圖.png
  • System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 打開保存開關
  • System.out.println("$Proxy0.class全名: "+Proxy.getProxyClass(IMovable.class.getClassLoader(), IMovable.class)); 可以通過打印信息獲取class在項目中路徑
  • 關于這個類是如何生成的,需要往下跟蹤 Proxy.getProxyClass0方法中的proxyClassCache.get(loader, interfaces),這與緩存相掛鉤未進行詳細分析(之后還會更新文章)功偿。

在得到 $Proxy0.class 之后我們可以使用一些工具將class進行反編譯,這里我使用了JD_GUI

$Proxy0.java部分代碼

public final class $Proxy0
  extends Proxy
  implements IMovable
{
  private static Method m1;
  private static Method m4;
  private static Method m2;
  private static Method m3;
  private static Method m0;
  
  public $Proxy0(InvocationHandler paramInvocationHandler)
    throws 
  {
    super(paramInvocationHandler);
  }
  
  ...
  
  public final void move()
    throws 
  {
    try
    {
      this.h.invoke(this, m4, null);
      return;
    }
    catch ...
  }
  

  
  public final void stop()
    throws 
  {
    try
    {
      this.h.invoke(this, m3, null);
      return;
    }
    catch ...
  }
  
    ...
  
  static
  {
    try
    {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m4 = Class.forName("com.zzz.proxy.IMovable").getMethod("move", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m3 = Class.forName("com.zzz.proxy.IMovable").getMethod("stop", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch ...
  }
}

  • 構造方法傳入InvocationHandler
  • 在靜態(tài)代碼塊中盆佣,可以看到我們接口定義的 movestop 分別為 m4m3
  • 所以在我們調用代理對象時,就使用 InvocationHandler 回調出去械荷,而 invoke 方法正是由我們實現的罪塔。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市养葵,隨后出現的幾起案子征堪,更是在濱河造成了極大的恐慌,老刑警劉巖关拒,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佃蚜,死亡現場離奇詭異,居然都是意外死亡着绊,警方通過查閱死者的電腦和手機谐算,發(fā)現死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來归露,“玉大人洲脂,你說我怎么就攤上這事【绨” “怎么了恐锦?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長疆液。 經常有香客問我一铅,道長,這世上最難降的妖魔是什么堕油? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任潘飘,我火速辦了婚禮,結果婚禮上掉缺,老公的妹妹穿的比我還像新娘卜录。我一直安慰自己,他們只是感情好眶明,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布艰毒。 她就那樣靜靜地躺著,像睡著了一般赘来。 火紅的嫁衣襯著肌膚如雪现喳。 梳的紋絲不亂的頭發(fā)上凯傲,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音嗦篱,去河邊找鬼冰单。 笑死,一個胖子當著我的面吹牛灸促,可吹牛的內容都是我干的诫欠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼浴栽,長吁一口氣:“原來是場噩夢啊……” “哼荒叼!你這毒婦竟也來了?” 一聲冷哼從身側響起典鸡,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤被廓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后萝玷,有當地人在樹林里發(fā)現了一具尸體嫁乘,經...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年球碉,在試婚紗的時候發(fā)現自己被綠了蜓斧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡睁冬,死狀恐怖挎春,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情豆拨,我是刑警寧澤直奋,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站辽装,受9級特大地震影響帮碰,放射性物質發(fā)生泄漏。R本人自食惡果不足惜拾积,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丰涉。 院中可真熱鬧拓巧,春花似錦、人聲如沸一死。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽投慈。三九已至承耿,卻和暖如春冠骄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背加袋。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工凛辣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人职烧。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓扁誓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蚀之。 傳聞我的和親對象是個殘疾皇子蝗敢,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內容

  • 參考資料:菜鳥教程之設計模式 設計模式概述 設計模式(Design pattern)代表了最佳的實踐,通常被有經驗...
    Steven1997閱讀 1,176評論 1 12
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理足删,服務發(fā)現寿谴,斷路器,智...
    卡卡羅2017閱讀 134,693評論 18 139
  • 還記得初中的一個冬天失受,老爸給我2元錢去買紅蘿卜讶泰,我在去菜市場的路上碰到一個騎著三輪車的菜販子。 他賣的紅蘿卜是一元...
    韋哥說道閱讀 1,452評論 0 0
  • 荊棘鳥生而尋刺贱纠,途遠而不知疲倦 棘刺深扎而泣血峻厚,超脫痛苦而放歌 奏極美之曲,曲終而命竭 羞云雀與夜鶯谆焊,感上帝于蒼穹...
    蔥蔥王閱讀 636評論 0 3