CGLIB 动态代理的原理是什么?
简化版
CGLIB 动态代理通过运行期生成目标类的子类,并在子类中重写方法来加入增强逻辑。它不要求目标类实现接口,但不能代理 final 类和 final 方法,因为这些无法被继承或重写。
详细版
CGLIB 的核心思路是“继承 + 方法拦截”。它会生成一个目标类的子类,调用方法时先进入拦截器,再决定是否调用父类原方法。
典型伪代码:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("调用前增强");
Object result = proxy.invokeSuper(obj, args);
System.out.println("调用后增强");
return result;
});
UserService proxy = (UserService) enhancer.create();
proxy.createUser("Tom");
关键点:
- CGLIB 代理的是类,不要求接口;
- 代理类是目标类的子类;
- 方法调用会进入
MethodInterceptor; - 通常通过
methodProxy.invokeSuper()调用父类原方法; final类无法继承,final方法无法重写,因此不能被 CGLIB 正常代理增强。
在 Spring AOP 中,如果目标类没有接口,常见做法就是使用 CGLIB 代理。现代 Spring 也可以配置强制使用 CGLIB。
完整版教学
一、为什么需要 CGLIB
JDK 动态代理要求目标对象实现接口。但现实项目里并不是所有类都有接口。
比如:
class UserService {
public void createUser(String name) {
System.out.println("创建用户");
}
}
这个类没有实现接口,JDK 动态代理就不适合直接代理它。
CGLIB 的思路是:既然没有接口,那就生成一个子类。
可以理解为运行期生成了类似这样的类:
class UserServiceProxy extends UserService {
public void createUser(String name) {
before();
super.createUser(name);
after();
}
}
真实实现更复杂,但这个模型足够用于面试说明原理。
二、CGLIB 的调用流程
CGLIB 代理调用大致是:
客户端调用代理子类方法
↓
代理子类拦截方法调用
↓
进入 MethodInterceptor
↓
执行前置增强
↓
invokeSuper() 调用父类原方法
↓
执行后置增强并返回
与 JDK 动态代理相比:
- JDK 动态代理通过接口转发到
InvocationHandler; - CGLIB 通过子类重写方法转发到
MethodInterceptor。
这就是二者最核心的实现差异。
三、为什么 final 会影响 CGLIB
CGLIB 依赖继承和方法重写。如果类是 final:
final class UserService { }
它不能被继承,因此无法生成代理子类。
如果方法是 final:
public final void createUser() { }
子类不能重写这个方法,因此无法在这个方法上织入增强逻辑。
如果方法是 private,子类也无法重写它,所以也不能被正常代理增强。
面试里这个点非常高频:CGLIB 不要求接口,但受继承规则限制。
四、CGLIB 和 Spring AOP 的关系
Spring AOP 可以使用 JDK 动态代理,也可以使用 CGLIB。常见判断是:
- 目标类实现了接口:可以用 JDK 动态代理;
- 目标类没有接口:使用 CGLIB;
- 配置强制使用类代理:使用 CGLIB。
需要注意,Spring AOP 是基于代理的 AOP。它不是直接修改目标对象本身,而是把目标对象包装成代理对象。
这也解释了 Spring AOP 的一些经典限制,比如同类内部方法调用可能绕过代理。
五、CGLIB 的优缺点
优点:
- 不需要接口;
- 对普通类更友好;
- 能统一增强类方法;
- 在框架中使用广泛。
缺点:
- 不能代理
final类; - 不能增强
final、private等不可重写方法; - 生成子类和字节码增强机制比 JDK 动态代理更复杂;
- 构造方法、类加载等细节更容易影响代理行为。
所以 CGLIB 不是“比 JDK 动态代理更高级”,它只是解决了不同约束下的问题。
六、子类代理与方法拦截
目标类有 10 个可覆写方法和 2 个 final 方法时,CGLIB 只能拦截前 10 个;final 方法无法通过覆写建立拦截点。
caller -> generated subclass -> MethodInterceptor -> MethodProxy.invokeSuper()
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | CGLIB 的关键不是“没有接口”,而是通过生成子类并覆写可拦截方法建立调用入口。 |
| 适用边界 | 类和方法不能为 final,private 方法也不能被子类覆写;构造、可见性和模块边界同样要考虑。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:CGLIB 的关键不是“没有接口”,而是通过生成子类并覆写可拦截方法建立调用入口。
八、常见误区与追问
- 误区:CGLIB 可以代理任何类和任何方法。 final 类无法被继承,final/private 方法无法被覆写,因此存在明确限制。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:invoke 和 invokeSuper 为什么不能随便混用? 错误地对代理对象再次 invoke 可能递归;invokeSuper 用于进入被代理的父类实现。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
CGLIB 的记忆锚点是:没有接口也能代理,因为它生成子类;但也因为它依赖子类,所以怕 final。JDK 动态代理走接口,CGLIB 走继承,这是两者最关键的分界线。