第 34 章:动态代理
学习目标
- 理解代理模式解决什么问题
- 掌握 JDK 动态代理与 CGLIB
- 理解 Spring AOP 的底层原理
一、为什么需要代理?
需求:给所有 Service 方法加上「执行耗时统计」。
java
// ❌ 每个方法都改:100 个方法改 100 遍,且业务代码被污染
public void createUser(User u) {
long start = System.currentTimeMillis();
// ... 真正的业务逻辑
log.info("耗时 {}ms", System.currentTimeMillis() - start);
}代理的思路:不改原类,在外面套一层。
二、静态代理(先理解思想)
java
public interface UserService {
void createUser(String name);
}
public class UserServiceImpl implements UserService {
@Override
public void createUser(String name) {
System.out.println("创建用户: " + name);
}
}
/** 代理类:与目标类实现同一接口 */
public class UserServiceProxy implements UserService {
private final UserService target; // ① 持有真实对象
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void createUser(String name) {
long start = System.currentTimeMillis(); // ② 前置增强
target.createUser(name); // ③ 调用真实方法
System.out.println("耗时: " + (System.currentTimeMillis() - start)); // ④ 后置
}
}问题:有 100 个接口就要写 100 个代理类。→ 需要动态生成。
三、JDK 动态代理
java
public class TimingHandler implements InvocationHandler {
private final Object target;
public TimingHandler(Object target) {
this.target = target;
}
/**
* ① 代理对象的所有方法调用,都会进到这个 invoke
* @param proxy 代理对象本身(不要在这里调它的方法,会死循环)
* @param method 被调用的方法
* @param args 调用参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(target, args); // ② 反射调用真实方法
System.out.printf("%s 耗时 %dms%n",
method.getName(), System.currentTimeMillis() - start);
return result;
}
}使用:
java
UserService real = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
real.getClass().getClassLoader(), // ① 类加载器
real.getClass().getInterfaces(), // ② 要实现的接口列表
new TimingHandler(real) // ③ 调用处理器
);
proxy.createUser("张三");
// 输出:创建用户: 张三
// createUser 耗时 0ms限制:JDK 动态代理只能代理接口。目标类没实现接口就用不了。
四、CGLIB 动态代理
没有接口时用 CGLIB:运行期生成目标类的子类。
java
public class TimingInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
long start = System.currentTimeMillis();
Object result = proxy.invokeSuper(obj, args); // ① 调用父类(即目标类)方法
System.out.printf("%s 耗时 %dms%n",
method.getName(), System.currentTimeMillis() - start);
return result;
}
}
// 使用
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // ② 设置父类
enhancer.setCallback(new TimingInterceptor());
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();五、JDK vs CGLIB
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | 实现接口 | 继承目标类 |
| 前提 | 目标类必须有接口 | 类不能是 final |
| 无法代理 | 没接口的类 | final 类 / final 方法 / private 方法 |
| 依赖 | JDK 内置 | 第三方库(Spring 已内置) |
| 创建速度 | 快 | 慢(要生成字节码) |
| 调用速度 | JDK 8 后基本持平 | 略快(FastClass 索引,非反射) |
Spring 的选择策略:
Spring Boot 2.x+ 默认
proxyTargetClass=true,一律用 CGLIB,避免「注入接口还是实现类」的困惑。
六、代理带来的经典坑
坑 1:@Transactional 自调用失效
java
@Service
public class OrderService {
public void createOrder() {
this.saveOrder(); // ❌ this 是原始对象,不是代理对象 → 事务不生效
}
@Transactional
public void saveOrder() { }
}为什么? 代理只在外部调用时拦截。this.saveOrder() 是对象内部的普通方法调用,没经过代理。
解决:
java
@Service
public class OrderService {
@Lazy @Autowired
private OrderService self; // ✅ 注入自己的代理
public void createOrder() {
self.saveOrder(); // ✅ 走代理,事务生效
}
}
// 或者:把 saveOrder 拆到另一个 Service 里(更推荐,职责更清晰)坑 2:private / final 方法上加注解无效
CGLIB 靠继承重写方法,private 和 final 方法重写不了,注解自然失效。
坑 3:构造器里拿不到代理增强
java
@Service
public class MyService {
public MyService() {
this.init(); // ❌ 此时代理还没创建,@Transactional 等注解都不生效
}
}
// ✅ 用 @PostConstruct 代替七、本章小结
| 要点 | 关键 |
|---|---|
| 代理模式 | 不改原类,外面套一层增强 |
| 静态代理 | 手写代理类,类爆炸 |
| JDK 代理 | Proxy.newProxyInstance,需接口 |
| CGLIB | 继承生成子类,无需接口 |
| Spring AOP | 默认 CGLIB |
| 自调用失效 | 内部 this.xxx() 不走代理 |
动手练习
练习 1:基础题
用 JDK 动态代理实现一个「方法调用日志打印器」:打印方法名、入参、返回值。
练习 2:进阶题
实现一个简易 @Retry(times=3) 注解 + 动态代理:方法抛异常时自动重试指定次数。
下一章:第 35 章:注解 →