设计模式 with Python 11:代理模式

83 篇文章 10 订阅

设计模式 with Python 11:代理模式

对于现代人来说,代理模式非常好理解,毕竟天朝网民不知道网络代理的估计是少数,事实上《Head First 设计模式》中这一章节的大部分内容都在介绍一些代理模式的应用和变种,代理模式本身的描述篇幅相对不多。

这里我会遵循原书的结构,并且因为原书介绍的相关代理模式应用都是Java语言直接相关的例子,并不能用Python示例代替,所以这里也全部采用原书的Java示例进行说明。

就像前面所说的,代理模式的概念并不复杂,所以一开始我就阐述一下代理模式的概念。

代理模式

简单地说,代理模式就是真正的服务对象提供一种替代品,让这个替代品来代替真是的对象处理所有的客户端请求。

我们之前所提到的网络代理就是这种东西,通过在电脑或者浏览器上设置网络代理,原本直接对目标web服务器的http请求就会转发给代理服务器,经由代理服务器转发,而web服务器的返回信息也是经由代理服务器转发回我们的计算机,这就是一个经典的代理案例。

当然,除了网络代理,代理模式可以实现多种用途,但其核心都是对真正的服务对象的一种“代理”,即对客户端和真正服务对象进行解耦,在中间起一个桥梁和访问控制的作用。而通过不同的控制实现细节,就可以实现不同用途的代理模式,这点在之后我们会用大量篇幅举例说明。

image-20210709154835395

代理模式的UML很简单,因为代理类Proxy必须要能代替真正的服务RealSubject对外提供服务,所以两者必然具有相同的接口Subject。原本客户端程序Client需要直接调用RealSubject,但现在所有对RealSubject的调用都通过Proxy,因为现在Proxy代理了所有对RealSubject的请求。

此外,一般来说代理类都会持有一个其代理的目标对象的引用,就像UML中的Proxy持有的subject,这是为了在必要时候直接调用目标对象的相应方法,甚至在有些情况下目标对象也是由代理类直接创建的,这点在之后会看到相关示例。

下面我们以Java中的远程代理为例说明代理模式的应用。

远程代理

Java中的远程代理是RMI机制(Remote Mehod Invocation 远程方法调用)的一部分,即通过远程代理,我们可以像使用本地JVM中的对象那样使用远程服务器上的JVM的对象。

其UML表示如下:

image-20210709212713532

这其中RealSubjectStub(目标桩)和RealSubjectSkeleton(目标骨架)是通过RMI组件自动生成的,不需要用户编写。目标桩除了可以在本地作为目标服务的代理,还肩负着连接本地和远程服务器的用途,因为客户端程序对目标桩的所有调用,都必须通过网络连接传递到服务器,然后由服务器上的RealSubject进行响应,然后再将结果传回。

需要说明的是,RealSubjectStub是和Client一起部署在客户端电脑的,而RealSubjectSkeletonRealSubject是部署在服务端的(事实上服务端也会部署一份RealSubjectStub,用于注册到远程对象列表,这点之后会说明)。

这就涉及到将Java对象通过网络传递的问题。我们知道,Java中是可以将对象序列化后存储或者通过网络传递的,所以理论上我们是可以编写服务端和客户端程序来传递可序列化的Java对象的,而这里的“桩”和“骨架”就是起到这样的作用,它们可以“自动”地序列化和反序列化地传递对象作为调用参数或者返回值。

这其中地调用步骤是这样的:

  1. Client调用RealSubjectStub的某个方法,RealSubjectStub序列化该方法的参数后将调用的方法名和参数通过网络传递给RealSubjectSkeleton
  2. RealSubjectSkeleton将接收到的参数进行反序列化,然后调用RealSubject的相应方法并传入参数。
  3. RealSubject方法响应后将返回值返回给RealSubjectSkeletonRealSubjectSkeleton进行序列化,并通过网络传递给RealSubjectStub
  4. RealSubjectStub接收到RealSubjectSkeleton通过网络传递的返回值,进行反序列化,然后传递给Client
  5. Client接收到返回值,完成对远程代理RealSubjectStub的一次调用。

