Skip to content

代理模式(Proxy Pattern) ​

一句话记忆口诀:代理控制访问,静态代理手动实现,JDK 动态代理基于接口,CGLIB 基于继承,Spring AOP 默认使用 CGLIB。

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

1.1 没有代理模式时的问题 ​

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

java
// ❌ 反例:直接在业务方法中混入非业务逻辑
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;
        }
    }
    // 每个方法都要重复这些非业务代码!
}

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

1.2 工作中的典型应用场景 ​

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

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

2.1 生活类比:明星经纪人 ​

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

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

2.2 抽象定义 ​

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

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

3.1 UML 类图 ​

3.2 三种代理方式逐一对比 ​

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

java
// ===== 公共接口 =====
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 个代理类。

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

java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
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();

        // 通过反射调用真实对象的方法
        // 注意:method.invoke 会把目标方法抛出的异常包装为 InvocationTargetException,
        // 需要解包并重新抛出 cause,否则调用方看到的异常与直接调用目标对象时不同
        Object result;
        try {
            result = method.invoke(target, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }

        // 后置处理:仅在目标方法正常返回后执行
        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() 在运行期动态生成一个继承 Proxy 类并实现目标接口的代理类字节码,加载到 JVM 中。代理类的每个方法都调用 InvocationHandler.invoke(),实现统一拦截。(ProxyGenerator 是 JDK 内部实现细节,不同版本可能变化,不宜作为公共契约依赖)

⚠️ 异常传播边界:即使解包 InvocationTargetException,也不保证与直接调用目标对象完全一致。如果目标方法抛出接口未声明的受检异常,代理调用仍会将其包装为 UndeclaredThrowableException,这是 JDK 动态代理的固有行为,编写横切逻辑时需注意。

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

java
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 代理不同
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();

        // 通过 MethodProxy.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));
    }
}

3.3 核心流程图 ​

4. 特性:关键对比 ​

4.1 三种代理方式对比 ​

对比维度静态代理JDK 动态代理CGLIB 动态代理
生成时机编译期运行期运行期
实现机制手动实现接口实现接口,反射调用继承目标类,字节码增强
接口要求需要接口必须有接口不需要接口
final 限制无无final 类无法代理;final 方法无法覆盖拦截
性能直接方法调用,无运行期生成开销运行期生成代理类,通过反射委托,JDK 8+ 已大幅优化运行期生成子类,通过覆盖方法拦截,开销取决于生成与调用路径
维护成本高(每个接口写一个代理类)低低
Spring 使用不使用有接口时优先使用无接口时使用

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

对比维度代理模式装饰器模式
核心意图控制对对象的访问(权限、延迟加载、远程访问)增强对象的功能(保持接口兼容、组合职责)
典型例子Spring AOP、Feign ClientBufferedInputStream、Collections.unmodifiableList()

两者结构相似,都持有被包装对象的引用。区分的关键是设计意图:代理为对象设置访问控制边界,装饰器为对象附加职责。「对象由谁创建」「调用方是否知情」只是常见实现特征,不能作为稳定的判定标准——代理可以接收外部传入的目标对象,装饰器也可以被透明使用,两者都能多层组合。

4.3 Spring AOP 代理选择策略 ​

⚠️ Spring Boot 默认代理策略:Spring Boot 2.0 起(含 3.x)在自动配置场景下默认将 spring.aop.proxy-target-class=true,即默认使用 CGLIB 代理,即使目标类实现了接口;显式设置 spring.aop.proxy-target-class=false 可改回 JDK 动态代理。注意这是 Spring Boot 自动配置的默认行为,不等于所有 Spring AOP 场景都必然使用 CGLIB,具体仍受配置与代理创建方式影响。

4.4 在 Spring / JDK 中的应用 ​

框架/类典型代理方式说明
Spring @TransactionalJDK/CGLIB事务管理 AOP
Spring @CacheableJDK/CGLIB缓存 AOP
Feign ClientJDK 动态代理接口生成 HTTP 客户端
MyBatis @MapperJDK 动态代理接口生成 SQL 执行器

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

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

java
// ❌ 错误:同一个类中方法 A 调用方法 B,B 上的 @Transactional 不生效
@Service
public class OrderService {
    public void createOrder(Order order) {
        // 直接调用同类方法,绕过了代理!
        // 目标对象方法内部用 this 调用,不会自动绕回外部代理
        this.saveOrder(order);
    }

    @Transactional
    public void saveOrder(Order order) {
        // saveOrder 上的 @Transactional 拦截不会单独触发!
        // (若外层 createOrder 已在事务中,本次操作仍处于该外层事务内)
        orderRepository.save(order);
    }
}

// 原因:Spring AOP 基于代理,只有通过代理对象调用方法才会触发拦截。
// 同类内部用 this 调用绕过了代理,不会单独触发 saveOrder 上的事务拦截;
// 但 saveOrder 的事务行为仍受外层事务影响(若外层已有事务,则并入其中)。

// ✅ 解决方案一:注入自身代理
@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 类

5.2 误区二:CGLIB 代理 final 类(运行期异常) ​

java
// ❌ 错误:对 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) { ... }
}

补充:final 方法不会导致代理创建失败,但该方法无法被子类覆盖,因此对该方法的调用不会被 CGLIB 拦截。

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

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

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

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

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

6.1 高频问题 ​

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

JDK 动态代理基于接口,运行期通过 Proxy.newProxyInstance() 生成实现目标接口的代理类,目标类必须实现接口;CGLIB 基于继承,通过字节码增强生成目标类的子类,不需要接口,但 final 类无法代理、final 方法无法覆盖拦截。Spring 的选择策略:Spring Boot 2.x 及 3.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、Collections.unmodifiableList()。「对象由谁创建」「调用方是否知情」只是常见实现特征,不是稳定的判定标准。

Released under the MIT License.