一、为什么需要动态代理
在写业务代码时,经常需要在某个方法执行前后"加料"------比如记录日志、检查权限、开启事务。最笨的办法是写一个静态代理类,手动实现接口的每个方法,在方法里包一层逻辑。但接口一多、方法一多,这种写法就变成了体力活。
动态代理要解决的就是这个问题:不手写代理类,让程序在运行时自动生成。Java 提供了两种主流方案,下面分别梳理完整流程。
二、JDK 动态代理:基于接口的委托机制
完整代码
先看一个最小可运行示例。假设有一个用户服务接口:
java
arduino
public interface UserService {
void addUser(String name);
String getUserInfo(Long id);
}
实现类:
java
typescript
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
@Override
public String getUserInfo(Long id) {
return "用户信息-" + id;
}
}
代理处理器(核心):
java
typescript
public class LogInvocationHandler implements InvocationHandler {
private final Object target; // 被代理的真实对象
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
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;
}
}
使用:
java
scss
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 接口数组
new LogInvocationHandler(target) // 调用处理器
);
proxy.addUser("张三");
// 输出:
// === 方法 addUser 开始 ===
// 添加用户:张三
// === 方法 addUser 结束 ===
代码执行流程
调用 proxy.addUser("张三") 时,实际发生了什么:
第一步:进入代理类的方法。 JVM 运行时生成的代理类(比如 $Proxy0)实现了 UserService 接口,它的 addUser 方法体里没有任何业务逻辑,只有一行代码:调用 InvocationHandler 的 invoke 方法,并把 Method 对象和参数数组传进去。
第二步:invoke 方法被触发。 这就是上面 LogInvocationHandler.invoke 的入口。三个参数的含义:
| 参数 | 含义 |
|---|---|
proxy |
生成的代理对象本身(一般不用) |
method |
被调用的方法,这里是 UserService.addUser |
args |
参数数组,这里是 ["张三"] |
第三步:手动调用真实对象。 method.invoke(target, args) 通过反射在 target(真实的 UserServiceImpl 实例)上执行 addUser。
第四步:返回结果。 真实方法执行的返回值被传回调用方。
为什么必须基于接口
Proxy.newProxyInstance 的内部逻辑是:拿到接口数组,用 ProxyClassFactory 生成一个类,这个类继承 java.lang.reflect.Proxy,同时实现传入的所有接口 。
Java 不支持多继承,代理类已经继承了 Proxy,就不可能再继承你的实现类。所以它只能通过"实现接口"的方式来代理,真实对象必须作为参数注入进来,靠反射调用------这是一种委托机制 。
三、CGLIB 动态代理:基于继承的拦截机制
完整代码
CGLIB 不需要接口,直接代理类。同样是用户服务,这次去掉接口:
java
typescript
public class UserService {
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
public String getUserInfo(Long id) {
return "用户信息-" + id;
}
}
方法拦截器:
java
typescript
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
System.out.println("=== 方法 " + method.getName() + " 开始 ===");
// 注意:这里用 invokeSuper,不是 method.invoke
Object result = proxy.invokeSuper(obj, args);
System.out.println("=== 方法 " + method.getName() + " 结束 ===");
return result;
}
}
使用:
java
scss
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 设置父类
enhancer.setCallback(new LogMethodInterceptor()); // 设置回调
UserService proxy = (UserService) enhancer.create(); // 生成代理对象
proxy.addUser("李四");
// 输出:
// === 方法 addUser 开始 ===
// 添加用户:李四
// === 方法 addUser 结束 ===
代码执行流程
第一步:Enhancer 生成子类。 enhancer.create() 会在运行时创建一个 UserService 的子类,比如 UserService$$EnhancerByCGLIB$$abc123。这个子类重写了父类的所有非 final 方法 。
第二步:调用子类方法。 当你调用 proxy.addUser("李四") 时,实际执行的是子类中重写后的 addUser 方法。
第三步:子类方法委托给拦截器。 重写的方法体内没有业务逻辑,而是调用 MethodInterceptor.intercept(),把当前对象、方法信息、参数、以及一个 MethodProxy 对象传进去。
第四步:invokeSuper 调用父类原始方法。 拦截器里的 proxy.invokeSuper(obj, args) 是关键------它通过 MethodProxy 和 FastClass 机制,直接调用父类(即真实的 UserService)的方法 ,而不是通过反射。
FastClass 解决了什么问题
MethodProxy 背后有一个 FastClass 机制。CGLIB 为每个类生成一个 FastClass 辅助类,里面维护了方法名到索引的映射。调用时通过索引直接定位方法,避免了反射的开销。
为什么不能代理 final 方法
CGLIB 的代理类继承目标类,并重写 方法。final 方法不能被子类重写,所以代理类无法拦截它。调用 final 方法时,会直接执行父类的原始逻辑,拦截器完全不生效 ------这是静默失败,不是报错。
四、两者的核心区别
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 代理方式 | 实现接口 | 继承类,生成子类 |
| 真实对象 | 作为参数注入,靠反射调用 | 代理类是子类,通过 invokeSuper 调用父类 |
| 机制 | 委托 | 继承 |
| 限制 | 必须有接口 | 不能代理 final 类/方法 |
| 常用场景 | Spring AOP 有接口时 | Spring AOP 无接口时 |
性能方面,JDK 8+ 对反射做了大量优化,高频调用下 JDK 代理和 CGLIB 的差距已经很小,业务系统中通常不需要刻意选择。
五、Spring AOP 怎么选
text
ini
目标类有接口 → JDK 动态代理
目标类没有接口 → CGLIB
配置 proxyTargetClass=true → 强制 CGLIB
Spring Boot 2.x 之后,proxyTargetClass 默认就是 true,所以即使类有接口,Spring 也倾向于用 CGLIB。这样代理类不依赖接口,适用范围更广。