跳转至

代理模式(Proxy Pattern)

一句话记忆口诀:代理控制访问,静态代理编译期织入,JDK 动态代理基于接口,CGLIB 基于继承,Spring AOP 优先 JDK 代理。


1. 引入:它解决了什么问题?

没有代理模式时的问题

在不修改原始类的前提下,需要为方法添加日志、权限校验、事务管理等横切逻辑时:

// ❌ 反例:直接在业务方法中混入非业务逻辑
public class OrderService {
    public void createOrder(Order order) {
        // 权限校验(非业务逻辑)
        if (!SecurityContext.hasPermission("ORDER_CREATE")) {
            throw new UnauthorizedException();
        }
        // 开启事务(非业务逻辑)
        TransactionManager.begin();
        try {
            // 记录日志(非业务逻辑)
            log.info("开始创建订单: {}", order.getId());

            // ===== 真正的业务逻辑 =====
            orderRepository.save(order);
            inventoryService.deduct(order);
            // ===========================

            log.info("订单创建成功: {}", order.getId());
            TransactionManager.commit();
        } catch (Exception e) {
            TransactionManager.rollback();
            throw e;
        }
    }
    // 每个方法都要重复这些非业务代码!
}

问题根因:业务逻辑与横切关注点(日志、事务、权限)混在一起,违反单一职责原则,且横切逻辑无法复用。

工作中的典型应用场景

场景 Spring/JDK 中的例子
AOP 事务管理 @Transactional — Spring 动态代理
权限控制 @PreAuthorize — Spring Security 代理
缓存 @Cacheable — Spring Cache 代理
远程调用 Feign Client — JDK 动态代理
MyBatis Mapper @Mapper 接口 — JDK 动态代理

2. 类比:用生活模型建立直觉

生活类比:明星经纪人

一位明星(真实对象)只负责表演(核心业务)。经纪人(代理)负责处理合同谈判、档期安排、费用收取(横切逻辑)。粉丝(调用方)联系经纪人,经纪人在适当时候让明星出场。

  • 接口/抽象角色:演出合同(Performer 接口),定义"表演"行为
  • 具体实现角色:明星本人(RealStar),实现真正的表演
  • 代理角色:经纪人(AgentProxy),控制对明星的访问,在表演前后处理杂务
  • 调用方:演出商(Client),只与经纪人打交道

抽象定义

代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。


3. 原理:逐步拆解核心机制

UML 类图

classDiagram
    class Subject {
        <<interface>>
        +request() void
    }
    class RealSubject {
        +request() void
    }
    class Proxy {
        -realSubject RealSubject
        +request() void
        -preProcess() void
        -postProcess() void
    }
    class Client

    Subject <|.. RealSubject
    Subject <|.. Proxy
    Proxy --> RealSubject : delegates
    Client --> Subject
    note for Proxy "代理和真实对象实现同一接口<br/>调用方无感知"

三种代理方式逐一对比

方式一:静态代理(编译期确定)

// ===== 公共接口 =====
public interface OrderService {
    void createOrder(Order order);
    Order getOrder(Long id);
}

// ===== 真实对象(核心业务)=====
public class OrderServiceImpl implements OrderService {
    @Override
    public void createOrder(Order order) {
        System.out.println("创建订单: " + order.getId());
        // 真正的业务逻辑
    }

    @Override
    public Order getOrder(Long id) {
        System.out.println("查询订单: " + id);
        return new Order(id);
    }
}

// ===== 静态代理(编译期确定,手动编写)=====
// 设计原因:在不修改 OrderServiceImpl 的前提下,添加日志功能
// 代价:每个接口都要写一个代理类,接口方法增加时代理类也要同步修改,维护成本高
public class LoggingOrderServiceProxy implements OrderService {
    private final OrderService realService; // 持有真实对象的引用

    public LoggingOrderServiceProxy(OrderService realService) {
        this.realService = realService;
    }

    @Override
    public void createOrder(Order order) {
        System.out.println("[LOG] 开始创建订单");
        long start = System.currentTimeMillis();
        realService.createOrder(order); // 委托给真实对象
        System.out.println("[LOG] 创建订单耗时: " + (System.currentTimeMillis() - start) + "ms");
    }

    @Override
    public Order getOrder(Long id) {
        System.out.println("[LOG] 开始查询订单");
        Order order = realService.getOrder(id);
        System.out.println("[LOG] 查询订单完成");
        return order;
    }
}

⚠️ 静态代理的代价:接口有 N 个方法,代理类就要实现 N 个方法,且每个方法都要写重复的前置/后置逻辑。如果有 100 个 Service 接口,就要写 100 个代理类。

方式二:JDK 动态代理(运行期生成,基于接口)

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

