Spring AOP:JDK 动态代理 vs CGLIB 的底层对决
一句话结论(30s)
JDK 动态代理与 CGLIB 的本质区别在于「基于接口 + 反射」vs「基于子类继承 + 索引直调」。CGLIB 更快,是因为它用 FastClass 机制按方法 index 直接调用,绕开了 Method.invoke() 的双层反射开销,比 JDK 代理快 2-3 倍。它的代价是不能代理 final 类/方法,因为子类继承要求方法可重写;而 Spring Boot 2.x 默认强制 CGLIB,是因为字段注入 @Autowired UserServiceImpl 需要代理对象是 UserServiceImpl 实例,JDK 代理只实现接口、不是实现类。
思考:JDK 代理和 CGLIB 各自适用什么场景?
判断标准是「有没有接口」和「要不要代理实现类」。类实现了一个清晰的接口时,JDK 动态代理轻量、稳定、JDK 内置零依赖,是更朴素的选择;而遇到「没有接口」「按实现类注入(
@Autowired UserServiceImpl)」或「需要代理具体类的方法」时,JDK 就无能为力了,只能上 CGLIB。反过来,如果目标是final类、final方法或private方法,CGLIB 的「子类继承」机制也代理不了——这时只能靠改造代码(抽接口、去掉final)。
核心原理(2min)
JDK 动态代理生成的 $Proxy0 继承 java.lang.reflect.Proxy 并实现目标接口,每次调用都经过 InvocationHandler.invoke() → method.invoke(target, args) 两层反射。CGLIB 生成的 UserService$$EnhancerByCGLIB 继承目标类,方法调用走 MethodInterceptor.intercept(this, method, args, methodProxy);关键在于它为每个方法生成两个 MethodProxy(一个走拦截器、一个走父类原始方法),并配合 FastClass 索引数组,用 methodProxy.invokeSuper(proxy, args) 按 index 直接调用父类方法,而非反射。这里必须用 invokeSuper 而不是 method.invoke,因为 method.invoke(proxy) 会再次触发代理子类的重写方法导致无限递归,而 invokeSuper 直接调父类原始实现、只织入一次。
底层深入(5-10min)
JDK 动态代理:JdkDynamicAopProxy.invoke
基于接口的代理。JDK 生成的 $Proxy0 继承 java.lang.reflect.Proxy 并实现目标接口,所有方法调用最终都会委托给 JdkDynamicAopProxy(它同时实现了 InvocationHandler)。下面是 Spring spring-aop 模块的真实源码:
// org.springframework.aop.framework.JdkDynamicAopProxy#invoke(真实源码)
@Override
public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
try {
if (!this.cache.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.cache.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
Object retVal;
if (this.advised.isExposeProxy()) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// Check whether we have any advice. If we don't, we can fall back on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
@Nullable Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}
// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and the return type of the method
// is type-compatible. Note that we can't help if the target sets
// a reference to itself in another returned object.
retVal = proxy;
}
else if (retVal == null && returnType != void.class && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
if (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(method)) {
return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ?
CoroutinesUtils.asFlow(retVal) : CoroutinesUtils.awaitSingleOrNull(retVal, args[args.length - 1]);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
这段代码是 JDK 动态代理的「心脏」:proxy 是 JDK 生成的 $Proxy0 实例,method 是接口方法对应的 Method 对象。核心逻辑在 getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)——它取出该方法匹配到的拦截器链,链为空时直接 AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse) 反射调目标方法,链非空时用 ReflectiveMethodInvocation.proceed() 把 advice 串成责任链逐个执行。这印证了 JDK 代理的两大本质:method 只来自接口(只能代理接口方法),且最终落点一定是 Method.invoke 反射调用。
思考:为什么 JDK 动态代理「必须基于接口」,不能直接代理一个类?
因为 JDK 代理的生成原理是:
Proxy.newProxyInstance生成一个实现了目标接口的$Proxy0,它继承的是java.lang.reflect.Proxy。Java 是单继承,$Proxy0已经占用了「继承 Proxy」这个名额,没法再去继承你的目标类,只能通过「实现相同接口」来伪装成目标类型。所以它只能拦截到「接口里声明的方法」,目标类里接口之外的方法(比如具体实现类额外加的方法)一概代理不到。
CGLIB 代理:CglibAopProxy.buildProxy 用 Enhancer 生成子类
基于子类继承。CGLIB 不要求接口,它用 Enhancer 把目标类设为父类,用 ASM 动态生成一个继承目标类的子类字节码。下面是 Spring 真实源码中生成代理的核心方法(getProxy 入口 + buildProxy):
// org.springframework.aop.framework.CglibAopProxy#buildProxy(真实源码,含 getProxy 入口)
@Override
public Object getProxy() {
return buildProxy(null, false);
}
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
return buildProxy(classLoader, false);
}
private Object buildProxy(@Nullable ClassLoader classLoader, boolean classOnly) {
if (logger.isTraceEnabled()) {
logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
}
try {
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");
Class<?> proxySuperClass = rootClass;
if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}
// Validate the class, writing log messages as necessary.
validateClassIfNecessary(proxySuperClass, classLoader);
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader smartClassLoader &&
smartClassLoader.isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts());
enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ?
new ClassLoaderAwareGeneratorStrategy(classLoader) :
new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy)
);
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// fixedInterceptorMap only populated at this point, after getCallbacks call above
ProxyCallbackFilter filter = new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset);
enhancer.setCallbackFilter(filter);
enhancer.setCallbackTypes(types);
// Generate the proxy class and create a proxy instance.
// ProxyCallbackFilter has method introspection capability with Advisor access.
try {
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks));
}
finally {
// Reduce ProxyCallbackFilter to key-only state for its class cache role
// in the CGLIB$CALLBACK_FILTER field, not leaking any Advisor state...
filter.advised.reduceToAdvisorKey();
}
}
catch (CodeGenerationException | IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
": Common causes of this problem include using a final class or a non-visible class",
ex);
}
catch (Throwable ex) {
// TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}
关键在 enhancer.setSuperclass(proxySuperClass):它把目标类设为父类,CGLIB 再用 ASM 生成 UserService$$EnhancerByCGLIB 子类,这就是「基于子类继承」的直接证据。getCallbacks(rootClass) 构建回调数组(DynamicAdvisedInterceptor 排在 AOP_PROXY=0 位),ProxyCallbackFilter 决定每个方法路由到哪个回调。注意 catch 块里的异常提示「final class or a non-visible class」——正因为是子类继承,final 类无法被继承、final 方法无法被重写,所以无法代理。
思考:为什么
final类、final方法、private方法都代理不了?它们卡在哪?根子都在「子类继承」这条机制上:
final类不能被继承,子类根本造不出来;final方法不能被重写,子类无法在方法里织入拦截逻辑;private方法对子类不可见、也无法重写,自然拦截不到。所以 CGLIB 的边界不是「性能」,而是「继承的语法限制」——凡是 Java 不允许你重写的,CGLIB 都代理不了。
CGLIB 方法织入:DynamicAdvisedInterceptor.intercept
被代理的子类方法经 CallbackFilter 路由后,进入 DynamicAdvisedInterceptor.intercept——它是 CGLIB 侧的织入入口,签名里的 methodProxy(不是 Method)正是后面 FastClass 加速的载体:
// org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept(真实源码)
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
private final AdvisedSupport advised;
public DynamicAdvisedInterceptor(AdvisedSupport advised) {
this.advised = advised;
}
@Override
public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.isExposeProxy()) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// Check whether we only have one InvokerInterceptor: that is,
// no real advice, but just reflective invocation of the target.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly.
// Note that the final invoker must be an InvokerInterceptor, so we know
// it does nothing but a reflective operation on the target, and no hot
// swapping or fancy proxying.
@Nullable Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
retVal = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain).proceed();
}
return processReturnType(proxy, target, method, args, retVal);
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
}
对比 JdkDynamicAopProxy.invoke 会发现结构几乎一样——都是「取拦截器链 → 空链直接反射、非空链走 ReflectiveMethodInvocation.proceed()」,因为 AOP 的 advice 织入逻辑是两套代理共用的,真正的区别只在「怎么到达这个方法」。CGLIB 的拦截器链 proceed() 走到末端时调用的是 MethodProxy.invokeSuper,它按 FastClass 索引直接调父类原始方法、绕开反射;而 JDK 代理末端始终是 Method.invoke 反射。intercept 的 methodProxy 参数就是这段「非反射直调」能力的载体。
思考:既然两个
invoke/intercept的结构几乎一样,那 JDK 和 CGLIB 真正的分水岭到底是什么?是「最终这一脚怎么踢到目标方法上」。取拦截器链、织入 advice 这套「前戏」是 Spring AOP 共用的(所以代码长得像),但到达目标方法的「最后一跳」决定了性能与前提条件:JDK 用
Method.invoke反射,代价是慢、收益是不依赖继承、只需接口;CGLIB 用MethodProxy.invokeSuper走 FastClass 索引直调,代价是不能代理 final/private,收益是快。抓住这一点,两套代理的差异就不会再记混。
MethodProxy 为什么比反射快?
CGLIB 为每个被代理方法生成两个 MethodProxy:一个代表子类的重写方法(经过拦截器),一个代表父类的原始方法(不经过拦截器)。调用父类方法时用 methodProxy.invokeSuper(proxy, args)——这不是反射!
FastClass 机制:CGLIB 为代理类和目标类各生成一个 FastClass(索引数组),每个方法有一个固定的 index。invokeSuper(proxy, args) 就是拿到目标类的 FastClass,按 index 直接调用对应方法——没有 Method.invoke() 的反射开销,比 JDK 代理快 2-3 倍。
为什么 invokeSuper 而不是 method.invoke?
// 错误: method.invoke(proxy, args)
// proxy 是代理子类 → 调子类重写后的 sayHello()
// → 子类 sayHello() 内部又调 interceptor.intercept()
// → 无限递归!
// 正确: methodProxy.invokeSuper(proxy, args)
// → 直接调父类 UserService 的原始 sayHello() 实现
// → 无递归, 只织入一次
JDK vs CGLIB 对比
| JDK 动态代理 | CGLIB | |
|---|---|---|
| 生成方式 | Proxy.newProxyInstance() | ASM 生成子类字节码 |
| 前提条件 | 需要接口 | 不需要接口 |
| 限制 | 只能代理接口方法 | final 类/方法不可代理 |
| 方法调用 | 反射 Method.invoke() | FastClass 索引(非反射) |
| 性能 | 慢 2-3x | 快 |
| Spring 2.x 默认 | 是(proxy-target-class=false) | 否 |
Spring Boot 2.x 强制默认 CGLIB——原因是字段注入 @Autowired UserServiceImpl 需要代理对象是 UserServiceImpl 实例(而 JDK 代理只实现了接口,不是实现类)。
思考:实际项目里到底该用哪个?为什么网上会看到「JDK 有接口就用 JDK」的老说法?
老说法对应的是 Spring 2.x/3.x 默认「有接口优先 JDK」的时代,那时 CGLIB 依赖第三方库、启动开销也更大。Spring Boot 2.x 之后默认 CGLIB,主要是因为「字段注入按实现类类型注入」太常见,JDK 代理的类型不匹配。今天的实用结论是:不用刻意选——Boot 默认就是 CGLIB;只有当你明确拿到的是接口引用、且在意代理类数量或想减少字节码生成时,才手动切回 JDK(
spring.aop.proxy-target-class=false)。
代理模式 vs 装饰器模式
结构上几乎一样(都持有目标对象引用 + 实现相同接口),区别在意图:
| 代理 | 装饰器 | |
|---|---|---|
| 目的 | 控制访问 | 扩展功能 |
| 调用方 | 不知情(透明替换) | 知情(显式包装) |
| 实例化 | 代理类隐藏目标 | 调用方创建目标传给装饰器 |
Spring AOP 是代理模式:@Autowired 拿到的就是代理,调用方不知道目标 Bean 被代理了。Java IO 是装饰器模式:new BufferedInputStream(new FileInputStream(...)),调用方知道自己包装了多层。
章末提问
1. JDK 动态代理和 CGLIB 的本质区别是什么?
结论先行:本质是「基于接口 + 反射」对「基于子类继承 + 索引直调」。
因为:JDK 代理生成 $Proxy0 实现目标接口、继承 Proxy(Java 单继承,所以只能伪装成接口类型),每次调用最终走 Method.invoke 反射;CGLIB 用 ASM 生成目标类的子类,配合 FastClass 按方法 index 直调父类方法、绕开反射。由此派生出一整套差异:JDK 必须要有接口、性能慢 2-3 倍;CGLIB 不需要接口、更快,但 final 类/方法、private 方法代理不了。
2. 为什么 CGLIB 比 JDK 代理快?
结论先行:因为 CGLIB 用 FastClass 索引直调绕开了 Method.invoke() 的反射开销。
因为:Method.invoke 每次都要做方法查找、参数装箱、可访问性检查等「反射税」;而 CGLIB 为每个方法预生成固定的 index,调用时 methodProxy.invokeSuper(proxy, args) 直接按 index 定位到父类原始方法执行,省去了反射的那一套运行时开销,所以约快 2-3 倍。
3. 为什么 MethodProxy.invokeSuper 不能换成 method.invoke?
结论先行:换成 method.invoke(proxy, ...) 会死循环,因为 proxy 是代理子类,会再次触发重写方法、再次进入拦截器。
因为:method 对应的是「子类重写后的方法」,对 proxy 调 method.invoke 相当于又调用了一次「被代理过的方法」,而那个方法内部又会 intercept,无限递归。invokeSuper 则是 CGLIB 生成的另一个 MethodProxy,它指向父类原始实现,从拦截器里调它能直接落到目标方法、只织入一次。
4. Spring AOP 是代理模式还是装饰器模式?怎么区分?
结论先行:是代理模式,区分标准看「调用方知不知情」。
因为:代理模式下,@Autowired 拿到的是代理对象,调用方完全不知道自己调的是代理(透明替换),目的是「控制访问」(如加事务、加日志);装饰器模式则是调用方显式地 new BufferedInputStream(new FileInputStream(...)),自己主动一层层包装,目的是「扩展功能」。一句话:调用方不知情的是代理,知情的是装饰器。
5. 哪些方法一定不会被 Spring AOP 代理到?
结论先行:final 方法、private 方法、静态方法、以及「类内部自调用」的方法,都不会被正常织入。
因为:final/private 方法子类无法重写(CGLIB 的机制限制),静态方法属于类不属于实例、没有 this 可代理;「自调用」是 this.method() 直接调目标对象本身,绕过了代理对象,所以切面失效。要补这一刀:自调用可以注入自身代理(或通过 AopContext.currentProxy()),final/private 则只能改代码设计。