16、代理模式:静态代理、动态代理(JDK与CGLIB)、AOP原理
代理模式,说白了就是「找人替你办事」。你想想看,生活中我们经常这样——找中介租房、找律师打官司、找经纪人谈代言。代码里也一样,有时候我们不想直接操作某个对象,或者想在操作前后加点「私活」,这时候代理就派上用场了。
我个人习惯把代理模式分成两类:静态代理和动态代理。动态代理里又分JDK代理和CGLIB代理。嗯,咱们一个一个来聊。
16.1 静态代理:最朴素的代理方式
静态代理,就是你在编译期就把代理类写死了。代理类和目标类实现同一个接口,代理类里持有目标类的引用,然后在调用目标方法前后,插入自己的逻辑。
我在项目中遇到过这样一个场景:早期的一个老系统,所有Service层的方法都需要打印日志。如果每个方法都手动加日志代码,那简直是一场灾难。用静态代理,写一个代理类,统一处理日志,代码瞬间清爽了。
来看个例子:
// 接口
public interface UserService {
void addUser(String name);
}
// 目标类
public class UserServiceImpl implements UserService {
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
// 代理类
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
public void addUser(String name) {
System.out.println("[日志] 开始添加用户...");
target.addUser(name);
System.out.println("[日志] 添加用户完成");
}
}
使用的时候:
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("张三");
16.2 动态代理:运行时生成代理
动态代理解决了静态代理的痛点。它不是在编译期写死代理类,而是在运行时动态生成代理对象。Java里主要有两种实现方式:JDK动态代理和CGLIB动态代理。
16.2.1 JDK动态代理
JDK动态代理基于接口实现。它要求目标类必须实现一个接口。代理对象在运行时通过java.lang.reflect.Proxy和InvocationHandler生成。
我曾经接手过一个项目,里面用了大量的静态代理,光代理类就有好几十个。后来我重构时全部换成了JDK动态代理,代码量直接砍掉了一半。嗯,这里要注意:JDK动态代理只支持接口代理,如果你的类没有实现接口,那就得用CGLIB了。
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;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[日志] 方法 " + method.getName() + " 开始执行...");
Object result = method.invoke(target, args);
System.out.println("[日志] 方法 " + method.getName() + " 执行完毕");
return result;
}
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogInvocationHandler(target)
);
}
}
使用:
UserService target = new UserServiceImpl();
UserService proxy = (UserService) LogInvocationHandler.createProxy(target);
proxy.addUser("李四");
16.2.2 CGLIB动态代理
CGLIB(Code Generation Library)通过继承的方式生成代理。它不需要目标类实现接口,直接对类进行代理。CGLIB在运行时生成目标类的子类,并重写父类的方法。
我记得有一次,一个第三方库的类没有实现任何接口,但我们需要给它的方法加缓存。JDK动态代理用不了,静态代理又太麻烦。最后用CGLIB,几行代码就搞定了。
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 LogMethodInterceptor implements MethodInterceptor {
private Object target;
public LogMethodInterceptor(Object target) {
this.target = target;
}
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;
}
public static Object createProxy(Object target) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new LogMethodInterceptor(target));
return enhancer.create();
}
}
使用:
UserService target = new UserServiceImpl();
UserService proxy = (UserService) LogMethodInterceptor.createProxy(target);
proxy.addUser("王五");
final类,也不能代理final方法。因为它是通过继承实现的,final类不能被继承,final方法不能被重写。
16.3 JDK vs CGLIB:怎么选?
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 实现方式 | 基于接口(InvocationHandler + Proxy) | 基于继承(Enhancer + MethodInterceptor) |
| 目标类要求 | 必须实现接口 | 无接口要求,但不能是final类 |
| 性能(创建代理) | 较快 | 较慢(需要生成字节码) |
| 性能(调用方法) | 较慢(反射调用) | 较快(直接调用,无反射) |
| 适用场景 | 面向接口编程 | 类代理、无接口场景 |
我个人习惯:优先用JDK动态代理。因为大部分项目都是面向接口编程的,JDK动态代理足够用了。只有当目标类没有实现接口时,才考虑CGLIB。Spring框架也是这个策略——默认使用JDK动态代理,如果目标类没有实现接口,自动切换为CGLIB。
16.4 AOP原理:代理模式的高级应用
AOP(面向切面编程),说白了就是代理模式在框架层面的封装。你想想看,AOP的核心是什么?就是在不修改原有代码的情况下,给方法增加额外的行为。这不就是代理模式干的事吗?
Spring AOP的底层实现,就是JDK动态代理和CGLIB动态代理的组合。当你定义一个@Aspect切面,配置好@Before、@After等通知时,Spring在运行时为你创建代理对象,把切面逻辑织入到目标方法中。
我曾经排查过一个AOP失效的问题:一个Service方法被同一个类中的另一个方法调用,结果切面没有生效。原因很简单——Spring AOP基于代理,同一个类内部的方法调用走的是this引用,而不是代理对象。所以切面不会触发。解决方案是:要么把方法拆分到不同的Bean中,要么通过AopContext.currentProxy()获取当前代理对象。
- 切面(Aspect):横切关注点的模块化,比如日志、事务
- 通知(Advice):切面的具体行为,比如@Before、@After、@Around
- 切入点(Pointcut):定义在哪些方法上应用通知
来看一个简单的AOP示例:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP日志] 方法 " + methodName + " 即将执行");
}
@AfterReturning("execution(* com.example.service.*.*(..))")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP日志] 方法 " + methodName + " 执行完毕");
}
}
这个切面会拦截com.example.service包下所有类的所有方法,在方法执行前后打印日志。你不需要修改任何业务代码,只需要定义这个切面,Spring会自动为你生成代理对象。
16.5 代理模式的核心知识图谱
下面这张图,帮你理清代理模式的整体脉络:
16.6 避坑指南
最后,分享几个我在实际项目中踩过的坑:
- 内部方法调用导致AOP失效:同一个类中的方法互相调用,不会经过代理对象。解决方案是注入自身或者使用
AopContext.currentProxy()。 - CGLIB代理final方法:CGLIB无法重写final方法,如果目标类有final方法,代理不会生效。我曾经因为这个排查了半天,最后发现是一个工具类的某个方法被标记了final。
- JDK动态代理的类型转换异常:JDK动态代理生成的代理对象只能转换为接口类型,不能转换为具体的实现类。如果你在代码里强转成实现类,会抛出
ClassCastException。 - 性能陷阱:虽然动态代理很方便,但如果你的方法调用频率极高(比如每秒百万次),反射带来的性能损耗还是需要考虑的。这种情况下,可以考虑使用字节码增强技术(如ByteBuddy)或者直接使用静态代理。
代理模式,说白了就是「不直接干活,找个中间人」。静态代理适合小规模场景,动态代理适合大规模、灵活的场景。而AOP,就是代理模式在框架层面的集大成者。理解了代理模式,你就理解了Spring AOP的一半。
公众号:蓝海资料掘金营,微信deep3321