第十四章:代理模式(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的代理选择逻辑:
- 检查
proxyTargetClass属性是否为true,如果是,直接使用CGLIB - 检查目标对象是否实现了接口,如果是,使用JDK动态代理
- 如果目标对象没有实现接口,使用CGLIB代理
说白了,Spring AOP就是代理模式的一个“工业化”版本。它把代理的创建、管理、配置都封装好了,你只需要写切面逻辑,剩下的交给Spring容器。
14.6 代理模式的知识体系
14.7 避坑指南
我总结几个实际项目中容易踩的坑,希望能帮你少走弯路:
我曾经踩过的坑:
- 自调用问题:在同一个类中,方法A调用方法B,如果方法B有AOP切面,这个切面不会生效。因为代理对象调用的是目标对象的方法,而方法A内部的
this指向的是目标对象本身,不是代理对象。解决办法:注入自身代理,或者使用AopContext.currentProxy()。 - final方法无法被代理:CGLIB通过继承覆盖方法,
final方法无法被覆盖,所以切面不会生效。JDK动态代理虽然不涉及继承,但如果接口中的方法被声明为default且是final的,同样会有问题。 - 性能考量:JDK动态代理创建代理对象快,但方法调用稍慢;CGLIB创建代理对象慢,但方法调用快。如果你的代理对象会被频繁创建(比如每次请求都新建),JDK动态代理更合适;如果代理对象是单例的,CGLIB更合适。
嗯,代理模式就讲到这里。记住一句话:代理模式的核心是控制访问和增强功能,而不是替代真实对象。理解了这一点,你就能在合适的场景下用好它。
公众号:蓝海资料掘金营,微信deep3321