代理模式

文章首發(fā)于我的個人博客,歡迎訪問:https://blog.itzhouq.cn/proxy

在代理模式(Proxy Pattern)中椎木,一個類代表另一個類的功能晒他。這種類型的設(shè)計模式屬于結(jié)構(gòu)型模式秸侣。在代理模式中改淑,我們創(chuàng)建具有現(xiàn)有對象的對象,以便向外界提供功能接口痴施。代理模式的優(yōu)勢是實現(xiàn)了無侵入的代理擴展擎厢,也就是方法的增強究流;讓你可以在不用修改源碼的情況下,增強一些方法动遭。

為什么要學習代理模式呢芬探?因為 Spring AOP 的底層就是代理模式。

現(xiàn)在模擬一個場景:房東需要出租自己的房子厘惦。

實際落地時候?qū)⒊鲎膺@個動作抽象為接口:

/**
 * 租房的接口
 */
public interface Rent {
    public void rent();
}

房東(Host)實現(xiàn)了這個接口偷仿,執(zhí)行一個租房的方法。

/**
 * 房東
 */
public class Host implements Rent {
    @Override
    public void rent() {
        System.out.println("房東要出租房子");
    }
}

這個例子在日常生活中很常見宵蕉。但是我們考慮到實際情況酝静,并不是所有的房東都有那么多自由的時間和資源,不上班就為了把自己的房子租出去的羡玛,為了省事他們都會借助于第三方公司做這個事情别智,就是我們常說的中介,這里的中介就是這個代理對象稼稿。

1薄榛、靜態(tài)代理

為什么要加一個代理對象呢?因為代理對象可以做一些額外的操作渺杉。比如這里中介除了可以幫房東租房子以外蛇数,還能帶客戶看房子,簽合同是越,收中介費等。

image

上面的接口和類不需要變動碌上,現(xiàn)在增加代理的功能倚评。

/**
 * 代理類:其作用是幫房東租房子
 */
public class Proxy implements Rent {
    private Host host;

    public Proxy() {
    }
    public Proxy(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        seeHouse();
        host.rent();
        fare();
    }

    public void seeHouse() {
        System.out.println("中介帶你看房子");
    }
    public void fare() {
        System.out.println("中介收中介費");
    }
}

模擬租房的過程:

/**
 * 模擬租房的過程
 */
public class Client {
    public static void main(String[] args) {
        // 房東要租房子
        Host host = new Host();
        // 代理角色:中介,幫房東租房子馏予,除此之外提供一些額外的服務(wù)
        Proxy proxy = new Proxy(host);
        // 你不用面對房東天梧,直接找中介即可
        proxy.rent();
    }
}

運行的結(jié)果:

中介帶你看房子
房東要出租房子
中介收中介費

可以看到代理類對被代理的對象進行了增強。

角色分析:

  • 抽象角色:一般會使用接口或者抽象類解決
  • 真實角色:被代理的對象
  • 代理角色:代理真實角色霞丧,代理真實角色后呢岗,我們一般會做一些附屬操作
  • 客戶:訪問代理對象的人。

2蛹尝、靜態(tài)代理舉例

再看一個實際開發(fā)中遇到的問題后豫。有個接口UserService,其實現(xiàn)類 UserServiceimpl中有很多方法⊥荒牵現(xiàn)在有個需求挫酿,需要在執(zhí)行的方法前面添加日志,知道哪個方法執(zhí)行了愕难,但是不改變原有的UserServiceimpl實現(xiàn)類早龟。

如果直接修改這個實現(xiàn)類會有以下幾個問題:

  • 可能這個實現(xiàn)類不能讓你直接修改
  • 這個實現(xiàn)類可以修改惫霸,但是修改的話有增加 BUG 的風險
  • 這個實現(xiàn)類可以修改,但是實現(xiàn)類中方法很多葱弟,需要添加大量重復(fù)的代碼

這個時候代理模式就能排上用場了壹店。

接口:

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void query();
}

