代理模式

GitHub代碼

遠程代理

場景描述:我們想要一臺遠程監視器!

我們可能需要引入一點新概念了,比如遠程代理。

遠程代理就好比“遠程對象的本地代表”。何謂有“遠程對象”?這是一種對象,活在不同的Java虛擬機(JVM)堆中(更一般的說法爲,在不同的地址空間運行的遠程對象)。何謂“本地代表”?這是一種可以由本地方法調用的對象,其行爲會轉發到遠程對象中。

客戶對象所做的就像是在做遠程方法的調用,但其實只是調用本地堆中的“代理”對象上的方法,再由代理處理所有網絡通信的底層細節。

客戶輔助對象不是真正的遠程服務,它並不擁有方法邏輯。客戶輔助對象會聯繫服務器,傳送方法調用信息(如方法名稱、變量等),然後等待服務器返回。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
RMI將客戶輔助對象稱爲stub(樁),服務輔助對象稱爲skeleton(骨架)。

我們試着實現一個使用rmi的遠程方法調用

public interface MyRemote extends Remote {
    public String sayHello() throws RemoteException;
}
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    public String sayHello(){
        return "Sever says, 'Hey'";
    }

    public MyRemoteImpl() throws RemoteException{}

    public static void main(String[] args) {
        try{
            MyRemote service = new MyRemoteImpl();
            Naming.rebind("RemoteHello", service);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
public class MyRemoteClient {
    public static void main(String[] args) {
        new MyRemoteClient().go();
    }

    public void go(){
        try {
            MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
            String s = service.sayHello();
            System.out.println(s);
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

寫完代碼後,我們需要執行rmic命令來生成相應的stub文件。
首先需要將jdk/binjre/bin加入path路徑中,使rmic命令可以執行;
接着在MyRemoteImpl上層包(我的爲remote_proxy)執行命令rmic remote_proxy.MyRemoteImpl,就會生成對應的MyRemoteImpl_Stub.class文件;
然後啓動rmiregistry,之後分別啓動MyRemoteImpl和MyRemoteClient。
在這裏插入圖片描述
對於RMI,程序員最常犯的三個錯誤是:
1). 忘了在啓動遠程服務之前先啓動rmiregistry(要用Naming.rebind()註冊服務,rmiregistry必須是運行的)。
2). 忘了讓變量的返回值的類型成爲可序列化的類型(這種錯誤無法在編譯期發現,只會在運行時發現)。
3). 忘了給客戶提供stub類。

如果在遠程傳輸時有不想傳輸的字段,使用transient關鍵字,可以告訴JVM不序列化相應的字段。

遠程代理是一般代理模式的一種實現,這個模式的變體相當多。

代理模式爲另一個對象提供一個替身或佔位符以控制對這個對象的訪問。

使用代理模式創建代表(representative)對象,讓代表對象控制某對象的訪問,被代理的對象可以是遠程的對象、創建開銷大的對象或需要安全控制的對象。

遠程代理控制訪問遠程對象。
虛擬代理控制訪問創建開銷大的資源。
保護代理基於權限控制對資源的訪問。

遠程代理的類圖如下
在這裏插入圖片描述

虛擬代理

虛擬代理作爲創建開銷大的對象的代表。虛擬代理經常直到我們真正需要一個對象的時候才創建它。當對象在創建前和創建中時,由虛擬代理來扮演對象的替身。對象創建後,代理就會將請求直接委託給對象。

我們寫一個使用虛擬代理的例子,它需要加載開銷大的對象(通過網絡加載圖片)。

public class ImageProxy implements Icon {
    ImageIcon imageIcon;
    URL imageURL;
    Thread retrievalThread;
    boolean retrieving = false;

    public ImageProxy(URL url){
        imageURL = url;
    }

    public int getIconWidth(){
        if (imageIcon != null){
            return imageIcon.getIconWidth();
        }else {
            return 800;
        }
    }

    public int getIconHeight(){
        if (imageIcon != null){
            return imageIcon.getIconHeight();
        }else {
            return 600;
        }
    }

    public void paintIcon(final Component c, Graphics g, int x, int y){
        if (imageIcon != null){
            imageIcon.paintIcon(c, g, x, y);
        }else {
            g.drawString("Loading CD cover, please wait...", x+300, y+190);
            if (!retrieving){
                retrieving = true;
                retrievalThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            imageIcon = new ImageIcon(imageURL, "CD Cover");
                            c.repaint();
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                });
                retrievalThread.start();
            }
        }
    }
}
public class ImageComponent extends JComponent {
    private Icon icon;

