结构型模式-代理模式:替身与本体的博弈,掌控访问的艺术

2025/10/04 Design Patterns 共 5634 字,约 17 分钟

结构型模式-代理模式:替身与本体的博弈

在软件设计与开发中,我们常常会遇到一种情况:客户端需要与一个对象交互,但这个对象由于某些原因(如创建开销大、位于远程、需要安全控制等)不宜或不适合被直接访问。此时,一个优雅的解决方案便应运而生——代理模式。它如同一位精明的“替身演员”,在“本体”对象与客户端之间架起了一座智能的桥梁,掌控着访问的流程。

一、 什么是代理模式?

代理模式是一种结构型设计模式,它为另一个对象提供一个替身或占位符,以控制对这个对象的访问。

这个定义包含了三个核心角色:

  1. 抽象主题: 声明了真实主题和代理主题的共同接口。这样,任何使用真实主题的地方都可以使用代理主题。
  2. 真实主题: 定义了代理所代表的真实对象,是业务逻辑的真正执行者,是最终我们引用的“本体”。
  3. 代理: 持有一个引用,使得代理可以访问真实主题。代理主题与真实主题实现相同的接口,因此可以在任何需要真实主题的地方替代它。它负责在适当的时候创建和删除真实主题对象,并在调用真实主题的前后处理一些额外逻辑。

它们之间的关系,可以用下面的类图清晰地表示:

客户端 --> 抽象主题接口
真实主题 --> 抽象主题接口
代理 --> 抽象主题接口
代理 --> 真实主题

二、 代理模式的三种主要形式

代理模式根据其实现时机和目的,主要分为三种形式:静态代理、动态代理和虚拟代理。

1. 静态代理

静态代理,顾名思义,在编译阶段就已经确定代理关系。我们需要手动为每一个真实主题类编写一个对应的代理类。

代码示例:文件加载器

假设我们有一个文件加载接口 FileLoader 和它的真实实现 RealFileLoader。现在我们想为文件加载添加权限校验。

// 1. 抽象主题
public interface FileLoader {
    String loadFile(String fileName);
}

// 2. 真实主题
public class RealFileLoader implements FileLoader {
    @Override
    public String loadFile(String fileName) {
        System.out.println("正在加载文件: " + fileName);
        // 模拟耗时的文件加载操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "文件: " + fileName + " 的内容";
    }
}

// 3. 代理
public class FileLoaderProxy implements FileLoader {
    private RealFileLoader realFileLoader;
    private String userRole;

    public FileLoaderProxy(String userRole) {
        this.userRole = userRole;
    }

    @Override
    public String loadFile(String fileName) {
        // 前置处理:权限控制
        if (!"admin".equals(userRole)) {
            System.out.println("权限不足,拒绝访问文件。");
            return null;
        }

        // 延迟初始化:在需要时才创建真实对象
        if (realFileLoader == null) {
            realFileLoader = new RealFileLoader();
        }

        // 调用真实对象的方法
        String content = realFileLoader.loadFile(fileName);

        // 后置处理:记录日志(这里只是示例,未实际记录)
        System.out.println("文件加载操作已完成。");

        return content;
    }
}

// 4. 客户端使用
public class Client {
    public static void main(String[] args) {
        // 使用代理访问
        FileLoader loader = new FileLoaderProxy("user"); // 普通用户
        String content = loader.loadFile("confidential.txt");
        System.out.println(content); // 输出:权限不足,拒绝访问文件。 null

        FileLoader adminLoader = new FileLoaderProxy("admin"); // 管理员
        content = adminLoader.loadFile("confidential.txt");
        System.out.println(content); // 输出:正在加载文件: confidential.txt ... 文件: confidential.txt 的内容
    }
}

优缺点分析

  • 优点:在不修改真实主题类的情况下,通过代理类扩展了功能(权限校验、日志等),符合开闭原则。
  • 缺点:如果真实主题接口有很多方法,代理类也需要实现所有方法,即使有些方法不需要增强,会导致代码冗余。并且,每个真实主题都需要一个对应的代理类,当系统规模变大时,类的数量会急剧增加。

2. 动态代理

动态代理解决了静态代理的缺点。它不是在编译期创建代理类,而是在运行时动态生成。在Java中,主要有两种实现方式:基于JDK的原生动态代理和基于CGLIB的字节码增强。

JDK动态代理示例

JDK动态代理要求真实主题必须实现至少一个接口。

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

// 动态代理的调用处理器
public class FileLoaderInvocationHandler implements InvocationHandler {
    private Object realTarget;
    private String userRole;

    public FileLoaderInvocationHandler(Object realTarget, String userRole) {
        this.realTarget = realTarget;
        this.userRole = userRole;
    }

    // 创建代理对象
    public static Object createProxy(Object target, String userRole) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new FileLoaderInvocationHandler(target, userRole)
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置增强:权限校验
        if ("loadFile".equals(method.getName()) && !"admin".equals(userRole)) {
            System.out.println("[动态代理] 权限不足,拒绝访问文件。");
            return null;
        }

