简介概念

举个简单的例说明代理模式就是:假如现在需要买一辆二手车,可以自己去找车源、做质量检测等一系列车辆过户的流程,但是这实在太浪费时间和精力了,其实可以通过找中介的方式,同样会找车源、做质量检测等一系列车辆过户的流程,但是这样自己就只需要选车、付钱即可。

分类

在实际开发中,代理模式根据其目的和实现方式的不同可分为很多种类,如下是常用的几种代理模式:

  • 远程代理:为一个位于不同地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以在另一台主机中
  • 虚拟代理:如果需要创建一个资源消耗较大的对象,可以先创建一个消耗相对较小的对象来表示,真实对象只有需要时才会被真正创建
  • 保护代理:控制对一个对象的访问,可以给不同用户提供不同级别的使用权限
  • 缓冲代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
  • 智能引用代理:当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等

功能分类远程代理

远程代理是一种常见的代理模式,远程代理对象承担了大部分的网络通信工作,使得客户端程序可以访问在远程主机上的对象。

对于客户端而言,无需关心实现具体业务的是谁,只需要按照服务接口所定义的方法直接与本地主机中的代理对象交互即可。

在 Java 语言中,可以通过 RMI(Remote Method Invocation, 远程方法调用) 机制来实现远程代理。代码示例如下:

服务端部分代理

import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.server.UnicastRemoteObject;// 远程接口public interface IMyRemote extends Remote {    String SayHello() throws RemoteException;}// 远程接口实现 - 远程对象public class MyRemoteImpl extends UnicastRemoteObject implements IMyRemote {    public MyRemoteImpl() throws RemoteException {        super();    }    @Override    public String SayHello() throws RemoteException {        return "Server says, 'Hey'";    }    public static void main(String[] args) throws RemoteException, MalformedURLException {        IMyRemote service = new MyRemoteImpl();        // 启动本地 RMI 服务,默认端口是 1099        LocateRegistry.createRegistry(1099);        // 注册远程对象        Naming.rebind("rmi://localhost:1099/RemoteHello", service);    }}

客户端部分代理

import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.NotBoundException;import java.rmi.RemoteException;public class MyRemoteClient {    private void go() throws RemoteException, NotBoundException, MalformedURLException {        IMyRemote service = (IMyRemote) Naming.lookup("rmi://localhost:1099/RemoteHello");        System.out.println(service.sayHello());    }    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {        new MyRemoteClients().go();    }}

虚拟代理

虚拟代理会在真实对象创建成功之前扮演其替身,而当真实对象创建成功之后,虚拟代理再将用户的请求转发给真实对象。

使用虚拟代理的场景非常容易理解,主要有以下两种:

  • 由于对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间相对较短的代理对象来代表真实对象,通常在实现时结合多线程使用
  • 当一个对象的加载十分耗费系统资源的时候,也非常适合使用虚拟代理

具体实现

代理模式的具体实现从运行时的角度可以分成两种:一种是静态代理,即在代码运行之前就已经确定好代理关系;另一种是动态代理,可以在代码运行时才决定如何实现代理关系。

静态代理

静态代理是比较好理解的实现方式,在这种实现方式中,代理类所实现的接口和所代理的方法都被固定,需要在编译期就预先对原始类编写代理类。

基于接口

一般情况下,参考基于接口而非实现编程的设计思想,为了让代码的改动尽量少,代理类和原始类应该实现同样的接口。

如下是使用图片展示作为例子的代码示例:

图片 Image 接口:描述图片具有的行为

public interface Image {    void display();}

展示图片 ShowImage 类:实际的图片原始类

public class ShowImage implements Image {    public ShowImage() {}    @Override    public void display() {        System.out.println("ShowImage display!");    }}

代理图片 ProxyImage 类:在真实图片前包装一层的代理类

public class ProxyImage implements Image {    // 通过依赖注入的方式    private ShowImage showImage;    public ProxyImage(ShowImage showImage) {        this.showImage = showImage;    }    @Override    public void display() {        System.out.println("ProxyImage display start!");        this.showImage.display();        System.out.println("ProxyImage display end!");    }}

基于继承

如果原始类并没有实现接口,并且原始类代码由其他人开发维护,可以通过代理类继承原始类的方法来实现代理模式。

假设上述展示图片 ShowImage 类没有实现接口,可以重新定义代理图片 ProxyImage 类如下:

public class ProxyImage extends ShowImage {    public ProxyImage() {}    @Override    public void display() {        System.out.println("ProxyImage display start!");        super.display();        System.out.println("ProxyImage display end!");    }}

动态代理

静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了开发成本和维护成本。

对于静态代理存在的问题,可以通过动态代理来解决。

动态代理的原理是:不事先为每个原始类编写代理类,而是在代码运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

在 Java 中,主要就是利用反射机制在运行时创建代理类。代码示例如下:

// 定义接口,描述行为public interface Subject {    void hello(String param);}// 实现接口public class SubjectImpl implements Subject {    public SubjectImpl() {}    @Override    public void hello(String param) {        System.out.println("hello " + param);    }}// 创建代理类public class SubjectProxy implements InvocationHandler {    private Subject subject;    public SubjectProxy(Subject subject) {        this.subject = subject;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        System.out.println("----- Start -----");        Object invoke = method.invoke(subject, args);        System.out.println("----- End -----");        return invoke;    }}// 实际调用public class Main {    public static void main(String[] args) {        Subject subject = new SubjectImpl();        InvocationHandler subjectProxy = new SubjectProxy(subject);        // 代理类的类加载器        // 被代理类的接口,如果有多个就是以数组形式传入        // 代理类实例        Subject proxyInstance = (Subject) Proxy.newProxyInstance(                subjectProxy.getClass().getClassLoader(),                subject.getClass().getInterfaces(),                subjectProxy        );        // 执行代理方法        proxyInstance.hello("world");    }}

常见的 Java 动态代理实现方式有 JDK 代理、CGLib 代理。基于 JDK 的动态代理必须实现一个接口,而 CGLib 动态代理没有这个限制,是另一种不错的选择。

总结优点

代理模式的主要优点如下:

  • 协调调用者和被调用者,在一定程度上降低了系统的耦合,满足迪米特原则
  • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无需修改源代码,符合开闭原则
  • 公共的事务由代理来完成,使得真实处理的业务更加纯粹,不再去关注公共业务,而公共业务发生扩展时也变得更加集中和方便

缺点

代理模式的主要缺点如下:

  • 在客户端和真实主题之间增加代理对象,有些类型的代理模式可能会造成请求的处理速度变慢
  • 实现代理模式需要额外的成本,有些代理模式的实现非常复杂

适用场景

代理模式的适用场景如下:

  • 当客户端对象需要访问远程主机中的对象时,可以使用远程代理
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象时,可以使用虚拟代理
  • 当需要控制一个对象的访问,为不同用户提供不同级别的的访问权限时,可以使用保护代理
  • 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时,可以使用缓冲代理
  • 当需要为一个对象的访问(引用)提供一些额外的操作时,可以使用智能引用代理

源码

在 JDK 中,提供了 java.lang.reflect.Proxy 支持创建动态代理类。

首发于翔仔的个人博客,点击查看更多。