實現(xiàn)類:

/**
 * 真實對象
 */
public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        System.out.println("增加了一個用戶");
    }

    @Override
    public void delete() {
        System.out.println("刪除了一個用戶");
    }

    @Override
    public void update() {
        System.out.println("更新了一個用戶");
    }

    @Override
    public void query() {
        System.out.println("查詢了一個用戶");
    }
}
/**
 * 模擬用戶的操作
 */
public class UserAction {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        userService.add();
    }
}

現(xiàn)在模擬添加日志的需求:

添加一個代理類,實現(xiàn) UserService接口芝加,注入UserServiceImpl硅卢,添加日志的方法:

public class UserServiceProxy implements UserService {
    private UserServiceImpl userService;
    public UserServiceProxy(UserServiceImpl userService) {
        this.userService = userService;
    }

    @Override
    public void add() {
        log("add");
        userService.add();
    }

    @Override
    public void delete() {
        log("delete");
        userService.delete();
    }

    @Override
    public void update() {
        log("update");
        userService.update();
    }

    @Override
    public void query() {
        log("query");
        userService.query();
    }

    // 日志方法
    public void log (String msg) {
        System.out.println("執(zhí)行了" + msg + "方法");
    }
}

模擬用戶操作:

/**
 * 模擬用戶的操作
 */
public class UserAction {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
//        userService.add();
        UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
        userServiceProxy.add();
    }
}

運行結(jié)果:

執(zhí)行了add方法
增加了一個用戶

通過這個例子,可以很好的理解靜態(tài)代理模式的使用場景和使用方法妖混。

image

3老赤、動態(tài)代理

  • 動態(tài)代理和靜態(tài)代理角色一樣
  • 動態(tài)代理的代理類是動態(tài)生成的,不是我們直接寫好的制市。
  • 動態(tài)代理分為兩大類:基于接口的動態(tài)代理和基于類的動態(tài)代理
    • 基于接口 --- JDK 動態(tài)代理
    • 基于類 --- cglib
    • Java 字節(jié)碼 --- javassist

3抬旺、1 JDK 自帶的動態(tài)代理

  • java.lang.reflect.Proxy:生成動態(tài)代理類和對象;提供了靜態(tài)方法祥楣,可以創(chuàng)建動態(tài)代理類和實例开财。
  • java.lang.reflect.InvocationHandler(處理器接口):可以通過invoke方法實現(xiàn)對真實角色的代理訪問。

每次通過 Proxy 生成的代理類對象都要指定對應(yīng)的處理器對象误褪。

下面的代碼演示了责鳍,如何通過 JDK 的動態(tài)代理生成一個特定代理對象,進而對被代理對象進行增強兽间。

接口:Subject.java

public interface Subject {
    public int sellBooks();

    public String speak();
}

真實對象:RealSubject.java

public class RealSubject implements Subject {
    @Override
    public int sellBooks() {
        System.out.println("賣書");
        return 1;
    }

    @Override
    public String speak() {
        System.out.println("說話");
        return "張三";
    }
}

處理器對象:MyInvocationHandler.java

等下就是使用這個處理器對真實對象的方法進行增強历葛。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 處理器對象
 */
public class MyInvocationHandler implements InvocationHandler {
    /**
     * 因為需要處理真實對象,需要把真實角色傳進來
     */
    Subject realSubject;

    public MyInvocationHandler(Subject realSubject) {
        this.realSubject = realSubject;
    }

    /**
     * @param proxy     代理類
     * @param method    正在調(diào)用的方法
     * @param args      方法的參數(shù)
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("調(diào)用代理類");

        if (method.getName().equalsIgnoreCase("sellBooks")) {
            int invoke = (int) method.invoke(realSubject, args);
            System.out.println("調(diào)用的是賣書的方法");
            return invoke;
        } else {
            String string = (String) method.invoke(realSubject, args);
            System.out.println("調(diào)用的是說話的方法");
            return string;
        }
    }
}

調(diào)用端進行測試:Main.java

import java.lang.reflect.Proxy;
/**
 * 調(diào)用類
 */
public class Main {
    public static void main(String[] args) {
        // 真實對象
        Subject realSubject = new RealSubject();
        // 根據(jù) 真實對象 創(chuàng)建一個 處理器對象
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(realSubject);

        // 代理對象:這個代理對象是通過 Proxy 類的靜態(tài)方法嘀略,動態(tài)生成的
        Subject proxyClass = (Subject) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Subject.class}, myInvocationHandler);