// ===== InvocationHandler:统一处理所有方法调用 =====
// 设计原因:运行期动态生成代理类,一个 Handler 可以代理任意接口
// 代价:被代理类必须实现接口(JDK 动态代理的核心限制)
//       原因:JDK 生成的代理类继承了 Proxy 类,Java 单继承限制导致无法再继承目标类
public class LoggingInvocationHandler implements InvocationHandler {
    private final Object target; // 被代理的真实对象

    public LoggingInvocationHandler(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);

        // 后置处理
        System.out.println("[LOG] 方法耗时: " + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

// ===== 使用 JDK 动态代理 =====
public class Main {
    public static void main(String[] args) {
        OrderService realService = new OrderServiceImpl();

        // Proxy.newProxyInstance 在运行期动态生成代理类
        // 参数1:类加载器(用于加载生成的代理类)
        // 参数2:代理类要实现的接口列表
        // 参数3:InvocationHandler(方法调用的处理器)
        OrderService proxy = (OrderService) Proxy.newProxyInstance(
                realService.getClass().getClassLoader(),
                new Class[]{OrderService.class},
                new LoggingInvocationHandler(realService)
        );

        proxy.createOrder(new Order(1L)); // 实际调用 InvocationHandler.invoke()
        proxy.getOrder(1L);
    }
}

JDK 动态代理的底层原理Proxy.newProxyInstance() 在运行期通过 ProxyGenerator 生成一个继承 Proxy 类并实现目标接口的字节码,加载到 JVM 中。代理类的每个方法都调用 InvocationHandler.invoke(),实现统一拦截。

方式三:CGLIB 动态代理(运行期生成,基于继承)

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

// ===== CGLIB MethodInterceptor =====
// 设计原因:目标类没有实现接口时,通过继承目标类生成子类代理
// 代价:
//   1. 目标类和方法不能是 final(final 类/方法无法被继承/覆盖)
//   2. 需要额外引入 CGLIB 依赖(Spring 已内置)
//   3. 生成子类的开销比 JDK 代理略大,但方法调用性能更好(FastClass 机制)
public class LoggingMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        System.out.println("[CGLIB LOG] 调用方法: " + method.getName());
        long start = System.currentTimeMillis();

        // invokeSuper 调用父类(真实对象)的方法,避免反射开销
        Object result = proxy.invokeSuper(obj, args);

        System.out.println("[CGLIB LOG] 方法耗时: " + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

// ===== 使用 CGLIB 代理 =====
public class Main {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(OrderServiceImpl.class); // 设置父类(被代理类)
        enhancer.setCallback(new LoggingMethodInterceptor());

        // 生成 OrderServiceImpl 的子类实例
        OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create();
        proxy.createOrder(new Order(1L));
    }
}

核心流程图

flowchart TD
    subgraph JDK 动态代理流程
        A1[Client 调用 proxy.method] --> B1[代理类 invoke 方法]
        B1 --> C1[InvocationHandler.invoke]
        C1 --> D1[前置处理]
        D1 --> E1[method.invoke 反射调用真实对象]
        E1 --> F1[后置处理]
        F1 --> G1[返回结果]
    end

    subgraph CGLIB 代理流程
        A2[Client 调用 proxy.method] --> B2[子类覆盖方法]
        B2 --> C2[MethodInterceptor.intercept]
        C2 --> D2[前置处理]
        D2 --> E2[proxy.invokeSuper 调用父类方法]
        E2 --> F2[后置处理]
        F2 --> G2[返回结果]
    end

4. 特性:关键对比

三种代理方式对比

对比维度 静态代理 JDK 动态代理 CGLIB 动态代理
生成时机 编译期 运行期 运行期
实现机制 手动实现接口 实现接口,反射调用 继承目标类,字节码增强
接口要求 需要接口 必须有接口 不需要接口
final 限制 final 类/方法无法代理
性能 最好(直接调用) 较好(JDK 8+ 优化后) 好(FastClass 避免反射)
维护成本 高(每个接口写一个代理类)
Spring 使用 不使用 有接口时优先使用 无接口时使用

代理模式 vs 装饰器模式(最容易混淆)

对比维度 代理模式 装饰器模式
目的 控制对对象的访问(权限、延迟加载) 增强对象的功能
关系 代理通常自己创建真实对象 装饰器由外部传入被装饰对象
透明性 调用方不知道在访问代理 调用方知道在使用装饰器
典型例子 Spring AOP、Feign Client BufferedInputStreamCollections.unmodifiableList()

Spring AOP 代理选择策略

flowchart TD
    A[Spring 创建 Bean 代理] --> B{目标类是否实现接口?}
    B -->|是| C{是否强制使用 CGLIB?}
    C -->|否| D[使用 JDK 动态代理]
    C -->|是 proxyTargetClass=true| E[使用 CGLIB 代理]
    B -->|否| E
    note1["Spring Boot 2.x 默认 proxyTargetClass=true<br/>即默认使用 CGLIB"]

⚠️ Spring Boot 2.x 的变化:Spring Boot 2.0 开始默认将 spring.aop.proxy-target-class=true,即默认使用 CGLIB 代理,即使目标类有接口。原因是避免某些场景下 JDK 代理导致的类型转换异常。

在 Spring / JDK 中的应用

框架/类 代理类型 说明
Spring @Transactional JDK/CGLIB 事务管理 AOP
Spring @Cacheable JDK/CGLIB 缓存 AOP
Feign Client JDK 动态代理 接口生成 HTTP 客户端
MyBatis @Mapper JDK 动态代理 接口生成 SQL 执行器
Collections.synchronizedList() 静态代理 线程安全包装

5. 边界:异常情况与常见误区

误区一:Spring AOP 自调用失效(运行期问题)

// ❌ 错误:同一个类中方法 A 调用方法 B,B 上的 @Transactional 不生效
@Service
public class OrderService {
    public void createOrder(Order order) {
        // 直接调用同类方法,绕过了代理!
        // this.saveOrder() 中的 this 是真实对象,不是代理对象
        this.saveOrder(order);
    }

    @Transactional
    public void saveOrder(Order order) {
        // 事务不会生效!
        orderRepository.save(order);
    }
}

// 原因:Spring AOP 是基于代理的,只有通过代理对象调用方法才会触发拦截。
// 同类内部调用使用 this 引用,绕过了代理对象。

// ✅ 解决方案一:注入自身代理
@Service
public class OrderService {
    @Autowired
    private OrderService self; // 注入自身的代理对象

    public void createOrder(Order order) {
        self.saveOrder(order); // 通过代理调用,事务生效
    }

    @Transactional
    public void saveOrder(Order order) {
        orderRepository.save(order);
    }
}

// ✅ 解决方案二:拆分到不同 Service 类

误区二:CGLIB 代理 final 类/方法(运行期异常)

// ❌ 错误:对 final 类使用 CGLIB 代理
@Service
public final class UserService { // final 类!
    @Transactional
    public void createUser(User user) { ... }
}
// 运行时抛出:Cannot subclass final class UserService
// 原因:CGLIB 通过继承生成子类,final 类无法被继承

// ✅ 正确:去掉 final 修饰符,或改用接口 + JDK 动态代理
public interface UserService {
    void createUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
    @Transactional
    public void createUser(User user) { ... }
}

误区三:JDK 动态代理强转为实现类(运行期 ClassCastException)

// ❌ 错误:将 JDK 动态代理对象强转为实现类
OrderService proxy = (OrderService) Proxy.newProxyInstance(...);
OrderServiceImpl impl = (OrderServiceImpl) proxy; // ClassCastException!

// 原因:JDK 动态代理生成的类继承自 Proxy,实现了 OrderService 接口,
// 但与 OrderServiceImpl 没有继承关系,强转失败

// ✅ 正确:只能转为接口类型
OrderService proxy = (OrderService) Proxy.newProxyInstance(...);
proxy.createOrder(order); // 通过接口调用

6. 总结:面试标准化表达

高频面试题

Q1:JDK 动态代理和 CGLIB 代理有什么区别?Spring 如何选择?

JDK 动态代理基于接口,运行期通过 Proxy.newProxyInstance() 生成实现目标接口的代理类,目标类必须实现接口;CGLIB 基于继承,通过字节码增强生成目标类的子类,不需要接口,但 final 类/方法无法代理。Spring 的选择策略:Spring Boot 2.x 默认使用 CGLIB(proxyTargetClass=true);若显式配置 proxyTargetClass=false,则有接口时用 JDK 代理,无接口时用 CGLIB。

Q2:Spring AOP 为什么会出现事务失效?

Spring AOP 基于代理实现,只有通过代理对象调用方法才会触发拦截逻辑(如事务管理)。当同一个类中方法 A 调用方法 B 时,使用的是 this 引用(真实对象),绕过了代理,导致 B 上的 @Transactional 失效。解决方案:①将方法拆分到不同 Service 类;②通过 ApplicationContext 获取代理对象后调用;③注入自身代理(@Autowired 注入自身)。

Q3:代理模式和装饰器模式有什么区别?

两者结构相似,都持有被包装对象的引用,但目的不同:代理模式的目的是控制访问,代理通常自己创建或持有真实对象,调用方不知道在访问代理(透明代理),典型例子是 Spring AOP、Feign Client;装饰器模式的目的是增强功能,被装饰对象由外部传入,调用方知道在使用装饰器,典型例子是 BufferedInputStream 包装 FileInputStream


一句话记忆口诀:代理控制访问,JDK 代理基于接口(必须有接口),CGLIB 基于继承(不能 final),Spring Boot 2.x 默认 CGLIB,自调用会绕过代理导致 AOP 失效。