Java設計模式之代理模式詳解

一、代理模式

代理模式就是有一個張三,別人都沒有辦法找到他,隻有他的秘書可以找到他。那其他人想和張三交互,隻能通過他的秘書來進行轉達交互。這個秘書就是代理者,他代理張三。

再看看另一個例子:賣房子

賣房子的步驟:

1.找買傢

2.談價錢

3.簽合同

4.和房產局簽訂一些亂七八糟轉讓協議

一般賣傢隻在簽合同的時候可能出面一下,其他的1,2,4都由中介去做。那你問這樣有什麼用呢?

首先,一個中介可以代理多個賣房子的賣傢,其次,我們可以在不修改賣傢的代碼的情況下,給他實現房子加價、打廣告等等夾帶私貨的功能。

而Java的代理模式又分為靜態代理和動態代理

二、靜態代理

靜態代理中存在著以下的角色:

  • 抽象角色:一般使用接口或者抽象類實現(一般是真實角色和代理角色抽象出來的共同部分,比如賣房子的人和中介都有公共的方法賣房子)
  • 真實角色:被代理的角色(表示一個具體的人,比如賣房子的張三)
  • 代理角色:代理真實角色的中介,一般在代理真實角色後,會做一些附屬的操作
  • 客戶:使用代理角色來進行一些操作(買房子的)

代碼實現:

//接口(抽象角色)
public interface Singer{
	// 歌星要會唱歌
	void sing();
}

實體類男歌手

//具體角色,男歌手
public class MaleSinger implements Singer{
    private String name;
    public MaleSinger(String name) {
        this.name = name;
    }
    @Override
    public void sing() {
        System.out.println(this.name+"男歌手在唱歌");
    }
}

歌手的經紀人

//代理角色
public class Agent implements Singer{
    private MaleSinger singer; //代理角色要有一個被代理角色
    public Agent(MaleSinger singer) {
        this.singer = singer;
    }
    @Override
    public void sing() {
        System.out.println("協商出場費,做會場工作");
        //一定是被代理角色歌手去唱歌
        singer.sing();
        System.out.println("會場收拾,結算費用");
    }
}

客戶

//客戶
public class Client {
    public static void main(String[] args) {
        MaleSinger singer=new MaleSinger("周傑倫");
        Agent agent=new Agent(singer);
        agent.sing();//通過代理來運行唱歌
    }
}

可以看到抽象角色就包含瞭具體角色和代理角色公共的方法sing()。然後通過歌手的經紀人在歌手唱歌的前後可以任意增加自己想要增加的代碼。從而達到不修改歌手類方法的同時給唱歌增加新功能的目的。

說白瞭。代理就是在不修改原來的代碼的情況下,給源代碼增強功能。

小結

靜態代理模式的主要優點有:

  • 代理模式在客戶端與目標對象之間起到一個中介作用和保護目標對象的作用(經紀人保護周傑倫,外界不能直接接觸周傑倫)
  • 代理對象可以擴展目標對象的功能(本來隻能唱歌,現在又多瞭協商出場費,做會場工作等等功能)
  • 代理模式能將客戶端與目標對象分離,在一定程度上降低瞭系統的耦合度,增加瞭程序的可擴展性

其主要缺點是:

  • 代理模式會造成系統設計中類的數量增加(多瞭個代理類)
  • 在客戶端和目標對象之間增加一個代理對象,會造成請求處理速度變慢(每次都要先找中介才能找到周傑倫)
  • 增加瞭系統的復雜度(一開始隻是個獨立的流浪歌手,然後有瞭經紀人後就十分復雜瞭)

三、動態代理

靜態代理中,比如上述的例子,我們所寫的經紀人隻能服務malesinger,不能再服務其他的類型的歌手,這很不現實。因為經紀人肯定能去服務不止一種歌手,甚至可能連歌手都不是,去服務跳舞的瞭。如果靜態代理中要實現這個結果,那我們要手動編寫好多個agent類,十分繁瑣而復雜。所以就出現瞭動態代理,動態代理可以自動生成代理人的代碼。

JDK原生的動態代理

核心類:InvocationHandler類Proxy類

我們重新寫一下Singer接口,給他多一個跳舞的方法

//歌手接口
public interface Singer2 {
    void sing();
    void dance();
}

當然對應的男歌手實現類也要改變

