Java 23 种设计模式:从踩坑到精通 | 代理模式 —— 你的 AOP 就是用代理实现的

摘要:当需要在不修改源码的情况下控制对象访问、添加横切逻辑(日志、事务、缓存、权限)时,代理模式是最优雅的解决方案。本文从仓储库存分配的性能优化场景出发,完整讲解静态代理、JDK 动态代理、CGLIB 动态代理三种实现方式,深入剖析 Spring AOP 的代理选择机制(含 Spring Boot 默认策略纠正)与自调用失效陷阱,结合 MyBatis Mapper、RPC 框架、Mockito 等经典应用,帮你彻底掌握“面向切面编程”的基石与避坑指南。

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 上一篇:享元模式 | 当前:代理模式 | 下一篇:责任链模式
🔗 返回系列总目录

关键知识点:

  • 场景驱动:通过“报表缓存”案例,展示如何在不修改源码的情况下控制对象访问。
  • 源码级解析:分析 Spring DefaultAopProxyFactory,澄清 Spring Boot 默认代理策略(proxyTargetClass)的常见误区。
  • 避坑指南:深度讲解 Spring AOP 自调用(Self-Invocation)导致事务失效的原因及三种解决方案(推荐拆分 Service)。
  • 模式辨析:清晰区分代理模式(控制访问)与装饰器模式(增强功能)的意图差异。
  • 实战应用:涵盖 Spring 事务、MyBatis Mapper 代理、RPC 远程调用等真实框架底层原理。

1. 从“每次查询都查全库”的性能灾难说起

假设你在开发一个仓储管理系统,InventoryService 负责分配库存。每次调用 allocateStock(),系统都要走完整的库存查询、锁定、更新流程,高并发下数据库压力巨大。你想加缓存、延迟加载、权限校验……但如果直接在 InventoryService 中写这些逻辑,会导致:

  1. 职责混乱InventoryService 既要处理分配逻辑,又要管理缓存和权限;
  2. 违反开闭原则:切换优化策略(本地缓存 → Redis)必须改核心代码;
  3. 代码冗余:10 个 Service 都要缓存,就得改 10 次。

代理模式(Proxy Pattern)的解决思路是:创建一个“替身”对象,代理对真实对象的访问。替身在调用前后做额外操作,客户端和真实对象都无需感知。

1.1 三种代理速览

代理类型 特点 创建时机 需要接口 性能 代表框架
静态代理 手动编写代理类 编译期 无损耗 早期设计模式教学
JDK 动态代理 反射动态生成 运行时 反射开销(约10~20x) MyBatis Mapper、Spring AOP
CGLIB 动态代理 字节码生成子类 运行时 接近原生(约1.2~2x) Spring AOP、Hibernate

💡 代理模式的核心在于 控制访问(权限、延迟加载、日志),而非单纯的“功能增强”。这一设计天然符合 开闭原则——新增横切逻辑无需修改原类。


2. 模式定义与 UML 结构

代理模式 为其他对象提供一种代理以控制对这个对象的访问。它属于 结构型设计模式

代理模式

三个角色

  • Subject(抽象主题):定义 RealSubjectProxy 的共同接口,体现 依赖倒置原则——客户端只依赖抽象;
  • RealSubject(真实主题):业务逻辑的真正执行者;
  • Proxy(代理):持有 RealSubject 引用,在调用前后添加控制逻辑。代理与真实对象实现同一接口,体现了 里氏替换原则

3. 代码实现:静态代理

3.1 抽象主题

public interface InventoryService {
    boolean allocateStock(String productId, int quantity);
}

3.2 真实主题

public class InventoryServiceImpl implements InventoryService {
    @Override
    public boolean allocateStock(String productId, int quantity) {
        System.out.println("【库存分配】商品 " + productId + " 分配数量 " + quantity);
        try { Thread.sleep(500); } catch (InterruptedException e) {}
        return true;
    }
}

3.3 代理类

public class InventoryServiceProxy implements InventoryService {
    private InventoryService realService;
    private Map<String, Boolean> cache = new HashMap<>();

    public InventoryServiceProxy(InventoryService realService) {
        this.realService = realService;
    }

    @Override
    public boolean allocateStock(String productId, int quantity) {
        String key = productId + "_" + quantity;
        if (cache.containsKey(key)) {
            System.out.println("【静态代理】缓存命中: " + key);
            return cache.get(key);
        }
        System.out.println("【静态代理】缓存未命中,调用真实服务...");
        boolean result = realService.allocateStock(productId, quantity);
        cache.put(key, result);
        return result;
    }
}

代理类只负责缓存,InventoryServiceImpl 只负责业务,单一职责分离清晰。

3.4 客户端

InventoryService service = new InventoryServiceProxy(new InventoryServiceImpl());
service.allocateStock("P1001", 5);  // 缓存未命中
service.allocateStock("P1001", 5);  // 缓存命中

⚠️ 致命缺点:每代理一个类就要手写一个代理类,违反 开闭原则 对扩展的约束。这就是动态代理要解决的问题。


4. 代码实现:JDK 动态代理

JDK 动态代理无需手写代理类,只需实现 InvocationHandler 接口。

4.1 通用代理工厂