下面我们看如何实现远程代理。

实现

在说明如何实现具体代码之前,有几个我遇到的坑需要说明:

  • 我本来是想使用树莓派作为服务器进行部署测试的,结果在调试时候提示“代码中包含JDK16的最新特性,无法运行”,然后我搜索了一下JDK的安装包,尝试给树莓派安装JDK16,结果发现OpenJDK16刚完成发布没多久,只提供了Linux/X64版本,而我的树莓派上装的是32位的Dibian系统,So…我尽力了。所以下面的实现和调试都是在我的笔记本上完成的,感兴趣的童鞋可以自行折腾。OpenJDK的Release页面是JDK 16.0.1 General-Availability Release
  • 因为这里需要在客户端和服务端(因为上面的原因这里都是我的笔记本)分别运行Java程序,方便起见最好单独打一个jar包用于执行,所以需要单独使用IDE创建一个项目,不再像之前的示例那样直接在IDE下进行调试。如果你和我一样使用的是VSCode的话,可以使用JAVA PROJECT工具:image-20210710133142009

下面我们来看具体如何实现。

  1. 创建Java项目并创建相关必要的包路径,代码结构可以参考我的remote_proxy2

  2. 创建一个用于约束远程服务的统一接口,这个接口必须要继承Remote接口,以表示用于RMI机制。

    我这里设计了一个用于远程切换电视节目的接口,假设目标服务器会提供一个切换电视节目的服务,切换结束后会返回Boolean告诉客户端是否成功。此外这里接口的常量RMI_NAME是用来注册远程代理时候作为名称使用的。

    package xyz.icexmoon.remote_proxy2;
    
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    public interface RaspbianTVInter extends Remote {
        public static final String RMI_NAME = "RaspbianTV";
        public Boolean chooseVhannel(String channelName) throws RemoteException;
    
    }
    
  3. 创建服务端的可以远程执行的类,这个类必须实现刚创建的接口,此外我们需要利用这个类来实现前面说的远程代理所需的桩和骨架,最方便的方式是将这个类直接继承UnicastRemoteObject,利用继承的构造函数即可动态生成对应的桩和骨架组件,因为桩和骨架会用于远程代理时的网络通信,所以这里继承的构造函数需要抛出一个RemoteException异常。

    chooseVhannel中的代码是具体的切换电视节目的逻辑。runServer负责提供给入口文件,实现将远程代理进行注册并起到一个类似web服务的作用,会等待客户端程序通过代理桩进行远程调用。在这个架构中桩和骨架都遵循同一个接口,所以这里进行注册的时候registry.rebind(RaspbianTVInter.RMI_NAME, raspbianTV);rebind的参数除了名称以外,是一个RaspbianTVInter类型的对象。

    package xyz.icexmoon.remote_proxy2.server;
    
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    import java.rmi.server.UnicastRemoteObject;
    
    import xyz.icexmoon.remote_proxy2.RaspbianTVInter;
    
    public class RaspbianTV extends UnicastRemoteObject implements RaspbianTVInter {
    
        protected RaspbianTV() throws RemoteException {
            super();
        }
    
        @Override
        public Boolean chooseVhannel(String channelName) {
            System.out.println("switch to " + channelName + "channel");
            return false;
        }
    
        public static void runServer(){
            try {
                RaspbianTVInter raspbianTV = new RaspbianTV();
                Registry registry = LocateRegistry.createRegistry(2002);
                registry.rebind(RaspbianTVInter.RMI_NAME, raspbianTV);
                System.out.println("server is ready");
            } catch (RemoteException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            RaspbianTV.runServer();
        }
    }
    
  4. 编写客户端代码,客户端代码相对简单,获取注册机后通过注册机查找远程代理,然后通过远程代理可以像使用本地代码那样进行调用。需要注意的是因为是远程调用,所以需要使用try/catch捕获相关异常。

    package xyz.icexmoon.remote_proxy2.client;
    
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    import xyz.icexmoon.remote_proxy2.RaspbianTVInter;
    
    public class Client {
        public static void runClient(String channelName, String remoteIP) {
            try {
                System.out.println("choose TV channel " + channelName + " on remote TV " + remoteIP);
                Registry registry = LocateRegistry.getRegistry(remoteIP, 2002);
                RaspbianTVInter raspbianTVInter = (RaspbianTVInter) registry.lookup(RaspbianTVInter.RMI_NAME);
                boolean result = raspbianTVInter.chooseVhannel(channelName);
                if (result) {
                    System.out.println("switch channel success");
                }
            } catch (RemoteException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NotBoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            if (args.length <= 0) {
                System.out.println("need channel name");
                return;
            }
            String channelName = args[0];
            String remoteIP = "192.168.1.1";
            Client.runClient(channelName, remoteIP);
        }
    }
    
  5. 编写项目的入口文件。为了方便起见,这里通过命令行参数来确定执行服务端程序还是客户端程序,否则需要打两个Jar包。

    package xyz.icexmoon.remote_proxy2;
    
    import xyz.icexmoon.remote_proxy2.client.Client;
    import xyz.icexmoon.remote_proxy2.server.RaspbianTV;
    
    public class Main {
        public static void main(String[] args) {
            if (args.length == 0) {
                System.out.println("Need input run mode(client/servere).");
                return;
            }
            String runMode = args[0];
            switch (runMode) {
                case "client":
                    if (args.length < 3) {
                        System.out.println("You need input 3 parameters: mode,ip,channel_name");
                        return;
                    }
                    String remoteIP = args[1];
                    String ChannelName = args[2];
                    Client.runClient(ChannelName, remoteIP);
                    break;
                case "server":
                    RaspbianTV.main(args);
                    break;
                default:
                    System.out.println("The run mode must be client or server.");
                    return;
            }
        }
    }
    
  6. 创建jar包,我这里使用的是VSC中的JAVA PROJECT工具,生成的时候指定Main.java作为入口文件即可。

整个项目的完整代码见remote_proxy2,我创建的jar包是remote_proxy2.jar,可以下载并执行我的jar包。

测试

因为之前已经说过的原因,这里在同一台电脑上测试客户端和服务端代码。

先打开一个cmd执行服务端程序。

java -jar --enable-preview remote_proxy2.jar server
  • 不知道什么原因,我使用power shell执行java时候打印的报错信息都是乱码,但是本身打印其它UTF-8编写的程序输出的中文都是没问题的,只能猜测java抽风使用的是非UTF-8中文编码,弄了半天没弄好我放弃了,改用cmd。
  • 添加参数--enable-preview可以输出相关提示,如果程序有错误的话。
  • 如果出现(class file version 60.65535) was compiled with preview features的字样,大概率是你的Java版本过低,请安装OpenJDK16,或者使用自己当前的JDK尝试重写我的整个示例代码,以避开某些JDK16的新特性(我现在也没搞清楚是哪些,我只是个Java门外汉)。

如果一切正常,会显示server is ready的字样,并且不会有光标等待输入,这表明服务进程已经正常执行了。

现在可以再打开一个cmd执行客户端程序了。

java -jar --enable-preview remote_proxy2.jar client 127.0.0.1 CCTV10

需要注意的事项和服务端程序是一样的,如果执行成功会显示choose TV channel CCTV10 on remote TV 127.0.0.1

并且此时切换到服务端进程所在的cmd会发现出现switch to CCTV10channel的信息,表示客户端确实调用了服务端的相关代码。

好了,远程代理的介绍告一段落,对RMI机制有兴趣的可以自行前往Java官方文档进行学习,我们下面介绍另一种代理:虚拟代理。

《Head First 设计模式》这本书的确挺老的,对于出现类似的问题我有所预料,但是没想到的确挺折腾人的,希望下面的部分不要太过折腾吧。

虚拟代理

虚拟代理的概念本身也很好理解,就是其会充当一个需要消耗一定资源才能创建的实体的临时替身,即在真正的服务实体创建完成前,会由虚拟代理来承担其所有的服务工作,在这期间所有对实体的请求都会由代理来响应,并且会“假装”自己就是目标实体,而真正的服务实体一旦完成创建,虚拟代理就会将相关响应委托给实体,只起一个“桥接”的作用,“不再参与”响应。这种代理的特点是,被委托的响应实体一般是由委托类所创建的。

下面我们看一个具体示例。

我们的目标程序是一个使用swing框架的图形化界面,长这样:

image-20210710153141540

最上方是一个菜单栏(JMenuBar),菜单栏包含一个菜单(JMenu)images,images下包含5个菜单项(JMenuItem),每个菜单项都有自己的名字,比如image1之类的,点击每个菜单项都会将下方的图片控件(ImageComponent)中的图片替换为相应的图片。

如果不使用虚拟代理,可能每次加载图片前我们都需要将图片控件先设置为默认图片,然后尝试从网络下载图片,如果获取到图片后再替换图片,但如果我们使用了虚拟代理,这一切都可以交给代理去处理,在没有获取到图片的时候,代理会告诉图片控件如何显示一个等待内容,一旦图片获取到了,代理就会让控件重新绘制,以展示完整的图片内容。

下面我们看着是如何做到的:

先创建一个图片控件,这是为了方便在JFrame中管理图片。

package xyz.icexmoon.virtual_proxy;

import java.awt.Graphics;

import javax.swing.Icon;
import javax.swing.JComponent;

public class ImageComponent extends JComponent {
    private Icon icon;

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

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

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        int width = icon.getIconWidth();
        int height = icon.getIconHeight();
        int x = (ImageProxy.DEFAULT_WIDTH - width) / 2;
        int y = (ImageProxy.DEFAULT_HEIGHT - height) / 2;
        icon.paintIcon(this, g, x, y);
    }

}

创建图片的虚拟代理,因为虚拟代理要表现的和图片一样,所以很自然地实现了Icon接口:

package xyz.icexmoon.virtual_proxy;

import java.awt.Component;
import java.awt.Graphics;
import java.net.URL;

import javax.swing.Icon;
import javax.swing.ImageIcon;

public class ImageProxy implements Icon {
    private ImageIcon imageIcon;
    private boolean initImage = false;
    private URL imageURL;
    public static final int DEFAULT_WIDTH = 800;
    public static final int DEFAULT_HEIGHT = 600;

    public ImageProxy(URL imageURL) {
        this.imageURL = imageURL;
    }

    @Override
    public int getIconHeight() {
        if (imageIcon == null) {
            return DEFAULT_HEIGHT;
        } else {
            return imageIcon.getIconHeight();
        }
    }

    @Override
    public int getIconWidth() {
        if (imageIcon == null) {
            return DEFAULT_WIDTH;
        } else {
            return imageIcon.getIconWidth();
        }
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        if (imageIcon != null) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("Loading image, please wait...", x + 300, y + 190);
            if (!initImage) {
                initImage = true;
                Thread initImageThread = new Thread(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            imageIcon = new ImageIcon(imageURL, "image");
                            c.repaint();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

                });
                initImageThread.start();
            }
        }
    }

}

