Skip to content
第 34 / 250 章后端⏱ 10 分钟阅读

第 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 靠继承重写方法,privatefinal 方法重写不了,注解自然失效。

坑 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 章:注解

本站基于 VitePress 构建 · 由 Codebook 团队维护