        // proxyClass.sellBooks();

        proxyClass.speak();
    }
}

執(zhí)行結(jié)果:

調(diào)用代理類
說話
調(diào)用的是說話的方法

從結(jié)果可以看到通過生成代理對象恤溶,對真實對象的方法進行了增強。

值得注意的是帜羊,這個處理類對象myInvocationHandler 其實是Proxy.newProxyInstance方法的一個參數(shù)咒程,可以寫成匿名內(nèi)部類。這個可以看我之前寫的筆記增強一個對象的方法的三種方式帐姻,那里就是這么寫的。

3饥瓷、2 Cglib 動態(tài)代理

Cglib 動態(tài)代理是針對代理的類,動態(tài)生產(chǎn)一個子類扛伍,然后子類覆蓋代理類中的方法。如果是final 或是 private 修飾的刺洒,則不會被重寫鳖宾。Cglib 是一個功能強大逆航,高性能的代碼生成包。它為沒有實現(xiàn)接口的類提供代理因俐,為 JDK 的動態(tài)代理提供了很好的補充拇惋。通常可以使用 Java 的動態(tài)代理創(chuàng)建代理抹剩,但當要代理的類沒有實現(xiàn)接口或為了更好的性能撑帖,Cglib 是一個好的選擇。

Cglib 作為一個開源項目澳眷,其代碼托管在在 GitHub 胡嘿,地址為: https://github.com/cglib/cglib

使用 Cglib 需要導(dǎo)入相關(guān)依賴或者 jar 包钳踊。我這里導(dǎo)入 jar 包衷敌,下載地址: https://github.com/cglib/cglib/releases/tag/RELEASE_3_3_0 。除此之外拓瞪,Cglib正常與運行還需要asm.jar的支持( cglib 底層使用字節(jié)碼處理框架ASM缴罗,來轉(zhuǎn)換字節(jié)碼并生成新的類 ),下載地址: https://mvnrepository.com/artifact/org.ow2.asm/asm/7.2 祭埂。

注意:如果沒有導(dǎo)入asm的依賴會拋出異常:

Exception in thread "main" java.lang.NoClassDefFoundError: org/objectweb/asm/Type

下面通過一個簡單的例子來說明其使用方式:

需要被代理的類:

package cn.itzhouq.proxy.cglib;

/**
 * 被代理類
 */
public class Engineer {
    // 可以被代理
    public void eat() {
        System.out.println("工程師正在吃飯");
    }

    // final 方法不會被生成的子類覆蓋
    public final void work() {
        System.out.println("工程師正在工作");
    }

    // private 方法不會被生成的子類覆蓋
    private void play () {
        System.out.println("工程師正在玩游戲");
    }
}

可以看到這個被代理的類是沒有實現(xiàn)接口的面氓。

Cglib 代理類:

package cn.itzhouq.proxy.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

/**
 * CGLIB 代理類
 */
public class CglibProxy implements MethodInterceptor {
    private Object target;

    public CglibProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("###   before invocation");
        Object result = method.invoke(target, objects);
        System.out.println("###   end invocation");
        return result;
    }

    public static Object getProxy(Object target) {
        Enhancer enhancer = new Enhancer();
        // 設(shè)置需要代理的對象
        enhancer.setSuperclass(target.getClass());
        // 設(shè)置代理人
        enhancer.setCallback(new CglibProxy(target));
        return enhancer.create();
    }
}

測試方法:

package cn.itzhouq.proxy.cglib;