        // 调用真实对象的方法
        Object result = method.invoke(realTarget, args);

        // 后置增强:记录日志
        System.out.println("[动态代理] 方法 " + method.getName() + " 被调用。");
        return result;
    }
}

// 客户端使用
public class DynamicProxyClient {
    public static void main(String[] args) {
        FileLoader realLoader = new RealFileLoader();

        // 创建动态代理
        FileLoader proxyLoader = (FileLoader) FileLoaderInvocationHandler.createProxy(realLoader, "user");
        proxyLoader.loadFile("test.txt"); // 输出:[动态代理] 权限不足,拒绝访问文件。

        FileLoader adminProxyLoader = (FileLoader) FileLoaderInvocationHandler.createProxy(realLoader, "admin");
        adminProxyLoader.loadFile("test.txt");
        // 输出:
        // 正在加载文件: test.txt
        // [动态代理] 方法 loadFile 被调用。
    }
}

优缺点分析

  • 优点:非常灵活,一个InvocationHandler可以代理多种类型的对象,大大减少了代码量。
  • 缺点:只能代理实现了接口的类。

3. 虚拟代理

虚拟代理是一种用于延迟初始化(懒加载)的代理。当创建一个对象的开销非常大时,虚拟代理可以延迟对象的实际创建,直到真正需要它的时候。

典型场景:加载高分辨率图片。在图片完全加载前,先用一个占位图(代理)显示,同时在后台加载真实图片,加载完毕后再替换。

// 抽象主题
public interface Image {
    void display();
}

// 真实主题 - 高开销对象
public class HighResolutionImage implements Image {
    private String filename;

    public HighResolutionImage(String filename) {
        this.filename = filename;
        loadImageFromDisk(); // 模拟昂贵的初始化过程
    }

    private void loadImageFromDisk() {
        System.out.println("从磁盘加载高分辨率图片: " + filename + ",这需要很长时间...");
        try {
            Thread.sleep(2000); // 模拟加载耗时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("图片加载完成!");
    }

    @Override
    public void display() {
        System.out.println("显示图片: " + filename);
    }
}

// 虚拟代理
public class ImageProxy implements Image {
    private String filename;
    private HighResolutionImage realImage; // 开始时为null

    public ImageProxy(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        System.out.println("【代理】图片预览区域已准备就绪。");

        // 只有当真正需要显示时,才创建真实对象
        if (realImage == null) {
            realImage = new HighResolutionImage(filename);
        }
        // 委托给真实对象
        realImage.display();
    }
}

// 客户端使用
public class VirtualProxyClient {
    public static void main(String[] args) {
        System.out.println("程序启动...");
        Image image = new ImageProxy("photo_10MB.jpg");

        System.out.println("--- 进行其他一些操作 ---");
        // 此时真实的HighResolutionImage还未被创建

        System.out.println("--- 第一次调用display ---");
        image.display(); // 此时才会创建真实对象并加载图片

        System.out.println("--- 第二次调用display ---");
        image.display(); // 真实对象已存在,直接使用
    }
}

三、 代理模式的应用场景

代理模式在现实世界的软件开发中无处不在:

  1. 远程代理:为一个对象在不同的地址空间提供局部代表。例如,RMI(Remote Method Invocation)和各种RPC框架的客户端存根就是远程代理。
  2. 虚拟代理:如上例所示,用于延迟创建开销很大的对象。
  3. 保护代理:控制对原始对象的访问,用于对象应该有不同的访问权限的时候。如上文中的权限校验例子。
  4. 智能引用代理:在访问对象时执行附加操作,例如:
    • 计算一个对象的引用次数。
    • 当第一次引用一个持久对象时,将它装入内存。
    • 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
  5. 缓存代理:为开销大的运算结果提供临时存储,允许多个客户端共享结果以减少计算或网络延迟。
  6. Spring AOP:Spring框架的面向切面编程,其底层大量使用了动态代理(JDK Proxy或CGLIB)来实现事务管理、日志、安全等横切关注点的织入。

四、 总结

代理模式通过引入一个“替身”,巧妙地实现了对“本体”访问的控制和增强。它完美地体现了面向对象设计中的“单一职责原则”和“开闭原则”,将核心业务逻辑与周边的系统服务(如安全、日志、延迟加载)解耦。

  • 静态代理简单直观,但灵活性差,适用于代理类较少的情况。
  • 动态代理灵活强大,极大地减少了冗余代码,是现代框架(如Spring)的基石。
  • 虚拟代理是性能优化的利器,通过延迟加载提升了系统响应速度。

理解并熟练运用代理模式,能够让你在设计系统时更加游刃有余,构建出更加灵活、健壮和高效的软件架构。在这场“替身”与“本体”的博弈中,开发者正是那位运筹帷幄的导演。

文档信息

Search

    Table of Contents