代理机制:JDK 动态代理与 CGLIB 选用
引言
Spring AOP、@Transactional、@Async 的背后都是“代理”:调用者拿到的其实是代理对象。Java 有两种造代理的方式——JDK 动态代理(基于接口)与 CGLIB(基于子类),框架按规则自动二选一。理解选用规则,才能解释“为什么强转失败”“为什么 final 方法不生效”。
核心概念
- JDK 动态代理:
java.lang.reflect.Proxy+InvocationHandler,要求目标类实现至少一个接口;生成的代理只实现这些接口,因此只能拦截接口上声明的方法,调用方必须以接口类型持有代理。 - CGLIB:运行时生成目标类的子类并覆写方法,不要求接口;但
final类无法被继承、final方法无法被覆写,也就无法被代理。 - Spring 对 CGLIB 做了重打包(repackaged,位于
org.springframework.cglib,随框架分发,无需另引 cglib 依赖)。 - Spring 的选用规则(DefaultAopProxyFactory,以官方文档为准):目标类实现了接口 → 默认用 JDK 动态代理;未实现接口 → 用 CGLIB;设置
proxyTargetClass=true则强制 CGLIB。 - 配置入口:
@EnableAspectJAutoProxy(proxyTargetClass = true);Spring Boot 默认spring.aop.proxy-target-class=true(即默认 CGLIB,与纯 Framework 默认不同)。 - 影响:JDK 代理下 Bean 的真实类型是接口实现代理类,
@Autowired按接口注入没问题,但若按具体类注入/强转会失败。
代码示例
// JDK 动态代理:手动演示(只认接口)
public interface Greeter { String greet(String name); }
public class JdkProxyDemo {
public static void main(String[] args) {
Greeter target = name -> "Hello " + name;
Greeter proxy = (Greeter) java.lang.reflect.Proxy.newProxyInstance(
Greeter.class.getClassLoader(),
new Class<?>[]{Greeter.class},
(p, method, a) -> {
System.out.println("[proxy] 调用 " + method.getName());
return method.invoke(target, a);
});
System.out.println(proxy.greet("Spring"));
}
}
// CGLIB 风格:不要求接口(Enhancer 由 Spring 重打包版提供),
// 框架内无需手写——@EnableAspectJAutoProxy(proxyTargetClass = true) 即强制
注意点
- 忘写接口时 JDK 代理无法生成,Spring 自动落到 CGLIB,别诧异“为什么这个 Bean 是 CGLIB 代理”。
- CGLIB 下目标类/方法不能是 final;JDK 代理下接口没声明的方法(哪怕实现类有)也不会被拦截。
- 依赖注入一律面向接口或抽象,代理类型差异才不会咬到你。
- 判断当前用的是哪种:打印
bean.getClass()看类名是否含$Proxy(JDK)或EnhancerBySpringCGLIB(CGLIB)。 - 事务/AOP 通知顺序与代理工厂配置相关,改动
proxyTargetClass前先确认切面与注入方式兼容。
小结
JDK 动态代理“认接口”,CGLIB“认类”,Spring 按“有接口默认 JDK、否则 CGLIB、可强制 CGLIB”选用,Boot 默认就是 CGLIB。记住代理的边界(接口方法/final 方法)即可解释大多数“代理不生效”的怪现象。