Head First 代理模式

21 阅读12分钟

一、定义

代理模式:为另一个对象提供替身或占位符以控制对这个对象的访问。我们也将代理描述成另一个对象的“代表”。
实例代理模式创建“代表对象”,让“代表对象”控制某对象的访问,被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。
代理模式的变体有很多,下面会提到主要的三种变体:远程代理、虚拟代理、动态代理。

  • 远程代理控制访问远程对象。
  • 虚拟代理控制访问创建开销大的资源。
  • 动态代理基于权限控制对资源的访问。

下面我们来看看类图

proxy.png

  • ISubject:它为RealSubject和Proxy提供了接口,通过实现同一接口,Proxy在RealSubject出现的地方取代它。
  • RealSubject是真正做事的对象,它是被Proxy代理和控制访问的对象。
  • Proxy持有RealSubject的引用。在某些例子中,Proxy还会负责RealSubject对象的创建和销毁。客户和RealSubject的交互都必须通过Proxy。因为Proxy和RealSubject实现了相同的接口(ISubject),所以任何用到RealSubject的地方,都可以用Proxy取代。Proxy也控制了对RealSubject的访问,在某些情况下,我们可能需要这样的控制。

1、远程代理

1.1 定义

远程代理可以作为远程对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。
何为远程对象?这是一种对象,活在不同的JVM堆中(更一般的说法为:在不同的地址空间运行的远程对象)。
何为本地代表?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。

remote.png

但是要如何创建一个代理,知道如何调用在另一个JVM中的对象的方法?Java RMI(RemoteMethodInvocation)远程方法调用可以做到。

1.2 下面来看看RMI的工作过程要点

Rmi.png

  1. 客户对象Client调用客户辅助对象ClientHelper的doBigThing()方法。
  2. 客户辅助对象打包调用信息(变量、方法名称等),然后通过网络将它运给服务辅助对象ServiceHelper。
  3. 服务辅助对象把来自客户辅助对象的信息解包,找出被调用的方法(以及在哪个对象内),然后调用真正的服务对象RealSubject上的真正方法。
  4. 服务对象上的方法被调用,将结果返回给服务辅助对象。
  5. 服务辅助对象把调用的返回信息打包,然后通过网络运回给客户辅助对象。
  6. 客户辅助对象把返回值解包,返回给客户对象。对于客户来说,这是完全透明的。

随着 RMI 技术的演进和现代分布式计算框架的普及,Java 平台对 RMI 的支持进行了精简。Java 21 正式移除了 rmic 工具,这意味着开发者不能再使用它来生成 RMI 所需的客户端存根代码。
这一变化是 Java 21 中一系列 RMI 相关移除和弃用操作的一部分,旨在减少对过时、使用率低且存在潜在安全风险的组件的依赖。官方建议开发者迁移到更现代、更安全的分布式通信框架,如 gRPC 或 RESTful API。

2、虚拟代理

2.1 定义

虚拟代理作为创建开销大的对象的代表。虚拟代理经常知道我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。

virtual.png

下面是一个例子:显示CD封面

从在线服务中获取封面的图片,但是限于连接带宽和网络负载,下载可能需要一些时间,所以在等待图像加载的时候,应该显示一些东西。我们也不希望在等待图像时整个应用程序被挂起。一旦图像加载完成,刚才显示的东西应该消失,图像显示出来。

想做到这样,简单的方式就是虚拟代理。虚拟代理可以代理Icon,管理背景的加载,并在加载未完成时显示“CD封面加载中,请稍候。。。”,一旦加载完成,代理就把显示的职责委托给Icon。

image.png image.png

下面让我们看看代码

import javax.swing.*;
import java.awt.*;
import java.net.URL;

/**
 * ImageProxy同ImageIcon一样,都实现了Icon接口
 * ImageProxy就是一个虚拟代理
 */
public class ImageProxy implements Icon {

    // 此ImageIcon是我们希望在加载后显示出来的真正图像
    private volatile ImageIcon imageIcon;
    // 图像Url
    final URL imageUrl;
    Thread retrievalThread;
    boolean retrieving = false;

    // 将图像的URL传入构造器中。这是我们希望显示的图像所在的位置
    public ImageProxy(URL url) {
        this.imageUrl = url;
    }

    // ImageIcon被两个不同的线程使用,因此,除了使变量volatile(保护读取)外,我们还使用synchronized(保护写入)。
    synchronized void setImageIcon(ImageIcon imageIcon) {
        this.imageIcon = imageIcon;
    }

