设计模式 · 代理模式(Proxy Pattern)java

文章目录

    • 前言
    • 一、核心定义
    • 二、标准体系结构图
    • 三、场景推演
      • [3.1 静态代理](#3.1 静态代理)
      • [3.2 动态代理](#3.2 动态代理)
    • 四、实战案例
      • [4.1 需求分析](#4.1 需求分析)
      • [4.2 架构图](#4.2 架构图)
      • [4.3 类图](#4.3 类图)
      • [4.4 时序图](#4.4 时序图)
      • [4.5 代码分析](#4.5 代码分析)
        • [4.5.1 自定义注解 `@Select`(模拟 MyBatis 的 SQL 注解)](#4.5.1 自定义注解 @Select(模拟 MyBatis 的 SQL 注解))
        • [4.5.2 DAO 接口 `IUserDao`(只定义接口,无实现类)](#4.5.2 DAO 接口 IUserDao(只定义接口,无实现类))
        • [4.5.3 代理工厂 `MapperFactoryBean`(核心:JDK 动态代理)](#4.5.3 代理工厂 MapperFactoryBean(核心:JDK 动态代理))
        • [4.5.4 Bean 注册器 `RegisterBeanFactory`(核心:Spring 扩展点)](#4.5.4 Bean 注册器 RegisterBeanFactory(核心:Spring 扩展点))
        • [4.5.5 Spring 配置文件 `spring-config.xml`](#4.5.5 Spring 配置文件 spring-config.xml)
        • [4.5.6 测试验证](#4.5.6 测试验证)
    • 总结

前言

在技术成长的路上,业务开发中的 CRUD 虽然熟悉,但框架和中间件背后的机制却常常被忽视。

你有没有想过:MyBatis 的 Mapper 接口只有接口定义,没有实现类,调用时却能执行 SQL 并返回数据------这是怎么做到的?

答案就是代理模式(Proxy Pattern)MyBatis-Spring 在启动时扫描 DAO 接口,为每一个接口动态生成一个代理类,将该代理类注册到 Spring 容器中 。调用接口方法时,实际上是代理类拦截了调用,从方法上的 @Select 等注解中读取 SQL,交给 SqlSession 执行,再将结果返回。

本章通过手写一个迷你版 MyBatis-Spring 代理注册流程,来深入理解代理模式的核心思想。


一、核心定义

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

  • 核心思想:在客户端与真实服务对象之间插入一个代理层,由代理负责访问控制、日志、缓存、懒加载等附加功能,而不改变真实对象
  • 解决问题:直接访问真实对象有困难(远程、权限控制、重量级实例创建)或需要在访问前后增加逻辑
  • 代理的三种常见分类
类型 说明 本案例
静态代理 编译期生成代理类,代码量大,扩展性差
JDK 动态代理 运行期通过 Proxy.newProxyInstance() + InvocationHandler 生成 ✅ 本案例使用
CGLIB 代理 运行期通过字节码增强生成子类,可代理无接口的类

二、标准体系结构图

实现
实现
持有引用(委托)
通过接口访问(不感知代理)
<<interface>>
Subject
+request() : void
RealSubject
+request() : void
Proxy
-realSubject: RealSubject
+Proxy(realSubject: RealSubject)
+request() : void
-checkAccess() : void
-logAccess() : void
Client

角色 本案例对应 说明
Subject(抽象主题) IUserDao 接口 定义真实对象和代理对象的公共接口
RealSubject(真实主题) 无(本例没有真实实现类) 在 MyBatis 场景中,真正没有手写实现类
Proxy(代理) MapperFactoryBean 内的 InvocationHandler 运行期动态生成,拦截接口方法调用,执行 SQL
ProxyFactory(代理工厂) MapperFactoryBean + RegisterBeanFactory 负责创建代理对象并将其注册到 Spring 容器
Client(客户端) ApiTest 从 Spring 容器取出 IUserDao 直接调用,不感知代理存在

三、场景推演

3.1 静态代理

场景:支付系统日志统计

需求:

  • 有一个支付服务 PaymentService,提供 pay() 方法。
  • 我们希望在支付操作前后:
    • 打印日志 → 记录支付开始和结束时间
    • 不想修改原支付类的代码

特点:

  • 静态代理需要手动写代理类。
  • 每个真实对象都需要一个对应的代理类。

优点:

  • 简单直观,逻辑清晰
  • 可以控制访问、增加额外逻辑

缺点:

  • 每个真实对象都要写一个代理类 → 代码重复
  • 扩展性差,维护成本高

使用场景:

  • 对象固定、代理逻辑简单、不会频繁改变

代码实现:

java 复制代码
// 1. 抽象接口
public interface PaymentService {
    void pay(double amount);
}

// 2. 真实对象
public class RealPaymentService implements PaymentService {
    @Override
    public void pay(double amount) {
        System.out.println("Processing payment of $" + amount);
    }
}

// 3. 静态代理对象
public class PaymentServiceProxy implements PaymentService {
    private PaymentService realService;

    public PaymentServiceProxy(PaymentService realService) {
        this.realService = realService;
    }

    @Override
    public void pay(double amount) {
        // 代理增强逻辑:日志
        System.out.println("Log: Payment start");
        realService.pay(amount);
        System.out.println("Log: Payment end");
    }
}

// 4. 客户端调用
public class Client {
    public static void main(String[] args) {
        PaymentService real = new RealPaymentService();
        PaymentService proxy = new PaymentServiceProxy(real);

        proxy.pay(100);
    }
}

3.2 动态代理

场景:接口方法执行耗时统计

需求:

  • 有一个接口 Calculator 提供 addsubtract 方法。
  • 希望在方法调用前后:
    • 统计执行耗时
    • 不想写重复的代理类 → 所有接口实现对象都可动态代理

特点:

  • 动态代理基于接口(JDK Proxy)或类(Cglib)
  • 在运行时生成代理对象,不需要手写

优点:

  • 无需手写代理类,统一代理处理
  • 易于扩展 → 任何实现接口的对象都可代理
  • 可插入日志、权限、缓存、性能统计等

缺点:

  • 只能代理接口(JDK Proxy)
  • 性能略低于静态代理(反射调用)

使用场景:

  • AOP(切面编程) → Spring 就是基于动态代理
  • 日志、权限、缓存、事务控制
  • 对象数量多或变化频繁时特别适合

代码实现:

java 复制代码
import java.lang.reflect.*;

// 1. 接口
public interface Calculator {
    int add(int a, int b);
    int subtract(int a, int b);
}

// 2. 真实对象
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) { return a + b; }

    @Override
    public int subtract(int a, int b) { return a - b; }
}

// 3. 动态代理调用处理器
public class TimeInvocationHandler implements InvocationHandler {
    private Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("Method " + method.getName() + " start");
        Object result = method.invoke(target, args);
        long end = System.currentTimeMillis();
        System.out.println("Method " + method.getName() + " end, elapsed: " + (end - start) + "ms");
        return result;
    }
}

// 4. 客户端调用
public class Client {
    public static void main(String[] args) {
        Calculator calc = new CalculatorImpl();

        Calculator proxy = (Calculator) Proxy.newProxyInstance(
            calc.getClass().getClassLoader(),
            calc.getClass().getInterfaces(),
            new TimeInvocationHandler(calc)
        );

        proxy.add(3, 5);
        proxy.subtract(10, 4);
    }
}

四、实战案例

4.1 需求分析

问题:MyBatis 的 Mapper 接口如何无需实现类就能执行 SQL?

复制代码
普通接口调用链(需要实现类):
Client → IUserDao(接口)→ UserDaoImpl(实现类,手写 JDBC)→ DB

MyBatis 代理调用链(无需实现类):
Client → IUserDao(接口)
              ↓(Spring 容器注入的其实是代理对象)
         $Proxy0(JDK动态生成)→ InvocationHandler.invoke()
              ↓
         读取 @Select("select userName from user where id = #{uId}")
              ↓(MyBatis 中真实流程)
         SqlSession.selectOne(sql, args) → DB → 返回结果

本案例模拟的核心步骤:

  1. 定义接口 IUserDao,方法上加 @Select(sql)
  2. MapperFactoryBean 实现 FactoryBean<T>
    getObject() 中用 Proxy.newProxyInstance() 生成代理
    InvocationHandler 读取 @Select 注解的 SQL,模拟执行
  3. RegisterBeanFactory 实现 BeanDefinitionRegistryPostProcessor
    Spring 启动时,将 MapperFactoryBean 包装的 bean
    以 "userDao" 为名注册到 Spring 容器
  4. 调用方通过 beanFactory.getBean("userDao", IUserDao.class)
    拿到的是代理对象,调用 queryUserInfo() 触发 invoke()

原因:代理模式在此处是一种框架/中间件级别的能力 ,其对比的不是"坏代码 vs 好代码",而是展示了一种"没有代理机制时必须手写大量重复 DAO 实现类 "与"有代理机制后只需定义接口"的本质差异。

本案例实现的等效功能:

对比维度 传统 JDBC 方式 本案例代理方式
需要手写实现类? 是,每个 DAO 都要写 XxxDaoImpl 否,只定义接口
SQL 放在哪里? 硬编码在实现类中 @Select 注解在方法上
Spring 注入方式 直接注入实现类 bean 代理工厂动态生成代理对象注入
扩展新 DAO 接口 写新实现类 + 配置 bean 只需定义接口 + 加注解

4.2 架构图


4.3 类图

方法上标注
mapperInterface 持有接口类
getObject() 内创建
从 method 读取注解
定义并注册到 Spring
beanFactory.getBean() 取代理对象调用
<<注解 @interface>>
Select
+value() : String
<<interface>>
IUserDao
+queryUserInfo(uId: String) : String
<<代理工厂 FactoryBean>>
MapperFactoryBean<T>
-logger: Logger
-mapperInterface: Class<T>
+MapperFactoryBean(mapperInterface: Class<T>)
+getObject() : T
+getObjectType() : Class<?>
+isSingleton() : boolean
<<Bean注册器 BeanDefinitionRegistryPostProcessor>>
RegisterBeanFactory
+postProcessBeanDefinitionRegistry(registry: BeanDefinitionRegistry) : void
+postProcessBeanFactory(factory: ConfigurableListableBeanFactory) : void
<<JDK代理核心>>
InvocationHandler
+invoke(proxy, method, args) : Object
ApiTest
+test_IUserDao() : void
FactoryBean 的 getObject() 返回的是\nProxy.newProxyInstance() 生成的动态代理对象\n而不是 MapperFactoryBean 本身
Spring 启动时调用\npostProcessBeanDefinitionRegistry()\n将代理 bean 以 'userDao' 名注册到容器


4.4 时序图

ApiTest Proxy0(JDK动态代理) MapperFactoryBean RegisterBeanFactory Spring容器启动 ApiTest Proxy0(JDK动态代理) MapperFactoryBean RegisterBeanFactory Spring容器启动 Spring 启动,加载 spring-config.xml 创建 GenericBeanDefinition setBeanClass(MapperFactoryBean.class) addConstructorArg(IUserDao.class) "userDao" bean 定义注册完成 Spring 初始化 bean 创建 InvocationHandler 调用 Proxy.newProxyInstance() "userDao" bean = Proxy0,容器初始化完成 InvocationHandler.invoke() 被触发 Select select = method.getAnnotation(Select.class) select.value() = "select userName from user where id = logger.info("SQL: {}", "select userName from user where id = 100001") postProcessBeanDefinitionRegistry(registry) registerBeanDefinition("userDao", beanDef) new MapperFactoryBean(IUserDao.class) getObject()(FactoryBean 接口) 返回 Proxy0(IUserDao 的代理实例) beanFactory.getBean("userDao", IUserDao.class) 返回 $Proxy0(客户端看到的是 IUserDao 接口) userDao.queryUserInfo("100001") "100001 小傅哥,沉淀、分享、成长..."


4.5 代码分析

4.5.1 自定义注解 @Select(模拟 MyBatis 的 SQL 注解)
java 复制代码
// agent/Select.java
@Documented
@Retention(RetentionPolicy.RUNTIME)   // 运行期保留,否则代理中无法通过反射读取
@Target({ElementType.METHOD})         // 作用于方法级别
public @interface Select {
    String value() default "";        // 存储 SQL 语句
}

4.5.2 DAO 接口 IUserDao(只定义接口,无实现类)
java 复制代码
// IUserDao.java
public interface IUserDao {
    // 用注解指定 SQL,MyBatis 风格:#{uId} 是占位符
    @Select("select userName from user where id = #{uId}")
    String queryUserInfo(String uId);
}

关键点:这里只有接口,没有任何实现类。调用时能工作,完全依赖代理机制。


4.5.3 代理工厂 MapperFactoryBean(核心:JDK 动态代理)
java 复制代码
// agent/MapperFactoryBean.java
public class MapperFactoryBean<T> implements FactoryBean<T> {

    private Class<T> mapperInterface;  // 被代理的接口 Class 对象

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;  // 通过构造函数接收接口类型
    }

    @Override
    public T getObject() throws Exception {
        // ① InvocationHandler:所有接口方法调用都会路由到这里
         // proxy:代理对象本身
		// method:当前被调用的方法,比如 queryUserInfo
		// args:方法参数数组,比如 ["100001"]
        InvocationHandler handler = (proxy, method, args) -> {
            // ② 从方法上读取 @Select 注解,获取 SQL 语句
            Select select = method.getAnnotation(Select.class);
            // ③ 模拟 SQL 执行(真实 MyBatis 中会调用 SqlSession 执行 SQL)
            logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
            // ④ 模拟返回结果
            return args[0] + " 代理模式测试";
        };
        // ⑤ JDK 动态代理:生成实现了 mapperInterface 的代理对象
        return (T) Proxy.newProxyInstance(
            this.getClass().getClassLoader(),
            new Class[]{mapperInterface},
            handler
        );
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;  // 告诉 Spring 这个 FactoryBean 产出的 bean 类型
    }

    @Override
    public boolean isSingleton() {
        return false;  // 代理对象非单例(每次 getObject() 生成新代理)
    }
}

FactoryBean<T> 的特殊性

当 Spring 容器中有一个 bean 实现了 FactoryBean<T> 接口时,beanFactory.getBean("userDao") 返回的不是 MapperFactoryBean 的实例,而是 getObject() 的返回值------即代理对象。这是 Spring 中实现代理注入的关键机制,MyBatis-Spring 的 MapperFactoryBean 正是同名同原理。


4.5.4 Bean 注册器 RegisterBeanFactory(核心:Spring 扩展点)
java 复制代码
// agent/RegisterBeanFactory.java
public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // ① 创建 bean 定义:指定产生此 bean 的工厂类为 MapperFactoryBean
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setScope("singleton");
        // ② 为 MapperFactoryBean 的构造函数传入 IUserDao.class
        //    相当于:new MapperFactoryBean(IUserDao.class)
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);

        // ③ 创建 BeanDefinitionHolder,指定 bean 在容器中的名称为 "userDao"
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        // ④ 注册到 DefaultListableBeanFactory(Spring 默认 bean 注册中心)
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
        // 此处留空,本案例不需要在此扩展点做额外处理
    }
}

三个关键 Spring API:

API 职责
BeanDefinitionRegistryPostProcessor Spring 扩展点接口,在所有 bean 定义加载后、bean 初始化前回调,可在此动态注册新的 bean 定义
GenericBeanDefinition 描述一个 bean 的元信息(用哪个类、什么作用域、构造参数等)
BeanDefinitionReaderUtils.registerBeanDefinition 将 bean 定义写入 BeanDefinitionRegistry(即 DefaultListableBeanFactory

4.5.5 Spring 配置文件 spring-config.xml
xml 复制代码
<!-- spring-config.xml -->
<beans ...>
    <!-- 只需要配置 RegisterBeanFactory,它会在启动时自动注册代理 bean -->
    <bean id="userDao" class="com.likerhood.design.agent.RegisterBeanFactory"/>
</beans>

注意 :这里配置的 bean class 是 RegisterBeanFactory(注册器),而不是 IUserDaoMapperFactoryBeanRegisterBeanFactory 实现了 BeanDefinitionRegistryPostProcessor,Spring 会在容器启动阶段特殊处理它,调用其 postProcessBeanDefinitionRegistry() 方法动态注册 userDao 代理 bean。


4.5.6 测试验证
java 复制代码
// ApiTest.java
@Test
public void test_IUserDao() {
    // ① 加载 spring-config.xml,触发 Spring 容器初始化
    //    → RegisterBeanFactory.postProcessBeanDefinitionRegistry() 被调用
    //    → MapperFactoryBean.getObject() 被调用
    //    → "userDao" = $Proxy0(IUserDao 的代理对象)注册完成
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");

    // ② 从容器取 "userDao"(取到的是代理对象,不是 MapperFactoryBean)
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);

    // ③ 调用接口方法(实际进入 InvocationHandler.invoke())
    String res = userDao.queryUserInfo("100001");
    logger.info("测试结果:{}", res);
}

// 测试输出:
// INFO  MapperFactoryBean - SQL:select userName from user where id = 100001
// INFO  ApiTest          - 测试结果:100001 小傅哥,沉淀、分享、成长,让自己和他人都能有所收获!

总结

维度 传统 DAO 实现 代理模式(本案例)
实现类 每个接口必须手写 XxxDaoImpl 无需实现类,代理自动生成
SQL 管理 硬编码在实现类中 通过 @Select 注解集中管理
Spring 集成 直接注册实现类 bean FactoryBean + BeanDefinitionRegistryPostProcessor 动态注册
扩展新接口 写新实现类、配置新 bean 定义接口、加注解,框架自动处理
代码体积 随接口数量线性增长 框架代码固定,接口数量增加不增加框架代码

代理模式的关键实现要点:

  1. JDK 动态代理三要素:ClassLoader(指定代理类的类加载器)、Class[](代理要实现的接口列表)、InvocationHandler(所有方法调用的拦截器)------缺一不可。
  2. FactoryBean 的间接性 :Spring 中 FactoryBean.getObject() 返回的才是真正注入的对象,这是代理模式与 Spring 结合的核心"技巧",MyBatis-Spring、Dubbo 的 RPC 代理等都用了这个机制。
  3. BeanDefinitionRegistryPostProcessor 扩展点 :这是框架动态注册 bean 的正规入口,比 XML 配置更灵活,批量注册时尤为强大(MyBatis-Spring 的 @MapperScan 底层正是扫描包内所有接口,循环注册每个 MapperFactoryBean)。

代理模式的适用场景:

场景 说明
MyBatis Mapper 接口代理,拦截方法调用执行 SQL
RPC 框架(Dubbo/gRPC) 接口代理,拦截调用发起远程网络请求
Spring AOP CGLIB/JDK 代理,切面拦截方法调用插入逻辑
权限控制代理 在调用前检查权限,通过再转发给真实对象
缓存代理 在调用前查缓存,未命中再转发并写缓存
延迟加载代理 对象使用时才真正初始化(Hibernate 懒加载)

与其他模式对比:

  • 代理模式 vs 装饰器模式 :代理控制对目标对象的访问 (可以拒绝、缓存、远程化),装饰器为目标对象添加功能。代理通常自己管理目标对象的生命周期,装饰器中目标对象由调用方传入
  • 代理模式 vs 适配器模式:适配器改变接口,代理不改变接口(接口完全相同,只是在调用链上插入一层)
  • 代理模式 vs 外观模式:外观对外提供简化接口(多个子系统 → 一个接口),代理是对单一对象的封装(一个接口 → 同一接口,透明)
相关推荐
東雪木13 小时前
Java 基础语法与核心数据类型 专属复习笔记
java·开发语言·笔记·java面试
转型AI的宏达13 小时前
解除autoclaw白名单审批机制
java·服务器·前端
ch.ju13 小时前
Java程序设计(第3版)第四章——方法的重载
java·开发语言
ch.ju13 小时前
Java Programming Chapter 4——Overloading of method
java·开发语言
dulu~dulu13 小时前
大模型---工具调用
java·服务器·前端
过期动态13 小时前
【RabbitMQ高级篇】生产者可靠性、MQ可靠性、消费者可靠性以及延迟队列的实现
java·数据结构·分布式·算法·rabbitmq·ruby
2401_8332693013 小时前
Java异常处理入门
java·开发语言
憧憬成为java架构高手的小白13 小时前
苍穹外卖--day07(缓存商品,购物车)
java·spring boot
观无13 小时前
若依框架在window的打包部署
java