第十四章:代理模式(Proxy)——静态代理、动态代理与Spring AOP

代理模式,说白了就是“找人替你办事”。

我在刚入行那会儿,总觉得这模式有点多余——直接调用不就行了?后来接手一个老项目,日志、事务、权限判断全混在业务代码里,改一个接口要动七八个地方。嗯,那时候我才明白,代理模式真正解决的是控制访问增强功能的问题。

14.1 代理模式的核心思想

代理模式给目标对象提供一个代理对象,由代理对象控制对目标对象的访问。你可以理解为:你(客户端)想找某个明星(目标对象)拍广告,但你不能直接找他,得先找他的经纪人(代理对象)。经纪人可以决定是否让你见明星,也可以在签约前后做一些额外的事情。

核心角色:

  • Subject(抽象主题):定义真实对象和代理对象的共同接口
  • RealSubject(真实主题):真正执行业务逻辑的对象
  • Proxy(代理):持有对真实对象的引用,控制访问并可能增加额外操作

你想想看,如果没有代理模式,你要给每个方法加日志,就得在每个方法里写一遍日志代码。有了代理,你只需要在代理对象里统一处理,真实对象只管业务。

14.2 静态代理

静态代理是最直观的实现方式。代理类在编译期就已经确定,由程序员手动编写。

我记得有一次,团队里有个同事写了一个支付接口,上线后发现需要记录每笔支付的来源IP。他打算在每个支付方法里加一段获取IP的代码。我拦住他,说:“写个代理类吧,别污染业务代码。”

// 抽象主题
public interface PaymentService {
    void pay(String account, double amount);
}

// 真实主题
public class PaymentServiceImpl implements PaymentService {
    @Override
    public void pay(String account, double amount) {
        System.out.println("执行支付:" + account + " 支付 " + amount + " 元");
    }
}

// 静态代理
public class PaymentServiceProxy implements PaymentService {
    private PaymentService target;

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

    @Override
    public void pay(String account, double amount) {
        System.out.println("[日志] 记录支付请求来源IP");
        target.pay(account, amount);
        System.out.println("[日志] 支付完成");
    }
}

// 使用
PaymentService service = new PaymentServiceProxy(new PaymentServiceImpl());
service.pay("张三", 100.0);

静态代理的优缺点:

  • 优点:实现简单,容易理解
  • 缺点:每个目标类都需要一个代理类,接口变化时代理类也要跟着改。如果系统里有几十个接口,你得写几十个代理类——这谁受得了?

14.3 JDK动态代理

静态代理的痛点在于“一个接口一个代理类”。JDK动态代理解决了这个问题——它可以在运行时动态生成代理对象。

JDK动态代理要求目标对象必须实现接口。它通过java.lang.reflect.Proxy类和InvocationHandler接口来实现。

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

public class LogInvocationHandler implements InvocationHandler {
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[JDK动态代理] 方法执行前:" + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("[JDK动态代理] 方法执行后:" + method.getName());
        return result;
    }

    public static Object createProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new LogInvocationHandler(target)
        );
    }
}

// 使用
PaymentService proxy = (PaymentService) LogInvocationHandler.createProxy(new PaymentServiceImpl());
proxy.pay("李四", 200.0);

JDK动态代理的原理:

运行时,JDK会动态生成一个实现了目标接口的匿名类。这个类继承自Proxy,并将所有方法调用转发到InvocationHandler.invoke()方法。说白了,就是“你调用任何方法,我都先拦截一下,再决定怎么处理”。

注意:JDK动态代理只能代理实现了接口的类。如果你的目标类没有实现任何接口,JDK动态代理就无能为力了。我曾经在一个老项目里遇到这个问题——那个项目里的Service类都没有实现接口,全是直接写的实现类。嗯,这时候就得请出CGLIB了。

14.4 CGLIB代理

CGLIB(Code Generation Library)通过继承目标类来创建子类,在子类中覆盖父类方法来实现代理。它不要求目标类实现接口。

