• GoF设计模式——代理模式


    本文是【GoF设计模式】系列第7篇,更多内容欢迎关注公众号:咖啡八杯

    image

    前言

    为什么需要代理模式?

    有时候我们不能或不想直接访问某个对象。比如对象创建开销很大需要延迟加载,或者需要在访问前做权限检查,或者需要记录访问日志。直接在业务代码中掺杂这些逻辑会让代码臃肿且难以维护。

    代理模式通过引入一个中间层,将这些控制逻辑从业务代码中分离出来。客户端代码不需要知道它是在和代理交互还是真实对象交互,两者可以透明替换。

    概念

    代理模式Proxy Pattern是一种结构型设计模式,核心思想是为一个对象提供一个替身(代理),以控制对这个对象的访问

    代理对象和真实对象实现相同的接口,客户端通过代理间接访问真实对象,代理可以在调用前后添加额外的控制逻辑。

    代理模式的主要角色有:

    • Subject(抽象主题):声明真实主题和代理共同实现的业务方法,客户端面向该接口编程
    • RealSubject(真实主题):定义代理所代表的真实对象,是客户端最终要访问的对象
    • Proxy(代理):持有对真实主题的引用,实现与真实主题相同的接口,在调用真实主题前后添加控制逻辑
      image

    类图展示了静态结构,但对代理模式这种"调用拦截"的场景,时序图更能体现动态调用流程:

    RealSubjectProxyClientRealSubjectProxyClientrequest()前置处理(权限/日志/延迟加载)request()返回结果后置处理(日志/缓存)返回结果

    可以把代理理解为私人助理:老板(客户端)有事找某人(真实对象),先通过助理(代理)。助理可以在前面挡掉不重要的打扰(权限控制),也可以在事后记录行程(日志),老板全程不需要直接接触对方。这个比喻贯穿后面的实现章节,方便对照理解。

    实现

    代理模式的基本实现分为以下几个步骤:

    1. 定义抽象主题,一般是接口或抽象类,声明真实主题和代理对象实现的业务方法
    2. 定义真实主题,实现抽象主题中的具体业务
    3. 定义代理类,包含对 RealSubject 的引用,提供和真实主题相同的接口,在调用前后添加控制逻辑
    4. 客户端使用代理
    // 抽象主题
    interface Subject {
        // 声明同业务对象同名的方法
        public void request();
    }
    
    // 真实主题
    class RealSubject implements Subject {
        public void request() {
            System.out.println("RealSubject request");
        }
    }
    
    // 代理类
    class Proxy implements Subject {
        private RealSubject realSubject;
    
        @Override
        public void request() {
            // 访问真实主题之前:延迟加载
            if (realSubject == null) {
                realSubject = new RealSubject();
            }
            // 调用真实主题的方法
            realSubject.request();
            // 访问真实主题之后:可添加日志等逻辑
        }
    }
    

    总结

    代理模式本质上是一层"中间人"——为真实对象提供一个替身,在调用前后添加控制逻辑。

    什么时候用

    • 想控制对某个对象的访问(权限、延迟加载、缓存、日志)
    • 需要为远程对象提供本地代表
    • 引入第三方库或遗留代码,需要统一调用方式

    什么时候不用

    • 接口差异巨大,代理会变得臃肿
    • 能修改真实对象源码且代价不大,直接修改更简单
    • 系统设计阶段就能定义接口规范,从源头统一即可

    简单记忆

    代理解决"控制访问"的问题,是给真实对象"加一层控制"。能改源码就改,改不了才用代理。

    代理 vs 装饰器 vs 适配器 vs 外观:四个结构型模式都"包了一层对象",结构相似但意图不同:

    模式 接口关系 核心意图
    代理 目标接口 = 被包装对象接口 控制访问,附加访问前后逻辑
    装饰器 目标接口 = 被包装对象接口 增强功能,接口不变
    适配器 目标接口 ≠ 被包装对象接口 转换接口,让不兼容的类协同
    外观 目标接口是新设计的 简化复杂子系统的调用

    口诀对比:代理控访问,装饰增功能,适配改接口,外观简调用。

    代理模式 vs 中介者模式

    两者都引入"中间层",结构相似,但意图完全不同:

    维度 代理模式 中介者模式
    核心意图 控制对单个对象的访问 协调多个对象之间的交互
    对象关系 客户端 → 代理 → 真实对象(单向委托) 多个同事对象 ↔ 中介者 ↔ 多个同事对象(多向协调)
    封装内容 访问控制逻辑(权限、延迟加载、缓存) 对象间的交互规则、通信协议
    客户端感知 客户端不知道真实对象存在 各同事对象知道中介者存在,但不直接知道其他同事
    应用场景 Spring AOP、MyBatis Mapper、远程调用 MVC 框架的 Controller、聊天室服务器、GUI 事件分发

    用例子说明

    • 代理模式:你要见 CEO,先通过秘书(代理)。秘书控制访问——过滤不重要的人、安排时间。你只和秘书打交道,CEO 对你透明。
    • 中介者模式:公司的各部门(销售、研发、财务)不直接相互沟通,所有协调通过行政部(中介者)。销售要研发资源,找行政部安排;财务要销售数据,找行政部转发。各部门知道行政部,但不直接依赖其他部门。

    简单记忆

    代理管"谁能动",中介者管"怎么联动"。代理是单对象的门禁,中介者是多对象的调度中心。

    常见误区

    • 误区:代理模式 = 装饰器模式 → 意图不同:一个控访问,一个增功能
    • 误区:代理必须和真实对象同接口 → 对,这是代理的基本要求,否则就不是代理了
    • 误区:Spring @Transactional 在同一个类内部调用不生效 → 这是自调用绕过代理的经典坑,AOP 代理需要外部调用才能触发拦截逻辑

    练习题目

    门禁系统权限控制

    题目描述:某科技园区的门禁系统管理着多个房间,每个房间有名称和最低访问权限等级。用户通过门禁终端访问房间,门禁终端作为代理,会检查用户的权限等级:

    • 用户权限 ≥ 房间要求的等级 → 放行,房间显示欢迎信息
    • 用户权限 < 房间要求的等级 → 拒绝,房间不会响应

    请使用代理模式实现该门禁系统。其中:

    • Room(真实对象)拥有 enter() 方法,输出欢迎信息
    • RoomProxy(代理)在调用 enter() 前进行权限检查,只有通过才委托给真实对象

    输入描述:第一行输入一个整数,表示用户的权限等级。第二行输入一个整数 N(1 ≤ N ≤ 20),表示要访问的房间数量。接下来 N 行,每行包含房间名称和该房间要求的最低权限等级,用空格分隔。

    输出描述:对每个房间,通过代理访问后输出:

    • 权限足够:欢迎进入[房间名]
    • 权限不足:权限不足,无法进入[房间名]

    输入示例

    2
    3
    MeetingRoom 1
    Lab 3
    Office 2
    

    输出示例

    欢迎进入MeetingRoom
    权限不足,无法进入Lab
    欢迎进入Office
    

    解题思路:代理类 RoomProxy 实现 Room 接口,内部持有用户权限。在 enter() 方法中先检查用户权限是否满足房间要求,满足则委托给 RealRoom.enter(),否则直接拒绝。这体现了代理模式的核心——代理在调用真实对象之前增加控制逻辑(权限校验),对客户端透明。

    import java.util.*;
    
    public class Main {
        public static void main(String[] args) {
            Scanner sc = new Scanner(System.in);
            int qx = sc.nextInt();
            int n = sc.nextInt();
            RoomProxy proxy = new RoomProxy(qx);
            while (n-- > 0) {
                String name = sc.next();
                int roomQx = sc.nextInt();
                RealRoom room = new RealRoom(name, roomQx);
                proxy.setRoom(room);
                proxy.enter();
            }
        }
    }
    
    interface Room {
        public void enter();
    }
    
    class RealRoom implements Room {
        private String name;
        private int qx;
    
        public RealRoom(String name, int qx) {
            this.name = name;
            this.qx = qx;
        }
    
        public void enter() {
            System.out.println("欢迎进入" + this.name);
        }
    
        public String getName() {
            return this.name;
        }
    
        public int getQx() {
            return this.qx;
        }
    }
    
    class RoomProxy implements Room {
        private int qx;
        private RealRoom room;
    
        public RoomProxy(int qx) {
            this.qx = qx;
        }
    
        public void setRoom(RealRoom room) {
            this.room = room;
        }
    
        public void enter() {
            if (this.qx >= room.getQx()) {
                room.enter();
            } else {
                System.out.println("权限不足,无法进入" + room.getName());
            }
        }
    }
    

    扩展:实际项目中的代理模式

    Spring AOP 的动态代理

    Spring AOP 是代理模式最经典的应用。当 Bean 被 AOP 增强时,Spring 不会返回原始对象,而是返回一个代理对象。代理在方法调用前后插入切面逻辑(事务、日志、权限校验等),对调用方完全透明。

    // 业务代码:完全感知不到代理的存在
    @Service
    public class OrderService {
        @Transactional  // 事务由代理自动管理
        public void createOrder(Order order) {
            orderDao.save(order);
            inventoryService.deduct(order.getProductId(), order.getQty());
        }
    }
    
    // Spring 内部:创建代理对象(JDK 动态代理或 CGLIB)
    // 代理在 createOrder 前后自动开启/提交/回滚事务
    

    关键点:Spring 默认对实现了接口的 Bean 使用 JDK 动态代理,对没有实现接口的 Bean 使用 CGLIB 代理。开发者只需写 @Transactional@Cacheable 等注解,代理负责增强逻辑的织入。

    JDK 动态代理 vs CGLIB 对比

    对比维度 JDK 动态代理 CGLIB
    原理 实现接口,运行时生成 Proxy 类 继承目标类,生成子类
    要求 目标必须实现接口 目标类不能是 final
    Spring 默认场景 有接口的 Bean 无接口的 Bean
    性能(高版本 JDK) 较高 较低

    MyBatis 的 Mapper 代理

    MyBatis 中我们只写 Mapper 接口,不需要写实现类,就能直接调用方法执行 SQL。这是因为 MyBatis 在启动时为每个 Mapper 接口生成了代理对象。

    // 开发者只写接口
    public interface UserMapper {
        @Select("SELECT * FROM user WHERE id = #{id}")
        User findById(Long id);
    }
    
    // 使用:直接注入接口,实际上是代理对象
    @Service
    public class UserService {
        @Autowired
        private UserMapper userMapper;  // 这是代理对象
    
        public User getUser(Long id) {
            return userMapper.findById(id);  // 代理拦截调用,执行 SQL
        }
    }
    
    // MyBatis 内部:MapperProxy 拦截接口方法调用
    // 根据方法上的注解或 XML 映射,执行对应的 SQL 语句
    

    关键点MapperProxy 实现了 InvocationHandler 接口,拦截所有方法调用,将接口方法映射为 SQL 执行。这是 JDK 动态代理的典型应用,让接口方法调用变成了数据库操作。

    Java 动态代理实现 AOP 拦截器

    Java 原生提供了 java.lang.reflect.ProxyInvocationHandler 接口,可以在运行时动态创建代理类。这是实现 AOP、拦截器等机制的基础。

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    // 通用的日志拦截器
    public class LogInterceptor implements InvocationHandler {
        private Object target;  // 被代理的真实对象
    
        public LogInterceptor(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("[LOG] 调用方法: " + method.getName());
            long start = System.currentTimeMillis();
    
            Object result = method.invoke(target, args);  // 调用真实对象
    
            long elapsed = System.currentTimeMillis() - start;
            System.out.println("[LOG] 方法 " + method.getName() + " 耗时: " + elapsed + "ms");
            return result;
        }
    
        // 工具方法:创建代理对象
        @SuppressWarnings("unchecked")
        public static  T createProxy(T target) {
            return (T) Proxy.newProxyInstance(
                    target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(),
                    new LogInterceptor(target));
        }
    }
    
    // 使用
    interface UserService {
        public User findById(Long id);
    }
    
    class UserServiceImpl implements UserService {
        public User findById(Long id) {
            // 查询数据库...
            return new User(id, "张三");
        }
    }
    
    // 创建代理
    UserService proxy = LogInterceptor.createProxy(new UserServiceImpl());
    proxy.findById(1L);
    // 输出:
    // [LOG] 调用方法: findById
    // [LOG] 方法 findById 耗时: 3ms
    

    关键点Proxy.newProxyInstance 在运行时生成一个实现了指定接口的代理类,所有方法调用都会被转发到 InvocationHandler.invoke。这是 JDK 动态代理,要求目标类必须实现接口。如果目标类没有接口,需要使用 CGLIB(通过继承生成子类代理)。

    Guava 的 Suppliers.memoize 懒加载代理

    Guava 提供了 Suppliers.memoize,本质上是一个缓存代理:第一次调用时执行真实的 Supplier 获取值,后续调用直接返回缓存的结果。

    import com.google.common.base.Supplier;
    import com.google.common.base.Suppliers;
    
    // 模拟一个开销大的初始化操作
    Supplier expensiveInit = new Supplier() {
        @Override
        public DatabaseConnection get() {
            System.out.println("正在建立数据库连接...");
            // 模拟耗时操作
            try { Thread.sleep(3000); } catch (InterruptedException e) {}
            return new DatabaseConnection("jdbc:mysql://localhost:3306/db");
        }
    };
    
    // 用 memoize 包装:第一次调用执行初始化,后续直接返回缓存
    Supplier cached = Suppliers.memoize(expensiveInit);
    
    // 第一次调用:触发初始化,耗时 3 秒
    DatabaseConnection conn1 = cached.get();  // 输出:正在建立数据库连接...
    
    // 第二次调用:直接返回缓存,瞬间完成
    DatabaseConnection conn2 = cached.get();  // 无输出,直接返回
    

    关键点Suppliers.memoize 内部用一个代理 Supplier 持有真实 Supplier 和缓存值,首次调用后将结果缓存,后续调用直接返回缓存。这是虚拟代理 + 缓存代理的结合,synchronized 保证线程安全。

    Nginx 的反向代理

    Nginx 作为反向代理服务器,是代理模式在架构层面的典型应用。客户端请求到达 Nginx,Nginx 将请求转发给后端的真实服务器,然后将响应返回给客户端。客户端只知道 Nginx 的地址,不知道后端服务器的存在。

    # nginx.conf
    http {
        upstream backend {
            server 192.168.1.10:8080 weight=3;  # 后端服务器1
            server 192.168.1.11:8080 weight=1;  # 后端服务器2
        }
    
        server {
            listen 80;
    
            location /api/ {
                proxy_pass http://backend;  # 代理转发
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
            }
        }
    }
    

    关键点:Nginx 代理在转发前后可以做很多事情——负载均衡(选择后端服务器)、权限校验(限制 IP)、缓存(静态资源直接返回)、日志记录(记录请求耗时)。这正是代理模式"控制访问"思想在架构层面的体现。

  • 相关阅读:
    BSA-maltose 牛血清白蛋白修饰麦芽糖 BSA-麦芽糖
    滚珠螺母的生产流程
    【操作系统】32进制小数转10进制
    2.3 初探Hadoop世界
    [Latex] \bibitem{} | .bbl 格式参考文献转换与获得
    4-3:点赞功能
    GZIP文件压缩
    VS2022+opencv4.6.0安装和配置过程
    NFT 的技术结构解释
    【PAT甲级 - C++题解】1063 Set Similarity
  • 原文地址:https://www.cnblogs.com/kfbb/p/20206352