    public ImageComponent(Icon icon){
        this.icon = icon;
    }

    public void setIcon(Icon icon){
        this.icon = icon;
    }

    public void paintComponent(Graphics g){
        super.paintComponent(g);
        int w = icon.getIconWidth();
        int h = icon.getIconHeight();
        int x = (800-w)/2;
        int y = (600-h)/2;
        icon.paintIcon(this, g, x, y);
    }
}
public class ImageProxyTestDrive {
    ImageComponent imageComponent;
    JFrame frame = new JFrame("CD Cover Viewer");
    JMenuBar menuBar;
    JMenu menu;
    Hashtable cds = new Hashtable();

    public static void main(String[] args) throws Exception{
        ImageProxyTestDrive testDrive = new ImageProxyTestDrive();
    }

    public ImageProxyTestDrive() throws Exception{
        cds.put("Buddha Bar", "http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
        cds.put("Ima", "http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZZ.jpg");

        URL initialURL = new URL((String) cds.get("Ima"));
        menuBar = new JMenuBar();
        menu = new JMenu("Favorite CDs");
        menuBar.add(menu);
        frame.setJMenuBar(menuBar);

        for (Enumeration e = cds.keys(); e.hasMoreElements();){
            String name = (String) e.nextElement();
            JMenuItem menuItem = new JMenuItem(name);
            menu.add(menuItem);
            menuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    imageComponent.setIcon(new ImageProxy(getCDUrl(e.getActionCommand())));
                    frame.repaint();
                }
            });
        }

        // 建立框架和菜單
        Icon icon = new ImageProxy(initialURL);
        imageComponent = new ImageComponent(icon);
        frame.getContentPane().add(imageComponent);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

    URL getCDUrl(String name){
        try {
            return new URL((String) cds.get(name));
        }catch (MalformedURLException e){
            e.printStackTrace();
            return null;
        }
    }
}

測試一下
在這裏插入圖片描述
在這裏插入圖片描述
在真實的世界中,代理模式有許多變體,這些變體都有共通點:都會將客戶對主題(Subject)施加的方法調用攔截下來。

動態代理

接下來我們再看另一個常用變體,保護代理。
什麼是保護代理?這是一種根據訪問權限決定客戶可否訪問對象的代理。
而我們將使用動態代理來實現保護代理。

動態代理的類圖如下
在這裏插入圖片描述
因爲Java已經爲你創建了Proxy類,所以你需要有辦法來告訴Proxy類你要做什麼。你不能像以前一樣把代碼放在Proxy類中,因爲Proxy不是你直接實現的。既然這樣的代碼不能放在Proxy類中,那麼要放在哪裏?放在InvocationHandler中。InvocationHandler的工作是響應代理的任何調用。你可以把InvocationHandler想成是代理收到方法調用後,請求做實際工作的對象。

接着我們寫一個動態代理的例子來看一下

public interface PersonBean {
    String getName();
    String getGender();
    String getInterests();
    int getHotOrNotRating();

    void setName(String name);
    void setGender(String gender);
    void setInterests(String interests);
    void setHotOrNotRating(int rating);
}
public class PersonBeanImpl implements PersonBean {
    String name;
    String gender;
    String interests;
    int rating;
    int ratingCount = 0;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getGender() {
        return gender;
    }

    @Override
    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String getInterests() {
        return interests;
    }

    @Override
    public void setInterests(String interests) {
        this.interests = interests;
    }

    @Override
    public int getHotOrNotRating() {
        if (ratingCount == 0) return 0;
        return (rating/ratingCount);
    }

    @Override
    public void setHotOrNotRating(int rating) {
        this.rating += rating;
        ratingCount++;
    }
}

接着就是動態代理的關鍵,我們需要實現相應的InvocationHandler

public class OwnerInvocationHandler implements InvocationHandler {
    PersonBean person;

    public OwnerInvocationHandler(PersonBean person){
        this.person = person;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException{
        try {
            if (method.getName().startsWith("get")){
                return method.invoke(person, args);
            }else if (method.getName().equals("setHotOrNotRating")){
                throw new IllegalAccessException();
            }else if (method.getName().startsWith("set")){
                return method.invoke(person, args);
            }
        }catch (InvocationTargetException e){
            e.printStackTrace();
        }
        return null;
    }
}
public class NotOwnerInvocationHandler implements InvocationHandler {
    PersonBean person;

