核心原理 ------ 并不是"反射",而是"继承"
1.1 本质区别
- JDK 动态代理 :基于组合 。生成一个实现了接口的
$Proxy类,持有目标对象,通过反射转发调用。- 限制:必须有接口;只能代理接口方法。
- CGLIB :基于继承 。在运行时动态生成一个目标类的子类 (
Target$$EnhancerBySpringCGLIB$$...)。- 优势 :不需要接口;可以代理类方法(只要不是
final)。 - 限制 :不能代理
final类或final方法(因为无法继承/重写);构造函数无法拦截。
- 优势 :不需要接口;可以代理类方法(只要不是
| 角色 | 英文 | 作用 | 生活化比喻 |
|---|---|---|---|
| Enhancer | net.sf.cglib.proxy.Enhancer |
生成器。负责配置父类、接口、拦截器,并生成字节码。 | 克隆工厂。你给它一张照片(目标类),它给你造出一个克隆人。 |
| MethodInterceptor | net.sf.cglib.proxy.MethodInterceptor |
拦截器。核心接口,拦截所有非 final 方法的调用。 |
克隆人的管家。克隆人想做任何事(调用方法),必须先问管家。 |
| MethodProxy | net.sf.cglib.proxy.MethodProxy |
快速调用器。比 JDK 反射快,利用字节码索引直接调用。 | 快速通道。管家不用查电话簿(反射),直接按快捷键呼叫本体。 |
| CallbackFilter | net.sf.cglib.proxy.CallbackFilter |
过滤器。决定哪个方法用哪个拦截器(Spring 内部常用)。 | 任务分配员。决定"吃饭"找管家 A,"睡觉"找管家 B。 |
源码级还原 ------ Spring 是如何生成 CGLIB 代理的
Spring 并没有直接使用原始的 CGLIB API,而是封装在 ObjenesisCglibAopProxy 中。但核心逻辑依然清晰。
2.1 代理生成的"四步走"战略
当 Spring 决定使用 CGLIB 时,内部大致执行了以下逻辑(简化伪代码):
// org.springframework.aop.framework.CglibAopProxy (简化版逻辑)
public Object getProxy(ClassLoader classLoader) {
// 1. 创建 Enhancer (生成器)
Enhancer enhancer = new Enhancer();
// 2. 设置父类 (Superclass) -> 这是关键!
enhancer.setSuperclass(targetClass);
// 如果目标类实现了接口,也设置进去
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(advised));
// 3. 设置回调 (Callback) -> 核心拦截逻辑
// Spring 这里用的是 DynamicAdvisedInterceptor,它实现了 MethodInterceptor
Callback callback = new DynamicAdvisedInterceptor(advised);
// 【高阶技巧】Spring 为了性能,不会对所有方法都用同一个拦截器
// 它会创建一个 Callback[] 数组,配合 CallbackFilter 使用
Callback[] callbacks = new Callback[] { callback, NoOp.INSTANCE };
enhancer.setCallbacks(callbacks);
// 设置过滤器:决定哪些方法需要拦截,哪些直接放行 (NoOp)
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(),
this.fixedInterceptorMap,
this.fixedInterceptorOffset
));
// 4. 生成类并实例化
// create() 方法会生成字节码,加载到 JVM,并调用默认构造函数
return enhancer.create();
}
2.2 生成的字节码长什么样?
public class UserService {
public void login() { System.out.println("Login"); }
public final void logout() { System.out.println("Logout"); } // final 方法
}
CGLIB 生成的类大致如下(反编译后):
// 类名:UserService$$EnhancerBySpringCGLIB$$123456
public class UserService$$EnhancerBySpringCGLIB$$123456 extends UserService {
// 缓存 MethodProxy 对象,用于快速调用父类方法
private static final MethodProxy METHOD_PROXY_LOGIN;
// 存储回调拦截器数组
private Callback[] CGLIB$CALLBACKS;
static {
// 初始化 MethodProxy,绑定父类方法签名
METHOD_PROXY_LOGIN = MethodProxy.create(..., "login", ...);
}
// 重写的 login 方法
public final void login() {
// 1. 检查是否开启了拦截
if (this.CGLIB$CALLBACKS[0] == null) {
// 没开启,直接调用父类 (超高速路径)
super.login();
return;
}
// 2. 获取拦截器 (DynamicAdvisedInterceptor)
MethodInterceptor interceptor = (MethodInterceptor) this.CGLIB$CALLBACKS[0];
// 3. 执行拦截逻辑
// 注意:这里传入的是 MethodProxy,而不是 java.lang.reflect.Method
interceptor.intercept(this, METHOD_PROXY_LOGIN, new Object[]{}, METHOD_PROXY_LOGIN);
}
// final 方法 logout 不会被重写!直接继承父类实现
// public final void logout() { ... } (来自父类)
}
extends UserService:它是真正的子类,拥有父类的所有非final属性和方法。MethodProxyvsMethod:- JDK 代理用
java.lang.reflect.Method.invoke(),需要反射查找,慢。 - CGLIB 用
MethodProxy.invokeSuper(),它内部维护了一个索引(Index) ,直接通过字节码指令调用父类方法,无需反射,速度极快(接近原生调用)。
- JDK 代理用
final方法失效 :因为 Java 禁止重写final方法,所以 CGLIB 生成的子类里根本没有重写logout()。调用logout()时,直接执行父类逻辑,完全绕过拦截器 。这就是为什么@Transactional在final方法上无效的原因。
第三章:拦截器的核心 ------ DynamicAdvisedInterceptor
Spring 的 CGLIB 拦截器并不是直接写死的,而是一个通用的 DynamicAdvisedInterceptor。它负责桥接 CGLIB 和 Spring 的 AOP 链。
// org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
private final Advised advised; // 包含目标对象、拦截器链等配置
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 1. 获取目标对象 (可能是原始对象,也可能是另一个代理)
Object target = advised.getTargetSource().getTarget();
// 2. 获取拦截器链 (和 JDK 代理逻辑一样,都是 List<Interceptor>)
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
if (chain.isEmpty()) {
// 【优化点】如果没有拦截器,直接用 MethodProxy 调用父类方法 (最快)
// 这比反射快得多
retVal = methodProxy.invoke(target, args);
} else {
// 3. 如果有拦截器,封装成 MethodInvocation (和 JDK 代理一样的递归逻辑)
// 注意:这里传入的是 methodProxy,用于后续 invokeJoinpoint 时加速
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
// 4. 处理返回值 (如果是 primitive 类型且返回 null 的特殊处理等)
retVal = processReturnType(retVal, target, method);
return retVal;
}
}
- 统一模型 :你会发现,无论是 JDK 还是 CGLIB,最终都汇聚到了
MethodInvocation.proceed()这个递归模型上。Spring AOP 的核心灵魂是"拦截器链",而不是底层的代理技术。 - 性能优化 :
if (chain.isEmpty())这个判断非常关键。如果一个方法没有任何切面(如toString()或普通 getter),CGLIB 会直接调用methodProxy.invokeSuper(),几乎零开销。
第四章:CGLIB 的独门绝技 ------ FastClass 机制
这是 CGLIB 比 JDK 反射快的核心秘密
4.1 什么是 FastClass?
在传统的反射中,调用方法需要:
getMethod()(查找方法,慢)invoke()(安全检查 + 参数转换 + 调用,慢)
CGLIB 生成的 MethodProxy 内部持有一个 FastClass 对象。
FastClass也是一个动态生成的类。- 它内部有一个巨大的
switch-case语句(或查找表),根据方法的**签名索引(Signature Index)**直接调用目标方法。
伪代码示意 FastClass 内部逻辑:
// 生成的 FastClass 伪代码
public class UserService$$FastClassByCGLIB$$999 {
// 根据方法签名获取索引
public int getIndex(String name, Class[] types) {
if (name.equals("login") && types.length == 0) return 1;
if (name.equals("save") && types.length == 1) return 2;
return -1;
}
// 核心加速方法:直接 switch 调用
public Object invoke(int index, Object obj, Object[] args) {
UserService target = (UserService) obj;
switch (index) {
case 1:
return target.login(); // 直接字节码调用,无反射!
case 2:
return target.save((String)args[0]);
default:
throw new IllegalArgumentException();
}
}
}
结果 :CGLIB 的方法调用速度通常是 JDK 反射的 5-10 倍,甚至在某些场景下接近原生调用
第五章:CGLIB 的坑与应对策略
5.1 致命陷阱:final 方法与类
- 现象 :给
final方法加@Transactional或@Cacheable,完全无效。 - 原因 :CGLIB 靠继承实现代理,Java 语法禁止重写
final方法。生成的子类只能原样继承父类的final实现,无法插入拦截逻辑。 - 解决 :
- 去掉
final:这是最根本的解法。Spring Bean 默认就不应该是final的。 - 使用 AspectJ :AspectJ 是字节码织入,不依赖继承,可以修改
final方法的字节码(但配置复杂)。 - 接口代理 :如果必须
final,让类实现接口,并强制 Spring 使用 JDK 代理(但 JDK 代理不了final类本身,只能代理接口方法,如果方法不在接口里依然无效)。
- 去掉
5.2 构造函数陷阱
- 现象 :在构造函数里调用
@Transactional方法,事务不生效。 - 原因 :
- CGLIB 生成子类时,先调用父类构造函数。
- 此时子类还没构造完成,代理对象(
this指向的是正在构造的父类部分)还没完全初始化好拦截器链。 - 构造函数内的
this.method()调用的是父类方法,未经过代理。
- 解决 :永远不要在构造函数中调用业务方法 。使用
@PostConstruct。
5.3 内存与启动速度
- 问题:CGLIB 需要生成大量的类(每个 Bean 一个子类 + 一个 FastClass)。
- 影响 :
- Metaspace 占用:类多了,元空间占用增加。
- 启动时间:大量类生成和加载会拖慢启动速度(尤其在微服务冷启动时)。
- 架构师建议 :
- 在 Spring Boot 2.x+ ,默认配置已经是
proxyTargetClass=true(倾向于 CGLIB),因为现代 JVM 对类加载优化很好,且 CGLIB 性能更稳。 - 如果是 GraalVM Native Image,CGLIB 的动态类生成是噩梦。必须使用 Spring Native 插件进行预处理,或者切换到静态代理/AOT 编译模式。
- 在 Spring Boot 2.x+ ,默认配置已经是
第六章:JDK vs CGLIB 终极对决表
| 维度 | JDK 动态代理 | CGLIB | 架构师点评 |
|---|---|---|---|
| 实现机制 | 实现接口 (implements) |
继承类 (extends) |
CGLIB 更通用,JDK 更轻量。 |
| 目标要求 | 必须有接口 | 无需接口,但不能是 final |
现代开发推荐接口编程,但 CGLIB 容错率高。 |
| 方法调用 | 反射 (Method.invoke) |
MethodProxy (索引直调) |
CGLIB 性能胜出 (尤其是高频调用)。 |
| 拦截范围 | 仅接口公开方法 | 所有非 final 方法 |
CGLIB 能拦截包可见、保护方法(如果配置允许)。 |
| 内存开销 | 低 (一个 $Proxy 类) |
高 (子类 + FastClass + 缓存) | 超大系统需关注 Metaspace。 |
| Spring 默认 | 旧版本默认 (有接口时) | Spring Boot 2+ 默认 | 趋势已定:CGLIB 成为主流。 |
| Native 支持 | 较好 (反射可注册) | 较差 (动态生成类难处理) | 云原生时代需注意 AOT 配置。 |
总结:CGLIB 是 Spring 的"隐形翅膀"
CGLIB 不仅仅是一个库,它是 Spring 能够"无视接口"、统一代理模型的基石。
- 它利用 继承 伪装成目标对象。
- 它利用 MethodInterceptor 拦截一切非
final调用。 - 它利用 FastClass 和 MethodProxy 抹平了动态代理的性能鸿沟。
- 它最终将控制权交给 Spring 统一的 拦截器链递归模型。
- 别写
final:除非你真的不想让它被代理(比如工具类)。 - 别在构造函数里玩花样:那是代理的盲区。
- 拥抱 CGLIB:在现代 Spring 应用中,它的性能损耗几乎可以忽略,带来的灵活性却是巨大的。
- 关注 Native:如果你要上 GraalVM,请提前研究 Spring AOT 如何处理 CGLIB 生成的类。