在 Spring AOP 中,使用 JDK 动态代理 的性能确实相对 CGLIB 代理稍低。主要原因在于 JDK 动态代理是基于 Java 反射机制实现的,而 CGLIB 是基于字节码操作生成代理类的。不过,Spring 没有选择统一使用 CGLIB,主要有以下几方面原因:
1. JDK 动态代理 vs. CGLIB 代理的性能差异
- JDK 动态代理 使用 反射机制 来调用目标方法,反射的开销较大,因此性能略逊于直接操作字节码的 CGLIB。
- CGLIB 通过 **ASM(Java 字节码操作库)**直接操作字节码,生成目标类的子类来实现代理,调用时无需使用反射,性能更高。
在大多数应用中,JDK 动态代理和 CGLIB 代理的性能差异通常是微不足道的,因为代理方法的调用开销相对业务逻辑处理的开销来说是很小的。因此,性能差异不一定会对大多数实际应用造成显著影响。
2. 为什么不统一使用 CGLIB 代理
尽管 CGLIB 在性能上稍优于 JDK 动态代理,但 Spring 并未统一使用 CGLIB 代理,而是优先选择 JDK 动态代理,主要原因有以下几点:
1. JDK 动态代理更轻量
- JDK 动态代理是 Java 官方提供的,属于 标准库,不需要额外的第三方依赖。
- 使用 JDK 动态代理时,Spring 可以避免加载和管理 CGLIB 库,从而减小应用的整体依赖,尤其对于不需要大量代理的简单应用,这种方式更轻量化。
2. JDK 动态代理内存开销更小
- JDK 动态代理基于接口,直接生成一个代理类实例,不涉及生成子类和操作字节码,因此内存开销更小。
- CGLIB 生成代理类的子类,每个代理类都会占用一定的内存,并且会产生大量的类文件字节码,尤其在高并发和频繁代理生成的情况下,容易导致内存消耗过大。
3. JDK 动态代理更适合接口编程
- 面向接口编程 是 Java 设计中的最佳实践,优先使用 JDK 动态代理符合 Java 规范,更符合设计规范。
- Spring 默认优先使用 JDK 动态代理,鼓励开发者使用接口来定义业务逻辑,使得代码解耦性更好,便于测试和扩展。
4. CGLIB 的局限性
- 不能代理
final
类和final
方法:CGLIB 是通过生成目标类的子类来实现代理的,而final
类无法被继承,final
方法也无法被重写,因此 CGLIB 不能代理这些类或方法。 - 兼容性问题:CGLIB 依赖底层字节码操作库 ASM,不同版本的 JDK 可能会对字节码有不同要求,CGLIB 在某些情况下可能会出现兼容性问题。尽管现代 CGLIB 版本已能兼容大部分 JDK,但 JDK 动态代理在 Java 环境下更加稳定。
5. 性能差距不显著
- 在大多数情况下,JDK 动态代理的性能已经能够满足需求,性能差异只有在极端高并发的场景下才会被放大。而且 Spring AOP 通常用于较少的、特定的切面操作(如事务、日志等),这些操作本身通常不会频繁发生,也不会对性能造成很大影响。
- 在需要极致性能的场景下,开发者可以手动选择强制使用 CGLIB(通过
@EnableAspectJAutoProxy(proxyTargetClass = true)
)。
3. Spring 如何选择代理方式
Spring 中的代理选择策略是优先使用 JDK 动态代理,只有在目标类没有实现接口或者开发者强制指定时,才会使用 CGLIB 代理。这种设计兼顾了 性能 和 兼容性,并且符合 Java 面向接口编程的理念。
4. 什么时候选择 CGLIB 代理
尽管 Spring 默认优先使用 JDK 动态代理,但在以下场景中可以选择使用 CGLIB 代理:
- 目标类未实现接口:如果目标类没有实现任何接口,Spring 会自动选择 CGLIB 代理。
- 需要代理具体类的所有方法:如果要代理类的所有方法(包括私有方法和
protected
方法等),可以选择 CGLIB。 - 强制使用 CGLIB:如果需要 CGLIB 的高性能,或在高并发环境下频繁调用代理方法,可以通过
@EnableAspectJAutoProxy(proxyTargetClass = true)
配置强制使用 CGLIB。
总结
Spring 优先选择 JDK 动态代理,主要是因为它轻量、与 Java 标准库兼容性好、符合面向接口编程的设计理念。尽管 CGLIB 在性能上略优于 JDK 动态代理,但其内存开销更大,并且在代理 final
类和 final
方法时存在局限性。因此,Spring 选择优先使用 JDK 动态代理,只有在特定情况下(如无接口类或性能需求较高)才会使用 CGLIB。
在大多数应用中,JDK 动态代理的性能已经足够满足需求,统一使用 CGLIB 代理带来的性能提升并不足以抵消其带来的内存开销和兼容性问题。