面向切面编程(Aspect Oriented Programming, AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。OOP 中模块化的关键单元是类,而在 AOP 中,模块化单元是切面。
AOP 旨在从业务逻辑中分离出来通用逻辑,切面实现了跨越多种类型和对象的关注点(例如事务管理、日志记录、权限控制)的模块化。(这些在 AOP 文献中通常被称为 “横切” 问题。)
切面织入的三种方法:
- 编译期织入 (Compile Time Weaving,CTW):指在 Java 编译期,采用特殊的编译器,将切面织入到 Java 类中。
- 类加载期织入(Load Time Weaving,LTW):指通过特殊的类加载器,在类字节码加载到 JVM 时,织入切面。
- 运行期织入:指采用 CGLIB 工具或 JDK 动态代理进行切面的织入。
AOP 的概念和术语
让我们首先定义一些重要的 AOP 概念和术语。这些术语不是特定于Spring的。不幸的是,AOP 术语不是特别直观。但是,如果 Spring 使用自己的术语,那将更加令人困惑。
-
切面(Aspect): 跨越多个类的关注点的模块化。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,切面是通过使用常规类(基于模式的方法)或使用
@Aspect
注释的常规类来实现的 。 - 连接点(JoinPoint):程序执行期间的一个点,例如执行方法或处理异常。在 Spring AOP 中,连接点始终表示方法执行。
-
通知(Advice):特定连接点的某个切面采取的操作。不同类型的建议包括
around
,before
和after
通知。许多 AOP 框架(包括 Spring)将通知建模为一个拦截器,并在围绕着连接点维护了一个拦截器链。 - 切入点(Pointcut):匹配连接点的谓词。建议与切入点表达式相关联,并在切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。
-
引介增强(Introduction):在一种类型代表上声明额外的方法或字段。Spring AOP 允许您向任何建议的对象引入新接口(以及相应的实现)。例如,您可以使用引介增强使 bean 实现
IsModified
接口,以简化缓存。 -
目标对象(Target Object):由一个或多个切面增强的对象。也称为
advised object
。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是一个被代理对象。 - AOP Proxy:由 AOP 框架创建的对象,用于实现且切面契约(建议方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
-
织入(Weaving):将切面与其他应用程序类型或对象链接以创建
advised object
。这可以在编译时(例如,使用 AspectJ 编译器),加载期或在运行期完成。与其他纯 Java AOP 框架一样,Spring AOP 在运行时执行编织。
Spring AOP 包括以下类型的通知:
- 前置通知(Before advice):在连接点之前运行但无法阻止执行流程进入连接点的增强(除非它抛出异常)。
- 后置通知(After returning advice):在连接点正常完成后运行的增强(例如,如果方法返回而不抛出异常)。
- 异常通知(After throwing advice):如果方法通过抛出异常退出,则执行建议。
- 最终通知(After (finally) advice):无论连接点退出的方式(正常或异常返回),都要执行建议。
- 环绕通知(Around advice):围绕连接点的增强,例如方法调用。这是最强大的建议。Around 通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续执行连接点还是通过返回自己的返回值或抛出异常来终止被增强方法的执行。
环绕通知是最通用的通知。由于 Spring AOP(如 AspectJ)提供了全方位的通知类型,因此我们建议您使用可以实现所需行为的影响范围最小的通知类型。
例如,如果您只需要使用方法的返回值更新缓存,那么最好实现后置通知而不是环绕通知,尽管环绕通知可以完成同样的事情。
使用最具体的建议类型可以提供更简单的编程模型,减少错误的可能性。例如,您不需要在用于环绕通知的 JoinPoint
上调用 proceed()
方法,因此,您不会忘记调用它。
所有通知参数都是静态类型的,因此您可以使用相应类型的通知参数(例如,方法执行的返回值的类型)而不是 Object
数组。
由切入点匹配的连接点的概念是 AOP 的关键,它将其与仅提供拦截的旧技术区分开来。切入点使得通知可以独立于面向对象的层次结构进行定向。例如,您可以将一个提供声明性事务管理的通知应用于跨多个对象的一组方法(例如服务层中的所有业务操作)。
Spring AOP 的功能和目标
Spring AOP 是用纯 Java 实现的。不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用程序服务器中使用。
Spring AOP 目前仅支持方法执行连接点(建议在 Spring bean 上执行方法)。虽然可以在不破坏核心 Spring AOP API 的情况下添加对字段拦截的支持,但未实现字段拦截。如果您需要建议字段访问和更新连接点,请考虑使用 AspectJ 等语言。
Spring AOP 的 AOP 方法与大多数其他 AOP 框架的方法不同。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 非常强大)。相反,目标是在 AOP 实现和 Spring IoC 之间提供紧密集成,以帮助解决企业应用程序中的常见问题。
因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用。通过使用普通 bean 定义语法来配置切面。这是与其他 AOP 实现的重要区别。
AOP 代理
Spring AOP 默认使用 AOP 代理的标准 JDK 动态代理。这使得任何接口(或接口集)都可以被代理。
Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。
由于优化的做法是编程接口而不是类,业务类通常实现一个或多个业务接口。可以强制使用 CGLIB,在那些需要建议未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下(希望很少见)。
掌握 Spring AOP 是基于代理的这一事实非常重要。
代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 为给定目标对象创建代理。JDK 动态代理内置在 JDK 中,而 CGLIB 是一个常见的开源库。
如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理。目标类型实现的所有接口都是代理的。如果目标对象未实现任何接口,则会创建 CGLIB 代理。
如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),您可以这样做。但是,您应该考虑以下问题:
- 使用 CGLIB 时,final 方法无法被增强,因为它们无法在运行时生成的子类中重写。
- 从 Spring 4.0 开始,代理对象的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。只有当您的 JVM 不允许构造函数绕过时,您才会看到 Spring 的 AOP 支持中的双重调用和相应的调试日志条目。
要强制使用 CGLIB 代理,请将元素 proxy-target-class
属性的值设置<aop:config>
为 true
,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
要在使用 @AspectJ
自动代理支持时强制 CGLIB 代理,请将元素的 proxy-target-class
属性设置 <aop:aspectj-autoproxy>
为 true
,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
参考文献
(正文完)