    public NotOwnerInvocationHandler(PersonBean person){
        this.person = person;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException{
        try {
            if (method.getName().startsWith("get")){
                return method.invoke(person, args);
            }else if (method.getName().equals("setHotOrNotRating")){
                return method.invoke(person, args);
            }else if (method.getName().startsWith("set")){
                throw new IllegalAccessException();

            }
        }catch (InvocationTargetException e){
            e.printStackTrace();
        }
        return null;
    }
}

最後測試一下

public class MatchMakingTestDrive {

    public static void main(String[] args) {
        MatchMakingTestDrive test = new MatchMakingTestDrive();
        test.drive();
    }

    public MatchMakingTestDrive(){
    }

    public void drive(){
        PersonBean joe = getPersonFromDatabase("Joe JavaBean");
        PersonBean ownerProxy = getOwnerProxy(joe);
        System.out.println("Name is " + ownerProxy.getName());
        ownerProxy.setInterests("bowling, Go");
        System.out.println("Interests set from owner proxy");
        try {
            ownerProxy.setHotOrNotRating(10);
        }catch (Exception e){
            System.out.println("Can't set rating from owner proxy");
        }
        System.out.println("Rating is " + ownerProxy.getHotOrNotRating());

        System.out.println("\n------------\n");

        PersonBean nonOwnerProxy = getNonOwnerProxy(joe);
        System.out.println("Name is " + nonOwnerProxy.getName());
        try {
            nonOwnerProxy.setInterests("what the fuck!");
        }catch (Exception e){
            System.out.println("Can't set interests from non owner proxy");
        }
        nonOwnerProxy.setHotOrNotRating(3);
        System.out.println("Rating set from non owner proxy");
        System.out.println("Rating is " + nonOwnerProxy.getHotOrNotRating());
    }

    PersonBean getOwnerProxy(PersonBean person){
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person)
        );
    }

    PersonBean getNonOwnerProxy(PersonBean person){
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new NotOwnerInvocationHandler(person)
        );
    }

    PersonBean getPersonFromDatabase(String name){
        PersonBean person = new PersonBeanImpl();
        person.setName(name);
        person.setHotOrNotRating(7);

        return person;
    }

}

結果
在這裏插入圖片描述
動態代理之所以被稱爲動態,是因爲運行時纔將它的類創建出來。代碼開始執行時,還沒有proxy類,它是根據需要從你傳入的接口集創建的。而對於傳入newProxyInstance()的接口類型是有一些限制的。詳情研讀javadoc相關文件。

其他代理模式變體:

  1. 防火牆代理(Firewall Proxy);控制網絡資源的訪問,保護主題免於“壞客戶”的侵害。
  2. 智能引用代理(Smart Reference Proxy);當主題被引用時,進行額外的動作,例如計算一個對象被引用的次數。
  3. 緩存代理(Caching Proxy);爲開銷大的運算結果提供暫時存儲:它也允許多個客戶共享結果,以減少計算或網絡延遲。
  4. 同步代理(Synchronization Proxy);在多線程的情況下爲主題提供安全的訪問。
  5. 複雜隱藏代理(Complexity Hiding Proxy);用來隱藏一個類的複雜集合的複雜度,並進行訪問控制。有時候也稱爲外觀代理(Facade Proxy),這不難理解。複雜隱藏代理和外觀模式是不一樣的,因爲代理控制訪問,而外觀模式只提供另一組接口。
  6. 寫入時複製代理(Copy-On-Write Proxy);用來控制對象的複製,方法是延遲對象的複製,直到客戶真的需要爲止。這是虛擬代理的變體。

代理在結構上類似裝飾者,但是目的不同。裝飾者模式爲對象加上行爲,而代理則是控制訪問。

總結:關於代理模式的應用,我們主要介紹了三種變體,遠程代理,虛擬代理和動態代理;他們的應用場景分別爲:遠程代理在想要像本地方法調用那樣進行遠程方法調用時使用,經常在分佈式的實現中看到它的身影;虛擬代理在加載大資源對象時,用代理做預先處理,我們經常看到的網頁或者視頻資源加載等,就是應用了這種技術;動態代理涉及反射,主要是動態生成對象,並通過權限控制方法時使用。關於代理還有靜態代理,動態代理和cglib代理,感興趣的可以查閱相關文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章