public class CacheProxyFactory {
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target, Class<T> interfaceType) {
        return (T) Proxy.newProxyInstance(
                interfaceType.getClassLoader(),
                new Class[]{interfaceType},
                new CacheInvocationHandler(target)
        );
    }

    private static class CacheInvocationHandler implements InvocationHandler {
        private Object target;
        private Map<String, Object> cache = new ConcurrentHashMap<>();

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

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String key = method.getName() + Arrays.toString(args);
            return cache.computeIfAbsent(key, k -> {
                try {
                    System.out.println("【JDK动态代理】缓存未命中,调用真实方法...");
                    return method.invoke(target, args);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}

🔧 invoke() 三个参数:proxy 是生成的代理对象本身,method 是被调用的方法,args 是方法参数。ConcurrentHashMap.computeIfAbsent 保证线程安全的原子操作。

4.2 客户端

InventoryService realService = new InventoryServiceImpl();
InventoryService proxy = CacheProxyFactory.createProxy(realService, InventoryService.class);
proxy.allocateStock("P1001", 5);  // 缓存未命中
proxy.allocateStock("P1001", 5);  // 缓存命中

5. CGLIB 动态代理

当目标类没有实现任何接口时,使用 CGLIB 生成子类代理。

Maven 依赖

<!-- Spring Boot 3.x / Spring 6.x 已内置 CGLIB,无需额外引入 -->
<!-- 若独立使用,需添加: -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

5.1 CGLIB 代理工厂

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class CglibProxyFactory {
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
            System.out.println("【CGLIB代理】方法执行前");
            Object result = proxy.invokeSuper(obj, args);
            System.out.println("【CGLIB代理】方法执行后");
            return result;
        });
        return (T) enhancer.create();
    }
}

⚠️ 限制final 类和方法无法代理。Java 14+ 的 record 类隐式 final,同样无法被 CGLIB 代理。Lombok 的 @Data 需避免将代理目标方法标记为 final


6. 进阶:Spring AOP 源码中的代理机制

@Transactional@Cacheable@PreAuthorize 的背后都是 Spring 通过代理织入横切逻辑。

6.1 Spring 如何选择代理方式?

// Spring 源码简化版:DefaultAopProxyFactory
public AopProxy createAopProxy(AdvisedSupport config) {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);   // JDK 动态代理
        }
        return new ObjenesisCglibAopProxy(config);     // CGLIB
    }
    return new JdkDynamicAopProxy(config);
}

选择规则

环境 默认策略 说明
Spring Framework(原生) JDK 动态代理优先(proxyTargetClass=false 目标类实现接口 → JDK;无接口 → CGLIB
Spring Boot 2.x / 3.x 默认 CGLIBspring.aop.proxy-target-class=true 自动配置强制开启 CGLIB,避免强制转类型时的 ClassCastException
# application.yml 中关闭 CGLIB 强制,回退到 JDK 优先
spring:
  aop:
    proxy-target-class: false

💡 Spring AOP = 代理模式 + 责任链模式。代理负责拦截方法调用,责任链按顺序执行拦截器(事务 → 缓存 → 权限),最后调用目标方法。


7. 实战陷阱:Spring 自调用失效

7.1 失效场景

@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        this.sendNotification(order);  // ⚠️ 自调用:事务失效!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification(Order order) { }
}

失效原因this 指向原始对象,绕过 Spring 容器中的代理对象,AOP 逻辑全部不生效。

7.2 解决方案

方案 写法 优缺点
自我注入 @Autowired private OrderService self; 简单,存在循环依赖风险
AopContext.currentProxy() 配置 exposeProxy=true 需显式配置,侵入性强
拆分 Service(推荐) sendNotification 抽到 NotificationService 符合单一职责,根本解决
// 推荐方案:拆分 Service
@Service
public class OrderService {
    @Autowired private NotificationService notificationService;

    @Transactional
    public void createOrder(Order order) {
        notificationService.sendNotification(order);  // 通过代理调用,事务生效
    }
}

8. 代理模式 vs 装饰器模式

对比维度 代理模式 装饰器模式
目的 控制访问(权限、延迟加载、日志) 增强功能(添加职责)
与被包装对象关系 一对一 可层层包装,任意组合
典型应用 Spring AOP、MyBatis Mapper、RPC Java I/O 流

💡 简单记忆:代理是“中介”(控访问),装饰器是“加料师”(加功能)。核心区别在于意图——代理要“管控”,装饰器要“增强”。


9. 框架与实践中的应用

  • Spring AOP@Transactional@Cacheable 的底层机制;
  • MyBatis Mapper:只定义接口,JDK 动态代理生成实现;
  • RPC 框架:Dubbo、gRPC 通过代理将本地调用转网络请求;
  • 单元测试 Mock:Mockito 使用 Byte Buddy 生成代理对象。

10. 高频面试与避坑指南

💡 面试必背

  • JDK vs CGLIB? → JDK 基于接口+反射,CGLIB 基于继承+字节码。
  • Spring 选择规则? → 原生 Spring 优先 JDK;Spring Boot 默认 CGLIB。
  • 自调用失效?this.xxx() 绕过代理。推荐拆分 Service 解决。
  • MyBatis Mapper 是什么模式? → 代理模式,JDK 动态代理生成接口实现。
  • 代理 vs 装饰器? → 代理控访问,装饰器加功能。Spring AOP 是代理,Java I/O 是装饰器。

❌ 常见误区

  • 动态代理一定比静态好 ❌ → 静态代理无反射损耗,简单场景更优。
  • Spring Boot 默认 JDK 代理 ❌ → Boot 2.x+ 默认 CGLIB。
  • CGLIB 无法代理 record ❌ → 正确,record 隐式 final,必须用 JDK 代理。

11. 总结

最终建议:日常优先用框架代理(Spring AOP);手动控制用 JDK 动态代理,无接口用 CGLIB。注意 Spring Boot 默认 CGLIB 的策略差异,以及自调用陷阱——拆分 Service 是根本解法。面试回答“Spring AOP = 代理 + 责任链 + 避坑自调用”,瞬间脱颖而出。


🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
🚀 下一篇责任链模式 —— 请求流转,审批流程的本质! 已发布,欢迎前去阅读相关设计细节。

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

更多推荐