    /**
     * 在屏幕上画出一个Icon图像(通过委托给ImageIcon)
     * 如果ImageIcon没有被完整创建出来,那就由ImageProxy来创建一个
     * @param c  a {@code Component} to get properties useful for painting
     * @param g  the graphics context
     * @param x  the X coordinate of the icon's top-left corner
     * @param y  the Y coordinate of the icon's top-left corner
     */
    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        if (imageIcon != null) {
            // 如果已经有ImageIcon,就告诉它画出自己
            imageIcon.paintIcon(c, g, x, y);
        } else {
            // 没有ImageIcon,显示“加载中...”的消息
            g.drawString("Loading CD Cover, please wait...", x + 300, y + 190);
            // 如果还没有试着取出图像
            if (!retrieving) {
                // 那么就开始取出图像
                retrieving = true;
                /*
                在这里加载真正的icon图像。
                请注意,使用ImageIcon加载图像是同步的。也就是说,只有在图像加载完之后,ImageIcon构造器才会返回。
                这样,我们的程序会耗在这里,动弹不得,也没办法显示消息。所以我们要使用异步的方式操作:使用线程。
                 */
                // 我们不希望挂起整个用户界面,所以用另一个线程取出图像
                retrievalThread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            // 在线程中,实例化此ImageIcon对象,其构造器会在图像加载完成后在返回。
                            setImageIcon(new ImageIcon(imageUrl, "CD Cover"));
                            // 当图像准备好时,我们告诉Swing,需要重绘。
                            c.repaint();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                retrievalThread.start();
            }
        }
    }

    /**
     * 在图像加载完毕前,返回默认的宽。
     * 图像加载完毕后,交给ImageIcon处理
     * @return
     */
    @Override
    public int getIconWidth() {
        if (imageIcon != null) {
            return imageIcon.getIconWidth();
        }
        return 800;
    }

    /**
     * 在图像加载完毕前,返回默认的高。
     * 图像加载完毕后,交给ImageIcon处理
     * @return
     */
    @Override
    public int getIconHeight() {
        if (imageIcon != null) {
            return imageIcon.getIconHeight();
        }
        return 800;
    }
}

/**
 * 这是测试类
 */
public class ImageProxyTest {

    ImageComponent imageComponent;
    JFrame jFrame = new JFrame("CD Cover viewer");
    JMenuBar jMenuBar;
    JMenu jMenu;
    Hashtable<String, String> albums = new Hashtable<>();

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

    public ImageProxyTest() throws Exception {
        albums.put("Buddha","https://cdn.stocksnap.io/img-thumbs/960w/brick-building_CNR0WYMVJX.jpg");
        albums.put("Ama","https://cdn.stocksnap.io/img-thumbs/960w/holding-glass_V0ZNGACCAS.jpg");
        albums.put("Alex","https://cdn.stocksnap.io/img-thumbs/960w/building-architecture_FFCNJ3BDFC.jpg");

        URL initialURL = new URL(albums.get("Buddha"));
        jMenuBar = new JMenuBar();
        jMenu = new JMenu("Favorite Albums");
        jMenuBar.add(jMenu);
        jFrame.setJMenuBar(jMenuBar);
        for (Enumeration e = albums.keys(); e.hasMoreElements();) {
            String name = (String) e.nextElement();
            JMenuItem jMenuItem = new JMenuItem(name);
            jMenu.add(jMenuItem);
            jMenuItem.addActionListener(event -> {
                imageComponent.setIcon(new ImageProxy(getAlbumUrl(event.getActionCommand())));
                jFrame.repaint();
            });
        }
        // set up frame and menus
        Icon icon = new ImageProxy(initialURL);
        imageComponent = new ImageComponent(icon);
        jFrame.getContentPane().add(imageComponent);
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.setSize(800, 600);
        jFrame.setVisible(true);
    }

    URL getAlbumUrl(String name) {
        try {
            return new URL(albums.get(name));
        } catch (Exception e) {
            return null;
        }
    }
}

import javax.swing.*;
import java.awt.*;

/**
 * 图片组件
 */
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 h = icon.getIconHeight();
        int w = icon.getIconWidth();
        int x = (800 - w) / 2;
        int y = (600 - h) / 2;
        icon.paintIcon(this, g, x, y);
    }
}
  1. ImageProxy类代理ImageIcon类,所以它同ImageIcon一样,也实现了Icon接口。
  2. ImageProxy类有一个ImageIcon类的引用。
  3. 如果ImageIcon没有被完整创建出来,那就由ImageProxy来创建一个,屏幕上显示“请稍候”。如果ImageIcon创建出来了,就将绘制任务委托给ImageIcon的实例。
  4. 因为不希望挂起整个用户界面,所以使用异步(新开一个线程)的方式加载图片并实例化ImageIcon类。

3、动态代理

3.1 定义

Java在java.lang.reflect包中有自己的代理支持,利用这个包你可以在运行时动态的创建代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类是在运行时创建的,我们称这个Java技术为:动态代理。

下面我们来看看动态代理的类图

dynamic.png

名词解释
InvocationHandler:调用处理器

因为Java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy不是你直接实现的。我们要把代码放在InvocationHandler中。InvocationHandler的工作是响应代理的任何调用。你可以把InvocationHandler想象成代理收到方法调用后,请求做实际工作的对象。

3.2 创建动态代理

1. 创建InvocationHandler
当代理的方法被调用时,不管是哪个方法,代理都会把这个调用转发给InvocationHandler,调用它的invoke()方法。

让我们看看这是如何工作的?

dynamic2.png 2. 创建动态代理
3. 利用适当的代理包装真实对象

下面来看看代码

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 hotOrNotRating);
}

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


    // 其他的getters、 setters

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

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

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

