如今已经存在很多AOP相关的类库,例如AspectJ、JAC、Nanning等。这些类库都有它们独特的目标和规范。本文将比较Java中最流行的两个AOP框架,Spring AOP和AspectJ。
AOP的概念
在我们开始前先简单回顾一下AOP相关的术语和核心概念:
- Aspect(切面):对一个横切多个类的关注点的一个模块化。事务管理是是一个横切关注点的一个很好的例子。在Spring AOP中,切面可以是通过基于XML(the schema-based approach)或用@Aspect注解(the @AspectJ style)来实现。
- Join point(连接点):是程序执行期间的一个点,例如方法的执行或异常的处理。在Spring AOP中,链接点总是表示一个方法的执行。
- Advice(通知):一个切面在连接点所采取的操作。有不同类型的通知,如前置通知、后置通知、环绕通知。许多AOP框架,包括Spring,将通知抽象成了一个拦截器,并在连接点周围维护了一个拦截器链。
- Pointcut(切点):一个匹配连接点的断言。通知与切点表达式相关联,并且与切点匹配的连接点上运行(例如,特定方法名的方法的执行)。与切点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切点表达式语言。
- Introduction(引入):代表一个类型额外的方法或字段。Spring AOP允许向任何增强的对象引入新的接口(以及相应的实现)。例如,你可以使用引入让一个bean实现IsModified接口来简化缓存。(在AspectJ社区中,引入称为类型间声明。)
- Target object(目标对象):一个被一个或多个切面增强(通知)的的对象。由于Spring AOP是通过运行时代理来实现的,所以这个对象始终是一个代理对象。
- AOP proxy(AOP代理):AOP框架为实现切面契约(通知方法的执行等)而创建的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
- Weaving(织入):将切面和其他应用程序类或对象链接以创建被增强的对象的过程。这可以是在编译期(例如使用AspectJ编译期)、加载期、运行期完成。Spring AOP 和其他纯Java AOP框架一样,在运行期植入。
Spring AOP 与 AspectJ
现在我们从多个维度来讨论Spring AOP和AspectJ,例如能力、目标、织入方式、内部结构、连接点以及简易性。
能力和目标
简单来说,Spring AOP 和AspectJ有不同的目标。
Spring AOP旨在通过Spring IoC容器来提供一个简单的AOP实现,以解决程序员面临的最常见问题。它并不是一个完整的AOP解决方案,它只能应用于由Spring容器管理的bean。
另一方面,AspectJ是最初的AOP技术,旨在提供完整的AOP解决方案。它比Spring AOP更健壮,但也要复杂得多。还值得注意的是,AspectJ可以应用于所有域对象。
织入方式
AspectJ和SpringAOP使用不同类型的织入方式,这会影响它们在性能和易用性方面的表现。
AspectJ使用三种不同的植入方式:
- Compile-time weaving(编译时织入):AspectJ编译期将切面和应用程序源代码作为输入,生成一个经过织入得类文件作为输出。
- Post-compile weaving(编译后织入):也称为二进制织入。它是把切面织入现有的类文件或者JAR文件。
- Load-time weaving(加载时织入):这与之前的二进制织入完全相同,区别在于织入过程延迟到类加载器加载类文件到JVM的时候。
AspectJ使用编译时织入和类加载时织入,而Spring AOP使用运行时织入。
通过运行时织入,在应用程序执行过程中使用目标对象的代理(使用JDK动态代理或CGLIB代理)织入这些切面:
内部结构及其应用
Spring AOP 是一个基于代理的AOP框架。这意味着要将目标对象织入切面,它将创建该对象的代理。这可以通过以下两种方式实现:
- JDK动态代理 – Spring AOP 的首选方式。只要目标对象实现一个接口,就将使用JDK动态代理。
- CGLIB 代理 – 如果对象没有实现接口,就会使用CGLIB代理。
AspectJ在运行时不做任何事情,因为类是直接用aspect编译的。因此,与Spring AOP不同,它不需要任何设计模式。为了将切面织入到代码中,它引入了一个AspectJ编译器(ajc),通过它编译程序,然后通过提供一个小的(<100K)运行库来运行程序。
连接点
上面我们提到Spring AOP是基于代理的模式。因此,它需要将目标对象子类化,并应用相应的横切关注点。但是这也有局限性。我们不能将切面应用到被声明为final的类,因为声明为final的类不能被重写,这会导致运行时异常。同样,静态方法和final方法也一样,不能被重写。由于这些限制,Spring AOP只支持方法执行作为连接点。然而,AspectJ在运行前将切面织入到代码中。与Spring AOP 不同,它不要求对目标对象子类化,因此支持其他连接点,下面是这两者的支持的连接点比较:
连接点 | Spring AOP | AspectJ |
---|---|---|
方法调用 | 不支持 | 支持 |
方法执行 | 支持 | 支持 |
构造方法调用 | 不支持 | 支持 |
构造方法执行 | 不支持 | 支持 |
静态代码块 | 不支持 | 支持 |
对象初始化 | 不支持 | 支持 |
字段引用 | 不支持 | 支持 |
字段赋值 | 不支持 | 支持 |
处理器执行 | 不支持 | 支持 |
通知执行 | 不支持 | 支持 |
值得注意的是,在Spring AOP中,切面不能应用于在同一个类中的方法调用,也就是说一个类中的方法中调用了这个类中的其他方法会导致切面失效。如下this.bar()的切面会失效:
1 | public class SimplePojo implements Pojo { |
这是因为this是目标对象而不是AOP代理对象,将不会执行切面的通知方法。但是你可以使用AspectJ或者像下面这样(Spring不推荐这样使用,因为这样会耦合Spring框架和你的业务代码):
1 | public class SimplePojo implements Pojo { |
简易性
Spring AOP 显然更简单,因为它没有在我们构建过程中引入任何额外的编译器或者织入器。因为它使用的是运行时织入,因此和我们通常的构建过程无缝集成。尽管它看起来简单,但是它只适用由Spring容器管理的bean。
然而,要使用AspectJ,我们需要引入AspectJ编译器(ajc)并且重新打包左右类库(除非我们使用后置织入或者载入时织入)。这当然比使用Spring AOP复杂,因为它引入了AspectJ Java工具(包括编译器(ajc),调试器(ajdb),文档生成器(ajdc),程序结构浏览器(ajbrowser)),我们需要将这些工具与IDE或构件工具集成。
性能差异
就性能而言,编译时织入比运行时织入更快。Spring AOP 是一个基于代理的框架,因此需要在程序启动时创建代理对象。另外,每个切面还需要执行其它额外方法,这会对性能产生负面影响。
然而不像Spring AOP,AspectJ在应用程序执行前将切面织入到代码中,因此运行时没有额外的开销。
基于这些原因,基准测试表明AspectJ要比Spring AOP 快8到35倍。
总结
这个表格总结了Spring AOP 和AspectJ的关键不同之处:
Spring AOP | AspectJ |
---|---|
纯Java实现 | Java语言的扩展 |
不需要额外编译器 | 需要AspectJ编译器(除非采用LTW) |
仅支持运行时织入 | 不支持运行时织入,支持编译时、载入时织入 |
仅支持方法织入 | 更强大,支持字段、方法构造器、静态块、final类/方法等 |
仅支持由Spring容器管理的bean | 支持所有域对象 |
仅支持方法执行切点 | 支持所有切点 |
为目标对象创建代理对象,将切面应用到代理对象 | 程序执行前将切面植入到代码中 |
性能更差 | 性能更好 |
容易学习和适用 | 相对更复杂 |
如何选择
从前面分析可以看出,并不是一个框架优于另外一个,很大程度上取决我们的需求:
框架:如果我们的应用没有使用Spring框架,那么我们只能放弃使用Spring AOP,因为Spring AOP依赖Spring容器。如果我们的应用使用了Spring框架,那么我们可以使用Spring AOP,因为她很容易学习和应用。
灵活性:由于连接点的限制,Spring AOP并不是一个完整的AOP解决方案,但它解决了程序员面临的常见问题。如果我们想用出了方法执行连接点以外的其他连接点的支持,那么使用AspectJ。
性能:如果我们使用少量的切面,那么性能差异很小。但是应用程序有成千上万的切面,那最好选择AspectJ。因为Aspect比Spring AOP快8到35倍。
同时使用:这两个框架完全兼容的。我们可以使用Spring AOP ,并仍然可以使用AspectJ来补充Spring AOP不支持的连接点。