//男歌手實現類
public class MaleSinger2 implements Singer2 {
    private String name;

    public MaleSinger2(String name) {
        this.name = name;
    }

    @Override
    public void sing() {
        System.out.println(this.name+"在唱歌");
    }

    @Override
    public void dance() {
        System.out.println(this.name+"在跳舞");
    }
}

然後我們直接進入客戶,測試。

import com.hj.Agent2;
import com.hj.MaleSinger2;
import com.hj.Singer2;

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

public class Client2 {
    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");//設置用於輸出jdk動態代理產生的類
        //簡單例子,把所有東西放到一段來解釋
        System.out.println("實例1------------------------------------------------");
        MaleSinger2 maleSinger = new MaleSinger2("周傑倫");
        //新建代理實例
        //newProxyInstance(ClassLoader loader, 類加載器,不懂的可以去看https://blog.csdn.net/Doraemon_Nobita/article/details/115702012?spm=1001.2014.3001.5501
        //Class<?>[] interfaces, 實現的接口,註意是個數組
        //InvocationHandler h 處理函數)
        Singer2 agent = (Singer2) Proxy.newProxyInstance(Client2.class.getClassLoader(),
                new Class[]{Singer2.class}, new InvocationHandler() {//匿名內部類的方式實現InvocationHandler接口,對這個看不懂的可以參考https://blog.csdn.net/Doraemon_Nobita/article/details/115506705?spm=1001.2014.3001.5501
                    @Override
                    // 這個invoke就是我們調用agent.sing()後調用的方法
                    // invoke(Object proxy, 代理對象
                    // Method method, method是方法,即我們要調用的方法(是唱歌還是跳舞,在調用的時候會是sing()還是dance())
                    // Object[] args 參數列表,可能你需要傳參)
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("協商出場費,做會場工作");
                        //關於invoke的講解,詳情可以參考:https://blog.csdn.net/Doraemon_Nobita/article/details/115702012?spm=1001.2014.3001.5501 調用指定的方法的那部分。
                        //invoke方法的參數,一個是Object類型,也就是調用該方法的對象,
                        //第二個參數是一個可變參數類型,也就是給這個方法的傳參,外層的這個已經給我們封裝成args瞭,直接用就是瞭
                        Object invoke = method.invoke(maleSinger,args);//通過反射獲取到的method名我們再invoke激活一下,傳入要調用該方法的對象。這裡用maleSinger
                        System.out.println("會場收拾,結算費用");
                        return invoke;
                    }
                });
        agent.sing();//可以調用到maleSinger的sing()
        agent.dance();//調用到maleSinger的dance()
        System.out.println("實例2------------------------------------------------");
        //這個簡單例子不行啊,我還每次必須寫死這裡是maleSinger,以後想換別的還得改這裡。動態代理豈是如此不便之物。
        //所以我們直接實現一下InvocationHandler接口,取名為Agent2
        MaleSinger2 JayZ=new MaleSinger2("周傑倫");
        MaleSinger2 JJ =new MaleSinger2("林俊傑");
        Singer2 agentJJ=(Singer2) Proxy.newProxyInstance(Client2.class.getClassLoader(),
                new Class[]{Singer2.class}, new Agent2(JJ));
        Singer2 agentJayZ=(Singer2) Proxy.newProxyInstance(Client2.class.getClassLoader(),
                new Class[]{Singer2.class}, new Agent2(JayZ));
        //可以看到現在代理人創建就十分方便瞭
        agentJJ.dance();
        agentJJ.sing();
        agentJayZ.sing();
        agentJayZ.dance();
    }
}

在第一個例子中,可以看到我們需要利用Proxy類的newProxyInstance()方法就可以生成一個代理對象。而newProxyInstance()的參數又有類加載器、實現的接口數組、以及InvocationHandler對象。在這裡使用匿名內部類來實現InvocationHandler接口。實現該接口需要實現他的invoke方法,這個方法就是我們代理對象調用原方法的時候會使用到的方法。區別於反射中的invoke方法,它有三個參數分別是代理對象,調用的方法,方法的參數數組。這裡代理對象我們不管,調用的方法則是通過反射獲取到的我們使用該代理調用sing()方法或者dance()方法的方法名。通過反射中的invoke方法,可以運行這個指定的對象裡方法名的方法。