/**
 * PersonBean的动态代理
 * 允许拥有者查看自己的信息、设置自己的信息、但是不能给自己评分
 *
 * 所有调用处理器都实现了InvocationHandler接口
 */
public class OwnerInvocationHandler implements InvocationHandler {

    PersonBean personBean;
    // 将personBean传入构造器,并保持它的引用
    public OwnerInvocationHandler(PersonBean personBean) {
        this.personBean = personBean;
    }

    /**
     * 每次proxy的方法被调用,就会导致proxy调用此方法
     * @param proxy 方法被调用的代理实例
     *
     * @param method 真正调用的方法
     *
     * @param args 方法参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (method.getName().startsWith("get")) {
                return method.invoke(personBean, args);
            } else if (method.getName().equals("setHotOrNotRating")) {
                throw new IllegalAccessException();
            } else if (method.getName().startsWith("set")) {
                return method.invoke(personBean, args);
            }
        } catch (InvocationTargetException e) {
            // 如果真正主题抛出异常的话,就会执行这里
            e.printStackTrace();
        }
        // 如果调用其他方法,一律不处理,返回null
        return null;
    }
}

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

/**
 * 这是一个测试类
 */	
public class MatchMarkingTest {

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

    private void drive() {
        PersonBean joe = new PersonBeanImpl();
        joe.setName("Joe JavaBean");
        joe.setGender("man");
        joe.setInterests("bowling, Go");
        joe.setHotOrNotRating(20);

        PersonBean ownerProxy = getOwnerProxy(joe);
        // Proxy.isProxyClass 判断是不是一个代理类
        System.out.println("is proxy?" + Proxy.isProxyClass(ownerProxy.getClass()));
        System.out.println("is proxy?" + Proxy.isProxyClass(joe.getClass()));

        System.out.println("name is " + ownerProxy.getName());
        System.out.println("interests is " + ownerProxy.getInterests());
        ownerProxy.setInterests("balls");
        System.out.println("interests is " + ownerProxy.getInterests());
        try {
            ownerProxy.setHotOrNotRating(30);
        } catch (Exception e) {
            System.out.println("cannot set rating from owner proxy");
        }
        System.out.println("hotOrNotRating is " + ownerProxy.getHotOrNotRating());
    }

    /**
     * 此方法需要一个Person对象作为参数,然后返回它的代理。
     * 因为代理和主题有相同的接口,所以我们返回一个PersonBean。
     * @param person
     * @return
     */
    PersonBean getOwnerProxy(PersonBean person) {
        // 将person传入调用处理器的构造器中。这正是处理器能够访问RealSubject的原因
        return getProxy(person, new OwnerInvocationHandler(person));
    }

    /**
     * 此方法创建了代理
     * @param person
     * @param handler
     * @return
     */
    PersonBean getProxy(PersonBean person, InvocationHandler handler) {
        // 利用proxy类的静态newProxyInstance方法创建代理
        // 将PersonBean的类载入器当做参数
        // person.getClass().getInterfaces() 是代理需要实现的接口
        return (PersonBean) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), handler);
    }
}

4、其他的代理模式变体

  • 防火墙代理:控制网络资源的访问,保护主题免于“坏客户”的侵害。
  • 智能引用代理:当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数。
  • 缓存代理:为开销大的运算结果提供暂时存储:它也允许多个客户共享结果,以减少计算或网络延迟。
  • 同步代理:在多线程的情况下为主题提供安全的访问。
  • 复杂隐藏代理:用来隐藏一个类的复杂集合的复杂度,并进行访问控制。有时候也称为“外观代理”。
  • 写入时复制代理:用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。

多种模式之间的对比

二、总结要点

  • 代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。
  • 远程代理管理客户和远程对象之间的交互。
  • 虚拟代理控制访问实例化开销大的对象。
  • 保护代理基于调用者控制对对象方法的访问。
  • 代理模式有许多变体,例如:缓存代理,同步代理、防火墙代理、写入时复制代理。
  • 代理在结构上类似装饰者,但是目的不同。
  • 装饰者模式为对象加上行为,而代理则是控制访问。
  • Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器。
  • 就和其他包装者一样,代理会造成你的设计中类的数目增加。

三、应用场景

代理模式在SpringBoot中的应用非常广泛。是实现 面向切面编程(AOP) 的核心机制。它允许开发者在不修改业务原有代码的前提下,动态的为对象添加额外的功能,从而实现关注点的分离,提升代码的可维护性和复用性。

核心应用场景

代理模式在Spring Boot中主要用于处理那些与核心业务逻辑无关,但又横跨多个模块的“横切关注点”,主要包括:

  • 事务管理:通过@Transactional注解,Spring会自动为方法创建代理,在方法执行前后自动管理数据库事务的开启、提交或回滚。
  • 日志记录:可以在方法调用前后自动记录执行时间、参数、返回值等信息,便于系统监控和问题排查。
  • 安全控制:在方法执行前检查用户权限,实现方法级别的访问控制。
  • 性能监控:统计特定方法的执行耗时,用于性能分析。
  • 缓存管理:通过@Cacheable等注解,代理层可以判断结果是否已缓存,避免重复计算。