虚拟代理相当简单,如果没有加载好图片,就显示一段文字,如果加载好了,就显示图片。

需要注意的是,加载图片需要占用一段时间,所以需要新开一个线程执行,以避免堵塞UI线程。

UI入口文件:

package xyz.icexmoon.virtual_proxy;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;

public class Main {
    private JFrame frame;
    private ImageComponent imageComponent;
    public static void main(String[] args) {
        Main main = new Main();
        main.initJFrame();
    }

    private void initJFrame(){
        Hashtable<String,String> urls = new Hashtable<String,String>();
        urls.put("image1", "https://icexmoon-public-1305291391.cos.ap-shanghai.myqcloud.com/image/5fa398772c60be4020d1436289df4f91f1dd7d58_5fa398774f632bb5ae534ec09829cb6c0e631f47.jpg");
        urls.put("image2", "https://icexmoon-public-1305291391.cos.ap-shanghai.myqcloud.com/image/5fb0ceb56b992a683cb84457b0261a2d5f2cbf73_5fb0ceb5210ca7cc093a4f9ba36fd5c4ce63f688.jpg");
        urls.put("image3", "https://icexmoon-public-1305291391.cos.ap-shanghai.myqcloud.com/image/5fb2655575eaa787d2d443fb991df7ba62f4f1a7_5fb2655561092198c80549bf9b7f493bc9efd81e.jpg");
        urls.put("image4", "https://icexmoon-public-1305291391.cos.ap-shanghai.myqcloud.com/image/5fb27ed1a64404709dda47d38357afbfd786589f_5fb27ed1429793c8cdf64720819132e873b84396.jpg");
        urls.put("image5", "https://icexmoon-public-1305291391.cos.ap-shanghai.myqcloud.com/image/5fb3cea357bd79a6e33e4f429aa35ef0c6843fe4_5fb3cea3d7b9059638444161a5a847480bddbd6f.jpg");
        frame = new JFrame("Web Image Viewer");
        JMenuBar menubar = new JMenuBar();
        JMenu menu = new JMenu("images");
        menubar.add(menu);
        frame.setJMenuBar(menubar);
        URL defaultImageURL = null;
        try {
            defaultImageURL = new URL(urls.get("image1"));
        } catch (MalformedURLException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        imageComponent = new ImageComponent(new ImageProxy(defaultImageURL));
        frame.getContentPane().add(imageComponent);
        for (Enumeration<String> e = urls.keys();e.hasMoreElements();) {
            String name = e.nextElement();
            URL url = getURLbyName(urls, name);
            JMenuItem menuItem = new JMenuItem(name);
            menu.add(menuItem);
            menuItem.addActionListener(new ActionListener(){
                
                @Override
                public void actionPerformed(ActionEvent e) {
                    imageComponent.setIcon(new ImageProxy(url));
                    frame.repaint();
                }
                
            });
        }
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(ImageProxy.DEFAULT_WIDTH, ImageProxy.DEFAULT_HEIGHT);
        frame.setVisible(true);
    }

    public static URL getURLbyName(Hashtable<String, String> urls, String name) {
        URL url = null;
        try {
            url = new URL(urls.get(name));
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return url;
    }
}

代码很多,但大部分都是组织菜单和UI控件的代码,关键的代码在监听器中使用imageComponent.setIcon(new ImageProxy(url));来实现图片切换,我们只需要像使用普通的Icon那样使用虚拟代理ImageProxy即可。

完整代码见virtual_proxy

保护代理

保护代理的用途是对已有的类型进行访问控制,通过使用这个代理,我们可以实现特定的访问控制逻辑。

这有点像Python中的属性描述符,或者说属性描述符本身就是一种保护代理。

关于Python中的属性描述符可以阅读Python学习笔记37:属性描述符

Java中保护代理的UML结构是这样的:

image-20210710164910602

其中Proxy是在Java运行时使用Java组件进行动态创建的,它的对外表现会和接口Subject一致。内建接口InvocationHandler起到一个钩子的用途,即自动创建的Proxy只能“呆板”地“扮演”Subject,并不知道如何处理具体的相关调用,这时候我们就需要创建一个类,并实现InvocationHandler接口,去处理真正的代理逻辑,自然地,为了代理RealSubject,需要持有一个RealSubject引用。

下面我们看具体实现,这里使用一个社交系统作为示例,假设这个系统中的个人资料具有下面的接口:

package xyz.icexmoon.protect_proxy;

public interface PersonBean {
    public int getRating();
    public String getName();
    public String getGender();

    public void setName(String name);
    public void setGender(String gender);
    public void setRating(int rating);
}

这些方法包括:

  • getRating,获取评价
  • getName,获取姓名
  • getGender,获取性别
  • setName,设置姓名
  • setGender,设置性别
  • setRating,设置评分

具体类的实现为:

package xyz.icexmoon.protect_proxy;

public class PersonBeanImpl implements PersonBean {
    private String name;
    private String gender;
    private int rating = 0;
    private int ratingCount = 0;

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

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

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

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

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

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

}

这里有一个隐含逻辑,即在我们的系统中,自己应当是不能给自己打分的,其它操作都可以。别人可以给自己打分,但别人不能修改自己的个人资料。

当然这种逻辑有很多方式可以实现,并且一般都不会用到保护代理,但我们这里作为展示,使用保护代理来实现这种访问控制逻辑。

我们这里需要两个保护代理,一个用于自己访问自己,另一个用于别人访问自己。

我们之前说过了,保护代理主要使用Java的标准组件创建,而我们的任务主要是创建InvocationHandler的实现类:

package xyz.icexmoon.protect_proxy;

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

public class OwnInvocationHander implements InvocationHandler{
    private PersonBean personBean;
    

    public OwnInvocationHander(PersonBean personBean) {
        this.personBean = personBean;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("setRating")){
            throw new IllegalAccessError();
        }
        else{
            return method.invoke(personBean, args);
        }
    }
    
}
package xyz.icexmoon.protect_proxy;

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

public class OtherInvocationHander implements InvocationHandler {
    private PersonBean personBean;

    public OtherInvocationHander(PersonBean personBean) {
        this.personBean = personBean;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().equals("setRating")){
            return method.invoke(personBean, args);
        }
        else if(method.getName().startsWith("get")){
            return method.invoke(personBean, args);
        }
        else{
            throw new IllegalAccessError();
        }
    }

}

InvocationHandler的实现类并不困难,invoke方法中的method对象表示代理被调用的方法,而我们可以通过这个方法的方法名等属性进行区分处理,完成保护控制逻辑。对于不希望的请求我们可以使用throw new IllegalAccessError()抛出异常,对于允许的访问,我们可以使用method.invoke(personBean, args)的方式转发给被代理的真正处理响应的对象。可以看到,这种将方法作为参数传入并进行处理的逻辑颇有Python的风格。事实上保护代理使用的都是Java中的反射模块(reflect)。

在使用保护代理的时候我们需要动态创建代理,为了方便起见这里创建一个代理的简单工厂:

package xyz.icexmoon.protect_proxy;

import java.lang.reflect.Proxy;

public class PersonProxyFactory {
    public static PersonBean getOwnPersonProxy(PersonBean personBean) {
        ClassLoader loader = personBean.getClass().getClassLoader();
        Class<?>[] interfaces = personBean.getClass().getInterfaces();
        return (PersonBean) Proxy.newProxyInstance(loader, interfaces, new OwnInvocationHander(personBean));
    }

    public static PersonBean getOtherPersonProxy(PersonBean personBean){
        ClassLoader loader = personBean.getClass().getClassLoader();
        Class<?>[] interfaces = personBean.getClass().getInterfaces();
        return (PersonBean) Proxy.newProxyInstance(loader, interfaces, new OtherInvocationHander(personBean));
    }
}

最后测试一下:

import xyz.icexmoon.protect_proxy.PersonBean;
import xyz.icexmoon.protect_proxy.PersonBeanImpl;
import xyz.icexmoon.protect_proxy.PersonProxyFactory;

public class App {
    public static void main(String[] args) throws Exception {
        System.out.println("Hello, World!");
        PersonBeanImpl Lilei = new PersonBeanImpl();
        PersonBean LileiSelf = PersonProxyFactory.getOwnPersonProxy(Lilei);
        LileiSelf.setName("Li lei");
        LileiSelf.setGender("female");
        System.out.println(LileiSelf.getName());
        System.out.println(LileiSelf.getGender());
        // LileiSelf.setRating(10);
        PersonBean LileiOther = PersonProxyFactory.getOtherPersonProxy(Lilei);
        System.out.println(LileiOther.getName());
        System.out.println(LileiOther.getGender());
        LileiOther.setRating(10);
        System.out.println(LileiOther.getRating());
        System.out.println(LileiSelf.getRating());
        // LileiOther.setName("HanMeimei");
    }
}

完整代码见protect_proxy

注释部分的代码会触发异常,这其中LileiSelf用于李磊自己对自己的请求,LileiOther用于别人对李磊的请求。

最后再次强调,在这个示例中,使用保护代理难以编写和理解,仅用于演示,类似的功能有很多更容易实现和理解的方式。

好了,虽然代理还有很多类型,但精力有限,就介绍到这里了,谢谢阅读。

参考资料:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值