而第二個例子中,為瞭實現可以代理任何類,我們實現InvocationHandler接口,並把取類名為Agent2。下面是Agent2的代碼。

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

public class Agent2 implements InvocationHandler {
    private Object object;//想代理誰都可以,隨便啊

    public Agent2(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("協商出場費,做會場工作");
        //一定是歌手去唱歌
        Object invoke = method.invoke(object,args);
        System.out.println("會場收拾,結算費用");
        return invoke;
    }
}

可以看出,這裡和第一個例子的實現是差不多的,隻不過我們使用Object類來代替瞭之前的寫死的MaleSinger類,這樣我們就可以代理任何的類型瞭,隻要這個類型需要我們在前後加”協商出場費,做會場工作”、“會場收拾,結算費用”。那可以看到第二個例子中,林俊傑和周傑倫的代理人可以很方便地創建出來,哪怕後面再實現瞭一個FemaleSinger類,也可以直接生成他的代理人。

加瞭

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");//設置用於輸出jdk動態代理產生的類這句代碼以後,我們就可以在項目中找到JDK自動生成的代理類代碼:

在這裡插入圖片描述

打開可以看到就是自動生成的一段幫我們寫代理的方法。

在這裡插入圖片描述

可以看到就是調用瞭h.invoke,這個h就是我們傳參為InvocationHandler的對象,調用瞭我們自己寫的invoke方法。

cglib動態代理

我們需要在maven配置文件中導入相應的包。在pom.xml文件裡增加如下代碼:

<dependencies>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
</dependencies>

使用方法和JDK的動態代理類似,隻是我們不需要再實現接口瞭,定義一個普通類CglibMaleSinger.java

public class CglibMaleSinger {
    public CglibMaleSinger(String name) {
        this.name = name;
    }

    private String name;

    public CglibMaleSinger() {//註意這裡一定要有無參構造器,不然之後會報錯Superclass has no null constructors but no arguments were given
    }

    public void sing(){
        System.out.println(this.name+"要去唱歌瞭");
    }
    public void dance(){
        System.out.println(this.name+"要去跳舞瞭");
    }
}

然後直接在客戶端測試:

import com.hj.CglibMaleSinger;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibClient {
    public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"./class");//用於輸出生成的代理class文件,"./class"表示存儲在class文件夾中
        CglibMaleSinger JayZ=new CglibMaleSinger("周傑倫");
        Enhancer enhancer = new Enhancer();//定義一個增強器enhancer
        enhancer.setSuperclass(CglibMaleSinger.class);//設置其超類,我們要代理哪個類就傳哪個類
        //MethodInterceptor是攔截器,就是把我的方法攔截住然後再去增強
        enhancer.setCallback(new MethodInterceptor() {//設置方法攔截器
            // o 是指被增強的對象,指自己
            // method是攔截後的方法,把父類的方法攔截,增強後寫在瞭子類裡
            // objs 參數
            // methodProxy 父類的方法(攔截前的方法對象)
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("談出場費");
                method.invoke(JayZ,objects);
                System.out.println("出場費談完瞭");
                return null;
            }
        });
        CglibMaleSinger cglibMaleSinger = (CglibMaleSinger)enhancer.create();
        cglibMaleSinger.sing();
        cglibMaleSinger.dance();
    }
}

和JDK的動態代理使用方法基本一致,隻是invoke方法變成瞭intercept方法而已。

加上System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"存儲路徑");語句後,在你自己設置的存儲路徑下會出現一個包含生成的class文件的文件夾。

在這裡插入圖片描述

點開hj文件夾下的.class文件

在這裡插入圖片描述

可以看到是繼承瞭我們的CglibMaleSinger類,並且重寫瞭我們的方法,重寫內容中調用瞭intercept()方法。

在這裡插入圖片描述

小結

Java動態代理隻能夠對接口進行代理,不能對普通類進行代理(因為所有生成的代理類的父類為Proxy,java不支持多重繼承)CGLIB可以代理普通類Java動態代理使用Java原生的反射API進行操作,在生成類上比較高效;而CGLIB使用ASM框架直接對字節碼(.class)改瞭,所以運行的時候是要比Java原生的效率要高些。

到此這篇關於Java設計模式之代理模式詳解的文章就介紹到這瞭,更多相關Java代理模式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: