5

代理模式

基本概念

不论是静态代理还是动态代理, 其本质都是代理模式的一种实现, 那么什么是代理模式呢?
代理模式, 即给某一个对象提供一个代理, 并由代理对象控制对原对象的引用.
代理模式其实取材于实际生活, 例如我们生活中常见的房屋租赁代理, 我们在租房时, 一般不是直接和房东打交道, 而是和中间商打交道, 即中间商代理了房东, 我们通过中间商完成与房东的间接沟通.
代理模式主要涉及三个角色:

  • Subject: 抽象角色, 声明真实对象和代理对象的共同接口.

  • Proxy: 代理角色, 它是真实角色的封装, 其内部持有真实角色的引用, 并且提供了和真实角色一样的接口, 因此程序中可以通过代理角色来操作真实的角色, 并且还可以附带其他额外的操作.

  • RealSubject: 真实角色, 代理角色所代表的真实对象, 是我们最终要引用的对象.

这三个角色的 UML 图如下(图片引用自维基百科)

clipboard.png

代理模式的优点

  • 代理模式能够协调调用者和被调用者, 在一定程度上降低了系统的耦合度.

  • 代理模式可以提供更大的灵活性

代理模式的缺点

  • 由于在客户端和真实主题之间增加了代理对象, 因此有些类型的代理模式可能会造成请求的处理速度变慢

  • 实现代理模式需要额外的工作, 有些代理模式的实现 非常复杂

代理模式的常用实现

  • 远程代理(remote proxy): 用本地对象来代表一个远端的对象, 对本地对象方法的调用都会作用于远端对象. 远程代理最常见的例子是 ATM 机, 这里 ATM 机充当的就是本地代理对象, 而远端对象就是银行中的存取钱系统, 我们通过 ATM 机来间接地和远端系统打交道.

  • 虚拟代理(virtual proxy): 虚拟代理是大型对象或复杂操作的占位符. 它常用的场景是实现延时加载或复杂任务的后台执行. 例如当一个对象需要很长的时间来初始化时, 那么可以先创建一个虚拟代理对象, 当程序实际需要使用此对象时, 才真正地实例化它, 这样就缩短了程序的启动时间, 即所谓的延时加载.

  • 保护代理(protect proxy): 控制对一个对象的访问, 可以给不同的用户提供不同级别的使用权限. 例如我们可以在代理中检查用户的权限, 当权限不足时, 禁止用户调用此对象的方法.

  • 缓存代理(cache proxy): 对实际对象的调用结果进行缓存. 例如一些复杂的操作, 如数据库读取等, 可以通过缓存代理将结果存储起来, 下次再调用时, 直接返回缓存的结果.

  • 图片代理(image proxy): 当用户需要加载大型图片时, 可以通过代理对象的方法来进行处理, 即在代理对象的方法中, 先使用一个线程向客户端浏览器加载一个小图片, 然后在后台使用另一个线程来调用大图片的加载方法将大图片加载到客户端.

关于静态代理

为了弄懂 Java 的动态代理, 我们首先来了解一下静态代理吧.
首先举一个例子, 假设我们需要实现一个从不同存储介质(例如磁盘, 网络, 数据库)加载图片的功能, 那么使用静态代理的方式的话, 需要实现如下工作:

  • 定义一个加载图片的接口

  • 实现实际操作对象(LoadFromDisk, LoadFromNet, LoadFromDB)

  • 实现代理对象

根据上面的流程, 我们实现的代码如下:
接口:

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/10/7
 */
public interface LoadImage {
    Image loadImage(String name);
}

代理:

public class LoadImageProxy implements LoadImage {
    private LoadImage loadImageReal;

    public LoadImageProxy(LoadImage loadImageReal) {
        this.loadImageReal = loadImageReal;
    }

    @Override
    public Image loadImage(String name) {
        return loadImageReal.loadImage(name);
    }
}

使用:

public class App {
    public static void main(String[] args) {
        LoadFromDisk loadFromDisk = new LoadFromDisk();
        LoadImageProxy proxy = new LoadImageProxy(loadFromDisk);
        proxy.loadImage("/tmp/test.png");
    }
}

根据代理模式, 我们在上面的代码中展示了一个基本的静态代理的例子, LoadImageProxy 是代理类, 它会将所有的接口调用都转发给实际的对象, 并从实际对象中获取结果. 因此我们在实例化 LoadImageProxy 时, 提供不同的实际对象时, 就可以实现从不同的介质中读取图片的功能了.

动态代理的实现

看完了上面的静态代理的例子, 下面我们来进入正题吧.
那么什么是 Java 的动态代理呢? 其实很简单, 顾名思义, 所谓动态代理就是 动态地创建代理并且动态地处理所代理对象的方法调用.
在 Java 的动态代理中, 涉及两个重要的类或接口:

  • Proxy

  • InvocationHandler

关于 Proxy 类

Proxy 主要是提供了 Proxy.newProxyInstance 静态方法, 其签名如下:

public static Object newProxyInstance(ClassLoader loader,
                                  Class<?>[] interfaces,
                                  InvocationHandler h)
    throws IllegalArgumentException

此静态方法需要三个参数:

  • loader: 即类加载器, 指定由哪个ClassLoader对象来对生成的代理对象进行加载

  • interfaces: 一个Interface对象的数组, 表示的是代理对象所需要实现的接口.

  • h: 即 InvocationHandler 的实现对象. 当调用代理对象的接口时, 实际上会 通过 InvocationHandler.invkoe 将调用转发给实际的对象.

这个静态类会返回一个代理对象, 在程序中可以可通过这个代理对象来对实际对象进行操作.

关于 InvocationHandler 接口

我们在前面提到过, 在调用 Proxy.newProxyInstance 方法时, 需要传递一个 InvocationHandler 接口的实现对象, 那么这个 InvocationHandler 接口有什么用呢?
实际上, 在 Java 动态代理中, 我们都必须要实现这个接口, 它是沟通了代理对象和实际对象的桥梁, 即:

InvocationHandler is the interface implemented by the invocation handler of a proxy instance.
Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.

当我们调用了代理对象所提供的接口方法时, 此方法调用会被封装并且转发到 InvocationHandler.invoke 方法中, 在 invoke 方法中调用实际的对象的对应方法.

InvocationHandler.invoke 方法的声明如下:

public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;

这个方法接收三个参数:

  • proxy: 指代理对象, 即 Proxy.newProxyInstance 所返回的对象(注意, proxy 并不是实际的被代理对象)

  • method: 我们所要调用真实对象的方法的 Method 对象

  • args: 调用真实对象某个方法时接受的参数

invoke 方法的返回值是调用的真实对象的对应方法的返回值.

动态代理例子

使用动态代理的步骤很简单, 可以概括为如下两步:

  1. 实现 InvocationHandler 接口, 并在 invoke 中调用真实对象的对应方法.

  2. 通过 Proxy.newProxyInstance 静态方法获取一个代理对象.

我们还是以在静态代理中展示的加载图片的例子为例, 首先加载图片的接口如下:
接口:

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/10/7
 */
public interface LoadImage {
    Image loadImage(String name);
}

接下来我们需要实现 InvocationHandler 接口:

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/10/7
 */
public class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Proxy class: " + proxy.getClass() + ", method: " + method + ", args: " + args);
        return method.invoke(proxied, args);
    }
}

可以看到, 在实现的 invoke 方法中, 我们简单地通过 method.invoke(proxied, args) 来调用了真实对象的方法.

有了 InvocationHandler 后, 我们就可以创建代理对象并通过代理对象来操作实际对象了:

public class App {
    public static void main(String[] args) {
        // 实际对象
        LoadFromDisk loadFromDisk = new LoadFromDisk();
        // 通过 Proxy.newProxyInstance 静态方法创建代理对象
        LoadImage loadImage = (LoadImage) Proxy.newProxyInstance(LoadImage.class.getClassLoader(), new Class[]{LoadImage.class}, new DynamicProxyHandler(loadFromDisk));

        // 通过代理对象操作实际对象.
        loadImage.loadImage("/tmp/test.png");
    }
}

为什么需要使用动态代理

看了 静态代理动态代理, 有的朋友就会有疑惑了, 明明使用静态代理就可以完成的功能, 为什么还需要使用动态代理呢?
我认为相比静态代理, 动态代理有两点优点:

  • 动态代理具有更强的灵活性, 因为它不用在我们设计实现的时候就指定某一个代理类来代理哪一个被代理对象, 我们可以把这种指定延迟到程序运行时由JVM来实现.

  • 动态代理更为统一与简洁.

为什么这么说呢? 我们还是以图片加载的例子说明吧. 现在我们假设 LoadImage 接口需要提供更多的方法, 并且我们希望每个方法调用都记录 Log. 因此 LoadImage 接口更改如下:

public interface LoadImage {
    // 加载图片
    Image loadImage(String name);
    // 加载图片, 并翻转图片
    Image loadAndRotateImage(String name);
    // 获取图片的缩略图
    Image loadSmallImage(String name);
}

我们添加了两个新的方法: loadAndRotateImage 和 loadSmallImage.
那么在静态代理的方法下, 我们怎么实现所需要的功能呢? 下面是具体的代码:

public class LoadImageProxy implements LoadImage {
    private LoadImage loadImageReal;

    public LoadImageProxy(LoadImage loadImageReal) {
        this.loadImageReal = loadImageReal;
    }

    @Override
    public Image loadImage(String name) {
        System.out.println("Call method: loadImage, file name: " + name);
        return loadImageReal.loadImage(name);
    }

    @Override
    public Image loadAndRotateImage(String name) {
        System.out.println("Call method: loadAndRotateImage, file name: " + name);
        return loadImageReal.loadImage(name);
    }

    @Override
    public Image loadSmallImage(String name) {
        System.out.println("Call method: loadSmallImage, file name: " + name);
        return loadImageReal.loadImage(name);
    }
}

上面代码例子中, 我们分别实现了 loadImage, loadAndRotateImage 和 loadSmallImage 代理方法, 并且为每个方法都添加了 log.
作为对比, 我们来看一下使用静态代理时的代码实现吧:

public class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Call method: loadImage, file name: " + args[0]);
        return method.invoke(proxied, args);
    }
}

我们看到, 在使用动态代理时, 我们除了添加一行 log 输出外, 没有进行任何的更改, 而在静态代理中, 我们需要分别实现每个代理方法, 并且在每个方法中添加日志输出. 可以想象, 当我们的接口方法比较多时, 使用静态代理就会造成了大量的代码修改, 并且在将来我们需要去除方法调用的 log 时, 静态代理的方式就十分不便了, 而对于动态代理而言, 仅仅需要修改一两行代码而已.

本文由 yongshun 发表于个人博客, 采用 署名-相同方式共享 3.0 中国大陆许可协议.
Email: yongshun1228@gmail .com
本文标题为: Java 动态代理(Dynamic proxy) 小结
本文链接为: https://segmentfault.com/a/1190000007089902


永顺
5.5k 声望1.3k 粉丝

好饿好饿好饿, 我真的好饿.