本节目标:从
AopProxy接口与DefaultAopProxyFactory.createAopProxy的判定分支出发,讲清 Spring 在什么条件下用 JDK 动态代理、什么条件下用 CGLIB,以及两种代理在字节码层面的差异与硬限制。
适用版本:Spring Boot 4.1.x(Java 21)
3.1 JDK 动态代理与 CGLIB
实战卷回答的是「怎么给一个 Service 加切面」,本节回答「加完切面之后,容器里那个 Bean 到底变成了什么」。这是理解后面两节(切面顺序、代理失效边界)的地基。
先固定一个贯穿本章的场景:一个图书借阅服务 BorrowService,我们要给它加一个审计切面,记录每次借书、还书的入参与耗时。
public interface BorrowService {
BorrowRecord borrow(String userId, String isbn);
void giveBack(String userId, String isbn);
}
@Service
public class BorrowServiceImpl implements BorrowService {
// 省略仓储依赖与实现
}
加了 @Aspect 之后,ctx.getBean(BorrowService.class) 拿到的不再是 BorrowServiceImpl 实例,而是一个代理。这一节就是要看清这个代理是谁、怎么选出来的。
3.1.1 代理从哪来:AopProxy 与两个实现
Spring AOP 的代理创建入口是 org.springframework.aop.framework.AopProxy,接口只有三个方法(javap 于 spring-aop-7.0.9.jar 核实):
public interface org.springframework.aop.framework.AopProxy {
public abstract java.lang.Object getProxy();
public abstract java.lang.Object getProxy(java.lang.ClassLoader);
public abstract java.lang.Class<?> getProxyClass(java.lang.ClassLoader);
}
它有两个实现,都在同一个包下:
| 实现类 | 关键技术 | 声明(javap 核实) |
|---|---|---|
JdkDynamicAopProxy | java.lang.reflect.Proxy | implements AopProxy, InvocationHandler, Serializable |
CglibAopProxy | org.springframework.cglib.proxy.Enhancer | implements AopProxy, Serializable |
两个实现都持有同一个 AdvisedSupport,它保存着目标对象(TargetSource)、拦截器链、以及 proxyTargetClass 等开关。换句话说,选 JDK 还是选 CGLIB 只影响「怎么生成字节码」,不影响「拦截哪些方法、跑哪些 advice」——后者由 AdvisedSupport 统一决定。这是 Spring AOP 把「代理方式」与「通知语义」解耦的关键设计。
JdkDynamicAopProxy 本身就是一个 InvocationHandler,所以它既是「代理工厂」也是「调用处理器」。它生成代理的那行代码(spring-aop-7.0.9-sources.jar 核实):
return Proxy.newProxyInstance(determineClassLoader(classLoader),
this.cache.proxiedInterfaces, this);
注意第三个参数是 this——所以每次接口方法调用都会进到 JdkDynamicAopProxy.invoke(...),由它去查拦截器链。
3.1.2 DefaultAopProxyFactory 的选择规则
真正做选择的是 org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy(AdvisedSupport)。它的判定分支不长,值得逐字读(spring-aop-7.0.9-sources.jar 核实):
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null && config.getProxiedInterfaces().length == 0) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass == null || targetClass.isInterface() ||
Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
把它翻译成判定表:
| 条件 | 结果 |
|---|---|
optimize=true 或 proxyTargetClass=true 或没有用户指定的接口 | 进入「类代理」分支 |
↳ 但目标类型是 null / 接口 / 已是 JDK 代理 / Lambda | 仍退回 JdkDynamicAopProxy |
| ↳ 否则 | ObjenesisCglibAopProxy |
其余(proxyTargetClass=false 且指定了接口) | JdkDynamicAopProxy |
两个容易踩的认知偏差:
- 「有接口就一定走 JDK 代理」是错的。 只有当
proxyTargetClass=false时才成立。一旦proxyTargetClass=true,即使目标实现了接口,也会走 CGLIB 类代理。 - 默认生成的是
ObjenesisCglibAopProxy,不是CglibAopProxy。ObjenesisCglibAopProxy是CglibAopProxy的子类,用 Objenesis 绕过构造器来实例化代理对象。这是很多「讲 AOP」的文章漏掉的细节。
3.1.3 proxyTargetClass 与 Boot 的默认值
proxyTargetClass 定义在 org.springframework.aop.framework.ProxyConfig(javap 核实有 setProxyTargetClass(boolean) / isProxyTargetClass()),AdvisedSupport 继承它。
裸 Spring 的 @EnableAspectJAutoProxy 默认是 proxyTargetClass=false:
// org.springframework.context.annotation.EnableAspectJAutoProxy(spring-context-7.0.9 核实)
boolean proxyTargetClass() default false;
boolean exposeProxy() default false;
但 Spring Boot 把这个默认值反过来了。org.springframework.boot.autoconfigure.aop.AopAutoConfiguration(spring-boot-autoconfigure-4.1.1-sources.jar 核实)里:
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnBooleanProperty(name = "spring.aop.proxy-target-class", matchIfMissing = true)
static class CglibAutoProxyConfiguration { }
matchIfMissing = true 意味着 spring.aop.proxy-target-class 不配也是 true。从 spring-configuration-metadata.json 读到的默认值也印证了这点:
spring.aop.auto default = true "Add @EnableAspectJAutoProxy."
spring.aop.proxy-target-class default = true "Whether subclass-based (CGLIB) proxies ..."
所以结论很明确:Boot 应用默认就是 CGLIB 类代理。这与「有接口走 JDK」的直觉相反,也是为什么在 Boot 里给 @Transactional 方法打断点,看到的是 XxxService$$SpringCGLIB$$0 而不是 $Proxy0。
想改回 JDK 代理,只需一行配置:
spring:
aop:
proxy-target-class: false
反过来,想强制 CGLIB 又不想依赖默认值,可以显式写 spring.aop.proxy-target-class: true,或在自己配置类上加 @EnableAspectJAutoProxy(proxyTargetClass = true)。
3.x 差异:
spring.aop.*两个属性在 3.5.x 与 4.1.x 语义一致。@ConditionalOnBooleanProperty自 3.5 起引入(官方 3.5 Release Notes 明列为新增注解),到 4.1 已广泛用于自动配置,它比@ConditionalOnProperty(havingValue = "true")更直接地表达布尔语义。
3.1.4 CGLIB 已内联进 spring-core
如果你按老资料去 pom.xml 里找 cglib:cglib 依赖,会一无所获——Spring 早已把 CGLIB 的源码重打包进 spring-core。核实方法:
unzip -l ~/.m2/repository/org/springframework/spring-core/7.0.9/spring-core-7.0.9.jar \
| rg 'org/springframework/cglib/proxy/Enhancer.class'
能看到 org/springframework/cglib/proxy/Enhancer.class 与 org/springframework/cglib/core/ReflectUtils.class。也就是说:
- 包名从
net.sf.cglib改成了org.springframework.cglib,避免与用户引入的原版 CGLIB 冲突。 - 你不需要、也不应该再单独引
cglib:cglib或cglib-nodep。 spring-boot-starter-aspectj(4.0 起由spring-boot-starter-aop改名,见官方 4.0 Migration Guide)带来的是 AspectJ 的切面注解与织入支持,不是 CGLIB——CGLIB 一直在spring-core里。
代理类名同样被 Spring 接管。org.springframework.cglib.core.SpringNamingPolicy(spring-core-7.0.9-sources.jar 核实)把标签定死为 $$SpringCGLIB$$:
private static final String SPRING_LABEL = "$$SpringCGLIB$$";
于是 CGLIB 代理的类名形如 com.example.audit.BorrowServiceImpl$$SpringCGLIB$$0。
3.1.5 两种代理的字节码差异与限制
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 生成机制 | Proxy.newProxyInstance 运行时生成实现接口的类 | Enhancer 生成目标类的子类 |
| 类名形态 | jdk.proxy1.$Proxy0(JDK 9+ 动态模块) | XxxServiceImpl$$SpringCGLIB$$0 |
| 继承关系 | 继承 java.lang.reflect.Proxy,实现目标接口 | 继承目标类本身 |
| 能拦截的方法 | 仅接口中声明的方法 | 目标类中可被覆盖的 public / protected 实例方法 |
| 需要接口 | 必须有接口 | 不需要接口 |
| final 类 | 不受影响(不继承目标类) | 无法代理(不能继承 final 类) |
| final 方法 | 接口方法若为 final 也不可代理 | 无法覆盖,静默失效并打 WARN |
| private / static 方法 | 不在接口里,天然不拦截 | 不可覆盖,天然不拦截 |
| 强制转型 | 只能转成接口类型 | 可转成目标类类型 |
| 目标对象 | 可持有一个独立的目标实例 | 代理即子类实例,目标逻辑在父类 |
| 额外依赖 | 无(JDK 自带) | 无(已内联进 spring-core) |
其中「强制转型」这一条最容易被业务代码感知:JDK 代理的实例不能转成 BorrowServiceImpl,只能转成 BorrowService。这也是 Boot 默认改用 CGLIB 的现实理由之一——很多遗留代码直接 @Autowired 具体类,用 JDK 代理会直接启动失败。
3.1.6 怎么亲手观察用的是哪种代理
写一个测试就能看到(示例输出,需你在自己的应用里运行):
@SpringBootTest
class ProxyTypeTest {
@Autowired
private ApplicationContext ctx;
@Test
void printProxyType() {
Object bean = ctx.getBean(BorrowService.class);
System.out.println("class = " + bean.getClass().getName());
System.out.println("superclass = " + bean.getClass().getSuperclass().getName());
System.out.println("isAopProxy = " + AopUtils.isAopProxy(bean));
System.out.println("isJdk = " + AopUtils.isJdkDynamicProxy(bean));
System.out.println("isCglib = " + AopUtils.isCglibProxy(bean));
}
}
默认配置下的示例输出:
class = com.example.audit.BorrowServiceImpl$$SpringCGLIB$$0
superclass = com.example.audit.BorrowServiceImpl
isAopProxy = true
isJdk = false
isCglib = true
把 spring.aop.proxy-target-class 改成 false 后再跑,输出会变成:
class = jdk.proxy1.$Proxy48
superclass = java.lang.reflect.Proxy
isAopProxy = true
isJdk = true
isCglib = false
AopUtils 的三个静态方法(isAopProxy / isJdkDynamicProxy / isCglibProxy,javap 于 spring-aop-7.0.9.jar 核实)是排查「这个 Bean 到底被代理了没有、用哪种代理」的第一把工具,第 3.3 节会反复用到它。
3.1.7 本节要点回顾
- 选代理只影响字节码生成方式,拦截语义由
AdvisedSupport统一决定。 - 判定逻辑集中在
DefaultAopProxyFactory.createAopProxy:optimize/proxyTargetClass/ 无用户接口三者任一为真就走类代理分支,再按目标类型决定是否退回 JDK 代理。 - Boot 默认
spring.aop.proxy-target-class=true,因此默认是 CGLIB;裸 Spring 默认是 JDK 代理。 - CGLIB 已内联进
spring-core的org.springframework.cglib包,无需额外依赖。 AopUtils.isJdkDynamicProxy/isCglibProxy是判断代理类型的标准手段。
小结
- 代理入口是
AopProxy,实现为JdkDynamicAopProxy(Proxy.newProxyInstance+InvocationHandler)与CglibAopProxy(Enhancer生成子类,实际默认子类是ObjenesisCglibAopProxy)。 - 选择规则写在
DefaultAopProxyFactory.createAopProxy:isOptimize() || isProxyTargetClass() || !hasUserSuppliedInterfaces()为真则尝试类代理,目标为接口 / JDK 代理 / Lambda 时再退回 JDK 代理。 proxyTargetClass定义在ProxyConfig,裸 Spring@EnableAspectJAutoProxy默认false,而 Boot 的AopAutoConfiguration用matchIfMissing=true把它默认成true。- CGLIB 源码已重打包进
spring-core的org.springframework.cglib,代理类名带$$SpringCGLIB$$标签。 - JDK 代理只能转成接口类型、只拦截接口方法;CGLIB 能拦截可覆盖的实例方法,但受 final 类 / final 方法 / private / static 限制。
- 判断代理类型用
AopUtils.isJdkDynamicProxy与isCglibProxy。
知道了「代理是谁」,下一步要看「一个切面是怎么变成拦截器链、多个切面谁先谁后」——那是 3.2 节的主题。
阅读导航:上一节:2.3 自动配置的覆盖与排除 · 下一节:3.2 切面织入与顺序 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。