我建议你在使用CGLIB时注意一点:目标方法不能是final的,因为CGLIB是通过继承来覆盖方法的,final方法无法被覆盖。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CglibProxy implements MethodInterceptor {
    private Object target;

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

    public Object createProxy() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("[CGLIB代理] 方法执行前:" + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("[CGLIB代理] 方法执行后:" + method.getName());
        return result;
    }
}

// 使用(假设PaymentServiceImpl没有实现接口)
CglibProxy cglibProxy = new CglibProxy(new PaymentServiceImpl());
PaymentServiceImpl proxy = (PaymentServiceImpl) cglibProxy.createProxy();
proxy.pay("王五", 300.0);

JDK动态代理 vs CGLIB代理:

对比维度 JDK动态代理 CGLIB代理
实现方式 基于接口,生成实现类 基于继承,生成子类
目标要求 必须实现接口 不能是final类,方法不能是final
性能 创建代理对象较快,调用稍慢 创建代理对象较慢,调用较快
适用场景 有接口的类 无接口的类

14.5 代理模式在Spring AOP中的应用

Spring AOP(面向切面编程)是代理模式最经典的应用场景。你想想看,Spring AOP是怎么实现事务管理、日志记录、权限校验这些“横切关注点”的?没错,就是靠代理。

Spring AOP默认的策略是:

  • 如果目标对象实现了接口,优先使用JDK动态代理
  • 如果目标对象没有实现接口,使用CGLIB代理

我曾经遇到过一个坑:一个Service类实现了接口,但我在配置AOP时用了@EnableAspectJAutoProxy(proxyTargetClass = true),强制使用CGLIB代理。结果因为那个Service类里有一个final方法,CGLIB无法覆盖,导致AOP对该方法失效。排查了半天才发现是这个问题。

// Spring AOP示例
@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("[AOP] 方法执行前:" + joinPoint.getSignature().getName());
    }

    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("[AOP] 方法执行后:" + joinPoint.getSignature().getName());
    }
}

// 配置
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // ...
}

Spring AOP的代理选择逻辑:

  1. 检查proxyTargetClass属性是否为true,如果是,直接使用CGLIB
  2. 检查目标对象是否实现了接口,如果是,使用JDK动态代理
  3. 如果目标对象没有实现接口,使用CGLIB代理

说白了,Spring AOP就是代理模式的一个“工业化”版本。它把代理的创建、管理、配置都封装好了,你只需要写切面逻辑,剩下的交给Spring容器。

14.6 代理模式的知识体系

代理模式知识体系 代理模式 (Proxy) 静态代理 JDK动态代理 CGLIB代理 特性 • 编译期确定代理类 • 手动编写代理类 • 一个接口一个代理类 • 实现简单,维护成本高 特性 • 运行时动态生成代理 • 基于接口实现 • 通过InvocationHandler • 目标类必须实现接口 特性 • 运行时动态生成代理 • 基于继承实现 • 通过MethodInterceptor • 目标类不能是final 应用:Spring AOP、事务管理、日志、权限

14.7 避坑指南

我总结几个实际项目中容易踩的坑,希望能帮你少走弯路:

我曾经踩过的坑:

  • 自调用问题:在同一个类中,方法A调用方法B,如果方法B有AOP切面,这个切面不会生效。因为代理对象调用的是目标对象的方法,而方法A内部的this指向的是目标对象本身,不是代理对象。解决办法:注入自身代理,或者使用AopContext.currentProxy()
  • final方法无法被代理:CGLIB通过继承覆盖方法,final方法无法被覆盖,所以切面不会生效。JDK动态代理虽然不涉及继承,但如果接口中的方法被声明为default且是final的,同样会有问题。
  • 性能考量:JDK动态代理创建代理对象快,但方法调用稍慢;CGLIB创建代理对象慢,但方法调用快。如果你的代理对象会被频繁创建(比如每次请求都新建),JDK动态代理更合适;如果代理对象是单例的,CGLIB更合适。

嗯,代理模式就讲到这里。记住一句话:代理模式的核心是控制访问和增强功能,而不是替代真实对象。理解了这一点,你就能在合适的场景下用好它。


公众号:蓝海资料掘金营,微信deep3321