/**
 * 測試方法
 */
public class CglibMainTest {
    public static void main(String[] args) {
        // 生成 Cglib 代理類
        Engineer engineerProxy = (Engineer) CglibProxy.getProxy(new Engineer());
        // 調(diào)用相關(guān)方法
        engineerProxy.eat();
        engineerProxy.work();
        // engineerProxy.play(); // 該代理對象中沒有該方法
    }
}

運行結(jié)果:

###   before invocation
工程師正在吃飯
###   end invocation
工程師正在工作

通過這種代理方式,也可以對方法進行增強蛆橡。被 final修飾的方法不會被生成的代理類(也是子類)覆蓋侧但,被private修飾的方法壓根兒不會被代理類繼承。

關(guān)于 Cglib 的原理航罗,可以參考這篇文章: https://www.runoob.com/w3cnote/cglibcode-generation-library-intro.html

4屁药、代理模式和裝飾器模式的區(qū)別

兩者都是對類的方法進行擴展粥血,但是裝飾器模式強調(diào)的是增強自身,在被裝飾之后你能夠在被增強的類上使用增強后的功能酿箭。增強后你還是你复亏,只不過能力更強了而已;而代理模式則強調(diào)的要讓別人幫你去做一些本身與你業(yè)務(wù)沒有太多關(guān)系的職責(記錄日志缭嫡、設(shè)置緩存)缔御。代理模式是為了實現(xiàn)對象的控制 ,因為被代理的對象往往難以直接獲得或者其內(nèi)部不想暴露出來妇蛀。

關(guān)于這個細節(jié)耕突,可以這篇知乎回答笤成,作者舉的例子很形象。

Java中“裝飾模式”和“代理模式”有啥區(qū)別眷茁? - 知乎 https://www.zhihu.com/question/41988550/answer/462204684

5炕泳、代理模式的優(yōu)缺點

優(yōu)點:

  • 可以使真實角色的操作更加純粹!不用去關(guān)注一些公共的業(yè)務(wù)
  • 公共業(yè)務(wù)交給代理角色上祈!實現(xiàn)了業(yè)務(wù)的分工
  • 公共業(yè)務(wù)發(fā)生擴展的時候,方便集中管理

缺點:

  • 一個真實對象就會產(chǎn)生一個代理對象籽腕,代理量會翻倍。

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末左痢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖傅寡,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荐操,死亡現(xiàn)場離奇詭異托启,居然都是意外死亡攘宙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铺韧,“玉大人,你說我怎么就攤上這事塔逃。” “怎么了鹏溯?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵丙挽,是天一觀的道長颜阐。 經(jīng)常有香客問我吓肋,道長是鬼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任李剖,我火速辦了婚禮篙顺,結(jié)果婚禮上德玫,老公的妹妹穿的比我還像新娘椎麦。我一直安慰自己观挎,他們只是感情好,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著普气,像睡著了一般现诀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仔沿,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天绵跷,我揣著相機與錄音成福,去河邊找鬼。 笑死净当,一個胖子當著我的面吹牛像啼,可吹牛的內(nèi)容都是我干的潭苞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼甚颂,長吁一口氣:“原來是場噩夢啊……” “哼秀菱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赶么,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤辫呻,失蹤者是張志新(化名)和其女友劉穎放闺,沒想到半個月后缕坎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡搬葬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年急凰,在試婚紗的時候發(fā)現(xiàn)自己被綠了猜年。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡企孩,死狀恐怖勿璃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情补疑,我是刑警寧澤莲组,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布锹杈,位于F島的核電站迈着,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咬清。R本人自食惡果不足惜奴潘,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一画髓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杖小,春花似錦、人聲如沸予权。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厚者。三九已至,卻和暖如春账忘,著一層夾襖步出監(jiān)牢的瞬間熙宇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工蒋荚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留期升,地道東北人播赁。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓吨铸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舟奠。 傳聞我的和親對象是個殘疾皇子沼瘫,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

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