文章摘要(AI生成)
AOP(面向方面编程)是一种编程范式,通过提供另一种思考程序结构的方式来补充 OOP(面向对象编程)。 在OOP中,模块化单元是一个类,而在AOP中,模块化单元是一个方面。 方面支持跨多个类型和对象的横切关注点的模块化,例如事务管理。 Spring的AOP框架是其关键组件之一。 尽管Spring IoC容器不依赖于AOP,但它提供了强大的中间件解决方案。 Spring AOP 提供了两种编写自定义切面的样式:基于模式和基于 @AspectJ 注解。 两种样式都提供类型安全建议并使用 AspectJ 切入点语言,同时仍然利用 Spring AOP 进行编织。 Spring框架中的AOP用于提供声明式企业服务,其中最重要的是声明式事务管理。 它还允许用户实现自定义方面来补充他们对 OOP 的使用。 如果您只对一般的声明式服务或其他预封装的中间件服务感兴趣,则可以跳过本章的大部分内容。 定义了核心 AOP 概念和术语,包括方面、连接点、建议和切入点。 方面将跨多个类的横切关注点模块化,事务管理就是一个很好的例子。 连接点表示程序执行中的点,例如方法执行或异常处理。 建议是方面在特定连接点采取的操作,具有不同类型的建议,包括周围建议、之前建议和之后建议。 切入点匹配连接点。 总之,AOP 通过提供模块化方式来解决横切问题来补充 OOP,并且 Spring 的 AOP 框架增强了其 IoC 容器以提供强大的中间件解决方案。
5. Spring 面向切面编程
面向切面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是切面。切面支持跨多种类型和对象的关注点(例如事务管理)的模块化。(这种关注点在 AOP 文献中通常被称为“横切”关注点。)
Spring 的关键组件之一是 AOP 框架。虽然 Spring IoC 容器不依赖 AOP(这意味着如果您不想使用 AOP,则无需使用 AOP),AOP 补充了 Spring IoC 以提供非常强大的中间件解决方案。
带有 AspectJ 切入点的 Spring AOP
Spring 通过使用 基于模式的方法或@AspectJ 注解样式提供了编写自定义切面的简单而强大的方法。这两种风格都提供了完全类型化的通知和使用 AspectJ 切入点语言,同时仍然使用 Spring AOP 进行编织。
本章讨论基于模式和@AspectJ 的 AOP 支持。较低级别的 AOP 支持将在下一章中讨论。
AOP 在 Spring Framework 中用于:
- 提供声明式企业服务。最重要的此类服务是 声明式事务管理。
- 让用户实现自定义切面,用 AOP 补充他们对 OOP 的使用。
如果您只对通用声明式服务或其他预打包的声明式中间件服务(例如池)感兴趣,则无需直接使用 Spring AOP,并且可以跳过本章的大部分内容。
5.1. AOP 概念
让我们从定义一些核心 AOP 概念和术语开始。这些术语不是 Spring 特定的。不幸的是,AOP 术语并不是特别直观。但是,如果 Spring 使用它自己的术语,那就更令人困惑了。
- Aspect:跨多个类的关注点的模块化。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,切面是通过使用常规类(基于模式的方法)或使用注解
@Aspect
注解的常规类(@AspectJ 样式)来实现的。 - 连接点:程序执行过程中的一个点,例如方法的执行或异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
- 通知:切面在特定连接点采取的行动。不同类型的通知包括“周围”、“之前”和“之后”通知。(通知类型将在后面讨论。)包括 Spring 在内的许多 AOP 框架将通知建模为拦截器,并在连接点周围维护一个拦截器链。
- 切入点:匹配连接点的谓词。Advice 与切入点表达式相关联,并在与切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。
- 简介:代表一个类型声明额外的方法或字段。Spring AOP 允许您向任何通知的对象引入新接口(和相应的实现)。例如,您可以使用介绍使 bean 实现
IsModified
接口,以简化缓存。(介绍在 AspectJ 社区中称为类型间声明。) - 目标对象:一个或多个切面通知的对象。也称为“通知对象”。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。
- AOP 代理:由 AOP 框架创建的对象,用于实现切面协定(通知方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
- 编织:将切面与其他应用程序类型或对象链接以创建通知对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。
Spring AOP 包括以下类型的通知:
- 通知前:在连接点之前运行但不能阻止执行流继续到连接点的通知(除非它抛出异常)。
- 返回通知后:在连接点正常完成后运行的通知(例如,如果方法返回而没有引发异常)。
- 抛出通知后:如果方法因抛出异常而退出,则运行通知。
- 在(最终)通知之后:无论连接点以何种方式退出(正常或异常返回),都将运行通知。
- 环绕通知:围绕连接点的通知,例如方法调用。这是最有力的通知。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点还是通过返回自己的返回值或抛出异常来缩短通知的方法执行。
环绕通知是最一般的通知。由于 Spring AOP 与 AspectJ 一样,提供了全方位的通知类型,因此我们通知您使用可以实现所需行为的最不强大的通知类型。例如,如果您只需要使用方法的返回值来更新缓存,那么您最好实现一个后返回通知而不是一个环绕通知,尽管一个环绕通知可以完成同样的事情。使用最具体的通知类型提供了一个更简单的编程模型,并且出错的可能性更小。例如,您不需要在用于环绕通知的JoinPoint
上调用proceed()
方法,因此您不会无法调用它。
所有通知参数都是静态类型的,因此您可以使用适当类型的通知参数(例如,方法执行的返回值的类型)而不是Object
数组。
切入点匹配的连接点的概念是 AOP 的关键,这将它与仅提供拦截的旧技术区分开来。切入点使通知的目标独立于面向对象的层次结构。例如,您可以将提供声明性事务管理的环绕通知应用到一组跨越多个对象(例如服务层中的所有业务操作)的方法。
5.2. 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 框架的 AOP 功能通常与 Spring IoC 容器结合使用。切面是通过使用普通的 bean 定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现的关键区别。您无法使用 Spring AOP 轻松或高效地做一些事情,例如通知非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ 是最佳选择。然而,我们的经验是,Spring AOP 为企业 Java 应用程序中大多数适合 AOP 的问题提供了出色的解决方案。
Spring AOP 从不努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们相信 Spring AOP 等基于代理的框架和 AspectJ 等成熟框架都很有价值,它们是互补的,而不是竞争的。Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以在一致的基于 Spring 的应用程序架构中实现 AOP 的所有使用。此集成不会影响 Spring AOP API 或 AOP Alliance API。Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参见下一章 。
Spring 框架的核心原则之一是非侵入性。这就是不应该强迫您将特定于框架的类和接口引入您的业务或领域模型的想法。但是,在某些地方,Spring Framework 确实为您提供了将 Spring Framework 特定的依赖项引入代码库的选项。为您提供此类选项的理由是,在某些情况下,以这种方式阅读或编写某些特定功能可能更容易。但是,Spring 框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。与本章相关的一个这样的选择是选择哪种 AOP 框架(以及哪种 AOP 风格)。您可以选择 AspectJ、Spring AOP 或两者兼而有之。您还可以选择 @AspectJ 注解样式方法或 Spring XML 配置样式方法。本章选择首先介绍@AspectJ 样式方法这一事实不应被视为Spring 团队更喜欢@AspectJ 注解样式方法而不是Spring XML 配置样式的指示。有关每种样式的“原因和原因”的更完整讨论,请参阅选择使用哪种 AOP 声明样式。
5.3. AOP 代理
Spring AOP 默认为 AOP 代理使用标准 JDK 动态代理。这使得任何接口(或一组接口)都可以被代理。
Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。由于对接口而不是类进行编程是一种很好的做法,因此业务类通常实现一个或多个业务接口。在那些(希望很少见)需要通知未在接口上声明的方法或需要将代理对象作为具体类型传递给方法的情况下,可以 强制使用 CGLIB 。
重要的是要掌握 Spring AOP 是基于代理的这一事实。请参阅 了解 AOP 代理,以全面了解此实现细节的实际含义。
5.4. @AspectJ 支持
@AspectJ 指的是一种将切面声明为带有注解的常规 Java 类的风格。@AspectJ 样式是由 AspectJ 项目作为 AspectJ 5 版本的一部分引入的。Spring 解释与 AspectJ 5 相同的注解,使用 AspectJ 提供的库进行切入点解析和匹配。但是,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。
使用 AspectJ 编译器和编织器可以使用完整的 AspectJ 语言,并在将 AspectJ 与 Spring 应用程序一起使用中进行了讨论。
5.4.1. 启用@AspectJ 支持
要在 Spring 配置中使用 @AspectJ 切面,您需要启用 Spring 支持以基于 @AspectJ 切面配置 Spring AOP,并根据这些切面是否通知它们来自动代理 bean。通过自动代理,我们的意思是,如果 Spring 确定一个 bean 由一个或多个切面通知,它会自动为该 bean 生成一个代理来拦截方法调用并确保根据需要运行通知。
可以使用 XML 或 Java 样式的配置启用 @AspectJ 支持。无论哪种情况,您还需要确保 AspectJ 的aspectjweaver.jar
库位于应用程序的类路径中(1.8 版或更高版本)。该库在 AspectJ 发行版的lib
目录中或从 Maven 中央存储库中可用。
使用 Java 配置启用 @AspectJ 支持
要使用 Java @Configuration
启用 @AspectJ 支持,请添加@EnableAspectJAutoProxy
注解,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
通过 XML 配置启用 @AspectJ 支持
要使用基于 XML 的配置启用 @AspectJ 支持,请使用aop:aspectj-autoproxy
元素,如以下示例所示:
<aop:aspectj-autoproxy/>
这假定您使用 基于 XML 模式的配置中所述的模式支持。有关如何在aop
命名空间中导入标签的信息,请参阅AOP 模式。
5.4.2. 声明一个切面
启用 @AspectJ 支持后,在应用程序上下文中定义的任何带有 @AspectJ 切面(具有@Aspect
注解)的类的 bean 都会被 Spring 自动检测并用于配置 Spring AOP。接下来的两个示例显示了一个不太有用的切面所需的最小定义。
这两个示例中的第一个显示了应用程序上下文中的常规 bean 定义,它指向具有@Aspect
注解的 bean 类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
两个示例中的第二个显示了NotVeryUsefulAspect
类定义,它使用注解org.aspectj.lang.annotation.Aspect
;
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(用@Aspect
注解的类)可以具有方法和字段,与任何其他类相同。它们还可以包含切入点、通知和介绍(类型间)声明。
通过组件扫描自动检测切面您可以通过类中的@Bean
方法将切面类注册为 Spring XML 配置中的常规 @Configuration
bean ,或者让 Spring 通过类路径扫描自动检测它们——与任何其他 Spring 管理的 bean 相同。但是,请注意, @Aspect
注解不足以在类路径中进行自动检测。为此,您需要添加一个单独的@Component
注解(或者,根据 Spring 组件扫描器的规则,一个符合条件的自定义构造型注解)。
通知切面与其他切面?在 Spring AOP 中,切面本身不能成为其他切面通知的目标。类上的@Aspect
注解将其标记为切面,因此将其排除在自动代理之外。
5.4.3. 声明切入点
切入点确定连接点,从而使我们能够控制通知何时运行。Spring AOP 仅支持 Spring bean 的方法执行连接点,因此您可以将切入点视为匹配 Spring bean 上的方法执行。切入点声明有两部分:一个包含名称和任何参数的签名和一个切入点表达式,它准确地确定我们感兴趣的方法执行。在 AOP 的 @AspectJ 注解样式中,切入点签名由常规方法定义提供, 切入点表达式使用@Pointcut
注解表示(作为切入点签名的方法必须有void
返回类型)。
一个示例可能有助于明确切入点签名和切入点表达式之间的区别。以下示例定义了一个名为的切入点anyOldTransfer
,它与任何名为transfer
的方法的执行相匹配:
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
形成@Pointcut
注解值的切入点表达式是常规的 AspectJ 切入点表达式。有关 AspectJ 切入点语言的完整讨论,请参阅AspectJ Programming Guide(以及扩展的 AspectJ 5 Developer’s Notebook)或有关 AspectJ 的书籍之一(例如Colyer 等人的Eclipse AspectJ或**AspectJ in Action,拉姆尼瓦斯·拉达德)。
支持的切入点指示符
Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示符 (PCD):
execution
:用于匹配方法执行连接点。这是使用 Spring AOP 时使用的主要切入点指示符。within
: 限制匹配到特定类型内的连接点(使用 Spring AOP 时执行匹配类型内声明的方法)。this
:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中 bean 引用(Spring AOP 代理)是给定类型的实例。target
:将匹配限制在目标对象(被代理的应用程序对象)是给定类型的实例的连接点(使用 Spring AOP 时方法的执行)。args
: 限制匹配到参数是给定类型的实例的连接点(使用 Spring AOP 时方法的执行)。@target
:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中执行对象的类具有给定类型的注解。@args
:将匹配限制为连接点(使用 Spring AOP 时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。@within
:将匹配限制为具有给定注解的类型内的连接点(使用 Spring AOP 时执行在具有给定注解的类型中声明的方法)。@annotation
:限制匹配到连接点的主题(在 Spring AOP 中运行的方法)具有给定注解的连接点。
其他切入点类型
完整的 AspectJ 切入点语言支持 Spring 中不支持的其他切入点指示符:call
、get
、set
、preinitialization
、 staticinitialization
、initialization
、handler
、adviceexecution
、withincode
、cflow
、 cflowbelow
、if
、@this
和@withincode
。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException
异常。
Spring AOP 支持的切入点指示符集可能会在未来的版本中扩展,以支持更多的 AspectJ 切入点指示符。
由于 Spring AOP 将匹配限制为仅方法执行连接点,因此前面对切入点指示符的讨论给出了比您在 AspectJ 编程指南中找到的更窄的定义。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点处,this
和target
两者都引用同一个对象:执行方法的对象。Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到this
)和代理背后的目标对象(绑定到target
)。
由于 Spring 的 AOP 框架基于代理的特性,根据定义,目标对象内的调用不会被拦截。对于 JDK 代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,代理上的公共和受保护的方法调用被拦截(如果需要,甚至包可见的方法)。但是,通过代理的常见交互应始终通过公共签名进行设计。请注意,切入点定义通常与任何拦截的方法匹配。如果切入点严格来说是只公开的,即使在 CGLIB 代理场景中,通过代理进行潜在的非公开交互,也需要相应地定义它。如果您的拦截需求包括目标类中的方法调用甚至构造函数,请考虑使用 Spring 驱动的原生 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这就构成了具有不同特点的不同AOP使用模式,所以在做决定之前一定要让自己熟悉编织。
Spring AOP 还支持一个名为bean
的PCD. 此 PCD 允许您将连接点的匹配限制为特定命名的 Spring bean 或一组命名的 Spring bean(使用通配符时)。bean
PCD具有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean
令牌可以是任何 Spring bean 的名称。提供了使用*
字符的有限通配符支持,因此,如果您为 Spring bean 建立了一些命名约定,则可以编写一个bean
PCD 表达式来选择它们。与其他切入点指示符的情况一样,bean
PCD 也可以与&&
(and)、||
(or) 和!
(negation) 运算符一起使用。
bean
PCD 仅在 Spring AOP 中受支持,在本机 AspectJ 编织中不支持。它是 AspectJ 定义的标准 PCD 的特定于 Spring 的扩展,因此不适用于@Aspect
模型中声明的切面。PCD在bean
实例级别(基于 Spring bean 名称概念)而不是仅在类型级别(基于编织的 AOP 受限)运行。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架的一种特殊功能,它与 Spring bean 工厂的紧密集成,通过名称来识别特定的 bean 是自然而直接的。
组合切入点表达式
您可以使用&&
,||
和!
组合切入点表达式。您还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
如前所述,使用较小的命名组件构建更复杂的切入点表达式是最佳实践。当按名称引用切入点时,应用正常的 Java 可见性规则(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何地方的公共切入点,等等)。可见性不影响切入点匹配。
共享通用切入点定义
在使用企业应用程序时,开发人员通常希望从多个切面引用应用程序的模块和特定的操作集。我们通知为此目的定义一个捕获通用切入点表达式的CommonPointcuts
切面。这样的切面通常类似于以下示例:
package com.xyz.myapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.myapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
您可以在需要切入点表达式的任何地方引用在此类切面中定义的切入点。例如,要使服务层具有事务性,您可以编写以下代码:
<aop:config>
<aop:advisor
pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
和<aop:advisor>
元素在基于模式的 AOP 支持中讨论。事务元素在事务管理中讨论。
例子
Spring AOP 用户可能execution
最常使用切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern?
ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式(在ret-type-pattern
前面的代码片段中)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配连接点。 *
最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,完全限定的类型名称才匹配。名称模式与方法名称匹配。您可以将*
通配符用作名称模式的全部或一部分。如果您指定声明类型模式,请包含一个尾随.
以将其连接到名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而(..)
匹配任意数量(零个或多个)的参数。该(*)
模式匹配采用任何类型的一个参数的方法。 (*,String)
匹配带有两个参数的方法。第一个可以是任何类型,而第二个必须是String
. 有关更多信息,请参阅 AspectJ 编程指南的 语言语义部分。
以下示例显示了一些常见的切入点表达式:
-
任何公共方法的执行:
execution(public * *(..))
-
名称以 开头的任何方法的执行
set
:execution(public * *(..))
-
AccountService
接口定义的任何方法的执行:execution(* com.xyz.service.AccountService.*(..))
-
包中定义的任何方法的执行
service
:execution(* com.xyz.service.*.*(..))
-
服务包或其子包之一中定义的任何方法的执行:
execution(* com.xyz.service..*.*(..))
-
服务包中的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service.*)
-
服务包或其子包之一中的任何连接点(仅在 Spring AOP 中执行方法):
within(com.xyz.service..*)
-
AccountService
代理实现接口的任何连接点(仅在 Spring AOP 中执行方法) :this(com.xyz.service.AccountService)
this
更常用于绑定形式。 有关如何使代理对象在通知正文中可用的信息, 请参阅声明通知部分。 -
AccountService
目标对象实现接口的任何连接点(仅在 Spring AOP 中执行方法) :target(com.xyz.service.AccountService)
target
更常用于绑定形式。有关如何使目标对象在通知正文中可用的信息, 请参阅声明通知部分。 -
任何接受单个参数且在运行时传递的参数为
Serializable
的连接点(仅在 Spring AOP 中执行方法):args(java.io.Serializable)
args
更常用于绑定形式。请参阅声明通知部分,了解如何使方法参数在通知正文中可用。请注意,此示例中给出的切入点与
execution(* *(java.io.Serializable))
. 如果在运行时传递的参数是Serializable
则args 版本匹配,如果方法签名声明了一个Serializable
类型的参数,则执行版本匹配。 -
目标对象具有
@Transactional
注解的任何连接点(仅在 Spring AOP 中执行方法):@target(org.springframework.transaction.annotation.Transactional)
您也可以
@target
在绑定表单中使用。有关如何使注解对象在通知正文中可用的信息, 请参阅声明通知部分。 -
目标对象的声明类型具有
@Transactional
注解的任何连接点(仅在 Spring AOP 中执行方法):@within(org.springframework.transaction.annotation.Transactional)
您也可以在绑定表单中使用
@within
。有关如何使注解对象在通知正文中可用的信息, 请参阅声明通知部分。 -
执行方法具有
@Transactional
注解的任何连接点(仅在 Spring AOP 中执行方法):@annotation(org.springframework.transaction.annotation.Transactional)
您也可以在绑定表单中使用
@annotation
。有关如何使注解对象在通知正文中可用的信息, 请参阅声明通知部分。 -
任何接受单个参数的连接点(仅在 Spring AOP 中执行方法),并且传递的参数的运行时类型具有
@Classified
注解:@args(com.xyz.security.Classified)
您也可以在绑定表单中使用
@args
。请参阅声明通知部分如何使注解对象在通知正文中可用。 -
Spring bean 上的任何连接点(方法仅在 Spring AOP 中执行)名为
tradeService
:bean(tradeService)
-
名称与
*Service
通配符表达式匹配的 Spring bean 上的任何连接点(仅在 Spring AOP 中执行方法):bean(*Service)
编写好的切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个代价高昂的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中放置测试以确定代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点在 DNF(析取范式)中被重写,切入点的组件被排序,以便首先检查那些评估成本较低的组件。
然而,AspectJ 只能使用它被告知的内容。为了获得最佳匹配性能,您应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然属于以下三组之一:kinded、scoping 和 contextual:
- 种类指示符选择一种特定类型的连接点:
execution
、get
、set
、call
和handler
。 - 范围指示符选择一组连接兴趣点(可能有多种):
within
和withincode
- 上下文指示符根据上下文匹配(并且可以选择绑定):
this
、、target
和@annotation
一个写得很好的切入点应该至少包括前两种类型(种类和范围)。您可以包含上下文指示符以根据连接点上下文进行匹配,或绑定该上下文以在通知中使用。由于额外的处理和分析,只提供一个 kinded 指示符或只提供一个上下文指示符是可行的,但可能会影响编织性能(使用的时间和内存)。范围指示符的匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地消除不应进一步处理的连接点组。如果可能,一个好的切入点应始终包含一个切入点。
5.4.4. 声明切点
Advice 与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。
前置通知
您可以使用注解@Before
在切面声明之前的通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果我们使用就地切入点表达式,我们可以将前面的示例重写为以下示例:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
返回通知
返回通知后,当匹配的方法执行正常返回时运行。您可以使用@AfterReturning
注解来声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
你可以有多个通知声明(以及其他成员),都在同一个切面。我们在这些示例中只展示了一个通知声明,以集中每个通知的效果。
有时,您需要在通知正文中访问返回的实际值。您可以使用@AfterReturning
绑定返回值的形式来获取该访问权限,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
属性returning
中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。returning
子句还将匹配限制为仅返回指定类型的值的那些方法执行(在这种情况下,Object
匹配任何返回值)。
请注意,在返回通知后使用时,不可能返回完全不同的参考。
异常通知
当匹配的方法执行通过抛出异常退出时,抛出通知运行后。您可以使用@AfterThrowing
注解来声明它,如以下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,您希望通知仅在引发给定类型的异常时运行,并且您还经常需要访问通知正文中引发的异常。您可以使用throwing
属性来限制匹配(如果需要 -否则用Throwable
作异常类型)并将抛出的异常绑定到通知参数。以下示例显示了如何执行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
属性throwing
中使用的名称必须与通知方法中的参数名称相对应。当通过抛出异常退出方法执行时,异常将作为相应的参数值传递给通知方法。throwing
子句还将匹配限制为仅抛出指定类型的异常(在本例中为DataAccessException
)的那些方法执行。
请注意,@AfterThrowing
这并不表示一般的异常处理回调。具体来说,@AfterThrowing
通知方法只应该从连接点(用户声明的目标方法)本身接收异常,而不是从伴随的 @After
/@AfterReturning
方法接收异常。
(最终)后置通知
当匹配的方法执行退出时(最终)通知运行。它是通过使用@After
注解来声明的。After 通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。下面的例子展示了如何使用 after finally 通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
请注意,@After
AspectJ 中的通知被定义为“在 finally 通知之后”,类似于 try-catch 语句中的 finally 块。它将在连接点(用户声明的目标方法)抛出的任何结果、正常返回或异常时调用,与之相反,@AfterReturning
它仅适用于成功的正常返回。
环绕通知
最后一种通知是环绕通知。环绕通知“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),则通常使用环绕通知。
始终使用满足您要求的最不强大的通知形式。例如,如果之前的通知足以满足您的需求,请不要使用环绕通知。
环绕通知是通过使用注解对方法进行@Around
注解来声明的。该方法应声明Object
为其返回类型,并且该方法的第一个参数必须是ProceedingJoinPoint
类型。在通知方法的主体中,您必须调用ProceedingJoinPoint
的proceed()
以使底层方法运行。不带参数调用proceed()
将导致调用者的原始参数在调用时提供给底层方法。对于高级用例,proceed()
方法有一个重载变体,它接受参数数组 ( Object[]
)。调用时,数组中的值将用作底层方法的参数。
使用proceed
调用Object[]
时的行为与 AspectJ 编译器编译的proceed
焕荣通知的行为略有不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给proceed
的参数 数量必须与传递给环绕通知的参数数量相匹配(而不是底层连接点采用的参数数量),并且传递给给定继续进行的值参数位置替换值绑定到的实体的连接点处的原始值(如果现在没有意义,请不要担心)。Spring 采用的方法更简单,更符合其基于代理的、仅执行的语义。如果您编译为 Spring 编写的@AspectJ
切面和与 AspectJ 编译器和编织器一起使用proceed
参数,您只需要注意这种差异 。有一种方法可以编写跨 Spring AOP 和 AspectJ 100% 兼容的切面,这将在 下一节有关通知参数的部分中讨论。
around 通知返回的值是方法调用者看到的返回值。例如,一个简单的缓存切面可以从缓存中返回一个值,如果它有一个值,或者调用proceed()
(并返回该值)如果它没有。请注意,proceed
可能会在环绕通知的主体内调用一次、多次或根本不调用。所有这些都是合法的。
如果您将环绕通知方法的返回类型声明为void
,将始终返回给调用者null
,有效地忽略任何调用proceed()
的结果。因此,使用环绕通知方法声明返回类型为Object
. 通知方法通常应该返回调用proceed()
返回的值,即使底层方法具有void
返回类型。但是,根据用例,通知可以选择返回缓存值、包装值或其他值。
下面的例子展示了如何使用环绕通知:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
通知参数
Spring 提供完全类型化的通知,这意味着您可以在通知签名中声明所需的参数(正如我们之前在返回和抛出示例中看到的那样),而不是一直使用数组Object[]
。我们将在本节后面看到如何使参数和其他上下文值可用于通知主体。首先,我们看一下如何编写通用通知,以了解通知当前通知的方法。
访问当前JoinPoint
任何通知方法都可以声明类型为 的参数作为其第一个参数 org.aspectj.lang.JoinPoint
。请注意,使用环绕通知来声明的第一个参数类型是ProceedingJoinPoint
,它是JoinPoint
的子类。
该JoinPoint
接口提供了许多有用的方法:
getArgs()
:返回方法参数。getThis()
:返回代理对象。getTarget()
:返回目标对象。getSignature()
:返回所通知方法的描述。toString()
:打印所通知方法的有用描述。
有关更多详细信息,请参阅javadoc。
将参数传递给 Advice
我们已经看到了如何绑定返回值或异常值(在返回和抛出通知之后使用)。要使参数值可用于通知正文,您可以使用args
. 如果在args
表达式中使用参数名称代替类型名称,则在调用通知时相应参数的值将作为参数值传递。一个例子应该更清楚地说明这一点。假设您要通知执行以Account
对象为第一个参数的 DAO 操作,并且您需要访问通知正文中的帐户。您可以编写以下内容:
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
args(account,..)
切入点表达式的部分有两个目的。首先,它将匹配限制为只匹配那些方法至少有一个参数的方法执行,并且传递给该参数的参数是Account
. 其次,它通过参数使实际Account
对象可用于通知account
。
另一种写法是声明一个切入点, 当它匹配一个连接点时“提供”Account
对象值,然后从通知中引用命名的切入点。这将如下所示:
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
有关详细信息,请参阅 AspectJ 编程指南。
代理对象 ( this
)、目标对象 ( target
) 和注解 ( @within
、 @target
、@annotation
和@args
) 都可以以类似的方式绑定。接下来的两个示例显示了如何匹配带有@Auditable
注解的方法的执行并提取审计代码:
这两个示例中的第一个显示了@Auditable
注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
这两个示例中的第二个显示了与@Auditable
方法执行相匹配的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
通知参数和泛型
Spring AOP 可以处理类声明和方法参数中使用的泛型。假设你有一个像下面这样的泛型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
您可以通过将通知参数绑定到要拦截方法的参数类型来将方法类型的拦截限制为某些参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
这种方法不适用于泛型集合。所以你不能定义一个切入点如下:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
为了完成这项工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理null
一般的值。要实现类似的效果,您必须键入参数Collection<?>
并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。参数名称不能通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:
- 如果用户已明确指定参数名称,则使用指定的参数名称。通知和切入点注解都有一个可选
argNames
属性,您可以使用它来指定带注解的方法的参数名称。这些参数名称在运行时可用。以下示例显示了如何使用该argNames
属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数是JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
类型,则可以在argNames
属性值中省略参数名称。例如,如果您修改前面的通知以接收连接点对象,则argNames
属性不需要包含它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
对JoinPoint
, ProceedingJoinPoint
和JoinPoint.StaticPart
类型的第一个参数的特殊处理对于不收集任何其他连接点上下文的通知实例特别方便。在这种情况下,您可以省略该argNames
属性。例如,以下通知不需要声明argNames
属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
-
使用
argNames
属性有点笨拙,所以如果没有指定argNames
属性,Spring AOP 会查看类的调试信息并尝试从局部变量表中确定参数名称。只要使用调试信息(至少)编译了类,就会出现此信息。使用此标志进行编译的后果是:(1)您的代码更容易理解(逆向工程),(2)类文件大小稍微大一点(通常无关紧要),(3)优化以删除未使用的本地您的编译器未应用变量。换句话说,打开此标志进行构建应该不会遇到任何困难。如果 AspectJ 编译器 (
ajc
) 已经编译了 @AspectJ 切面,即使没有调试信息,您也不需要添加argNames
属性,因为编译器会保留所需的信息。 -
如果在没有必要调试信息的情况下编译了代码,Spring AOP 会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,并且advice 方法只接受一个参数,则配对很明显)。如果给定可用信息,变量的绑定不明确,则抛出
AmbiguousBindingException
。 -
如果上述所有策略均失败,则抛出
IllegalArgumentException
。
参数处理
我们之前提到过,我们将描述如何编写一个带有在 Spring AOP 和 AspectJ 中一致工作的参数的proceed
调用。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
在许多情况下,无论如何都要执行此绑定(如前面的示例中所示)。
通知优先级
当多条通知都想在同一个连接点运行时会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序。最高优先级的通知首先“在进入的路上”运行(因此,给定两条之前的通知,优先级最高的一条首先运行)。从连接点“退出”时,优先级最高的通知最后运行(因此,给定两条后通知,具有最高优先级的一条将运行第二个)。
当不同方面定义的两条通知都需要在同一个连接点运行时,除非您另外指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这是通过在方面类中实现 org.springframework.core.Ordered
接口或使用 @Order
注释对其进行注释,以正常的 Spring 方式完成的。给定两个方面,从 Ordered.getOrder()
(或注释值)返回较低值的方面具有较高的优先级。
特定方面的每个不同通知类型在概念上都意味着直接应用于连接点。因此,@AfterThrowing 通知方法不应从随附的 @After/@AfterReturning 方法接收异常。
从 Spring Framework 5.2.7 开始,需要在同一连接点运行的同一 @Aspect 类中定义的通知方法将根据其通知类型按以下顺序分配优先级,从最高优先级到最低优先级:@Around、@Before 、@After、@AfterReturning、@AfterThrowing。但请注意,@After 通知方法将在同一方面中的任何 @AfterReturning 或 @AfterThrowing 通知方法之后有效地调用,遵循 AspectJ 的 @After 的“after finally 通知”语义。
当在同一个 @Aspect 类中定义的两个相同类型的通知(例如,两个 @After 通知方法)都需要在同一连接点运行时,顺序是未定义的(因为无法检索源)通过 javac 编译类的反射来声明代码顺序)。考虑将此类通知方法折叠为每个 @Aspect 类中每个连接点的一个通知方法,或者将通知片段重构为单独的 @Aspect 类,您可以通过 Ordered 或 @Order 在方面级别订购这些类。
5.4.5. 切面说明
切面说明(在 AspectJ 中称为类型间声明)使切面能够声明通知对象实现给定接口,并代表这些对象提供该接口的实现。
您可以使用@DeclareParents
注解进行介绍。此注解用于声明匹配类型有一个新的父级(因此得名)。例如,给定一个名为UsageTracked
的接口和一个名为DefaultUsageTracked
的接口的实现, 以下切面声明服务接口的所有实现者也实现UsageTracked
接口(例如,通过 JMX 进行统计):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
要实现的接口由注解字段的类型决定。注解@DeclareParents
的 value
属性是 AspectJ 类型模式。任何匹配类型的 bean 都会实现UsageTracked
接口。请注意,在前面示例的之前通知中,服务 bean 可以直接用作UsageTracked
接口的实现。如果以编程方式访问 bean,您将编写以下内容:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
5.4.6. 切面实例化模型
这是一个高级话题。如果您刚开始使用 AOP,您可以放心地跳过它,直到以后。
默认情况下,应用程序上下文中的每个切面都有一个实例。AspectJ 将此称为单例实例化模型。可以定义具有备用生命周期的切面。Spring 支持 AspectJperthis
和pertarget
实例化模型;percflow
, percflowbelow
, 和pertypewithin
当前不受支持。
您可以通过在@Aspect
注解中指定一个perthis
子句来声明一个perthis
切面。考虑以下示例:
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}
在前面的示例中,perthis
子句的效果是为执行业务服务的每个唯一服务对象(每个唯一对象绑定到切入点表达式匹配的this
连接点)创建一个切面实例。切面实例是在第一次在服务对象上调用方法时创建的。当服务对象超出范围时,切面超出范围。在创建切面实例之前,其中的任何通知都不会运行。一旦创建了切面实例,其中声明的通知就会在匹配的连接点处运行,但仅当服务对象是与此切面关联的对象时。有关per
子句的更多信息,请参阅 AspectJ 编程指南。
pertarget
实例化模型的工作方式与 完全相同perthis
,但它在匹配的连接点为每个唯一目标对象创建一个切面实例。
5.4.7. AOP 示例
现在您已经了解了所有组成部分的工作原理,我们可以将它们组合在一起做一些有用的事情。
由于并发问题(例如,死锁失败者),业务服务的执行有时会失败。如果该操作被重试,则很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException
. 这是一个明确跨越服务层中多个服务的要求,因此非常适合通过切面实现。
因为我们要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用proceed
。以下清单显示了基本切面的实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
请注意,切面实现了Ordered
接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。maxRetries
和order
属性都是由 Spring 配置的。主要动作doConcurrentOperation
发生在环绕通知中。请注意,目前,我们将重试逻辑应用于每个businessService()
. 我们尝试继续,如果我们失败了PessimisticLockingFailureException
,我们再试一次,除非我们用尽了所有的重试尝试。
对应的Spring配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了改进切面以使其仅重试幂等操作,我们可以定义以下 Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
然后我们可以使用注解来注解服务操作的实现。对仅重试幂等操作切面的更改涉及改进切入点表达式,以便仅@Idempotent
操作匹配,如下所示:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
5.5. 基于模式的 AOP 支持
如果您更喜欢基于 XML 的格式,Spring 还支持使用aop
命名空间标签定义切面。支持与使用 @AspectJ 样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点放在该语法上,并请读者参考上一节中的讨论(@AspectJ 支持),以了解编写切入点表达式和通知参数的绑定。
要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop
架构,如基于 XML 架构的配置中所述。 有关如何在aop
命名空间中导入标签的信息,请参阅AOP 模式。
在您的 Spring 配置中,所有切面和顾问元素都必须放在一个<aop:config>
元素中(您可以在应用程序上下文配置中拥有多个<aop:config>
元素)。一个<aop:config>
元素可以包含切入点、顾问和切面元素(请注意,这些元素必须按此顺序声明)。
<aop:config>
配置风格大量使用了 Spring 的 自动代理机制。如果您已经通过使用BeanNameAutoProxyCreator
或类似的方式使用显式自动代理,这可能会导致问题(例如未编织通知) 。推荐的使用模式是仅使用<aop:config>
样式或仅使用AutoProxyCreator
样式并且从不混合使用它们。
5.5.1. 声明一个切面
当您使用模式支持时,切面是在 Spring 应用程序上下文中定义为 bean 的常规 Java 对象。在对象的字段和方法中捕获状态和行为,在 XML 中捕获切入点和通知信息。
您可以使用<aop:aspect>
元素声明切面,并使用属性ref
引用支持 bean ,如以下示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支持切面的 bean(在这种情况下是aBean
)当然可以像任何其他 Spring bean 一样进行配置和依赖注入。
5.5.2. 声明切入点
您可以在<aop:config>
元素内声明一个命名切入点,让切入点定义在多个切面和顾问之间共享。
表示服务层中任何业务服务执行的切入点可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
请注意,切入点表达式本身使用与@AspectJ 支持中描述的相同的 AspectJ 切入点表达式语言。如果您使用基于模式的声明样式,您可以在切入点表达式中引用类型 (@Aspects) 中定义的命名切入点。定义上述切入点的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.CommonPointcuts.businessService()"/>
</aop:config>
假设您有共享通用切入点定义中描述的CommonPointcuts
切面。
然后在切面内声明切入点与声明顶级切入点非常相似,如以下示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与@AspectJ 切面非常相似,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点收集this
对象作为连接点上下文并将其传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
必须通过包含匹配名称的参数来声明通知以接收收集的连接点上下文,如下所示:
public void monitor(Object service) {
// ...
}
组合切入点子表达式时,&&
在 XML 文档中很尴尬,因此您可以分别使用and
、or
和not
关键字来代替&&
、 ||
和!
。例如,前面的切入点可以更好地写成如下:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
请注意,以这种方式定义的切入点由它们的 XML 引用,id
不能用作命名切入点来形成复合切入点。因此,基于模式的定义风格中的命名切入点支持比@AspectJ 风格提供的更有限。
5.5.3. 声明通知
基于模式的 AOP 支持使用与 @AspectJ 样式相同的五种通知,并且它们具有完全相同的语义。
前置通知
Before 通知在匹配的方法执行之前运行。 通过在 <aop:aspect>
内声明使用<aop:before>
元素,如以下示例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
这里,dataAccessOperation
是在顶层 ( <aop:config>
) 级别定义的切入点的id
。要改为内联定义切入点,请将pointcut-ref
属性替换为pointcut
属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
正如我们在讨论@AspectJ 样式时所指出的,使用命名切入点可以显着提高代码的可读性。
method
属性标识提供通知正文的方法 (doAccessCheck
)。必须为包含通知的切面元素引用的 bean 定义此方法。在执行数据访问操作(切入点表达式匹配的方法执行连接点)之前,将调用切面 bean 上的doAccessCheck
方法。
返回通知
当匹配的方法执行正常完成时,返回通知运行后。它在 <aop:aspect>
内部声明的方式与之前的通知相同。以下示例显示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
与@AspectJ 样式一样,您可以在通知正文中获取返回值。为此,请使用returning
属性指定应将返回值传递到的参数名称,如以下示例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
该doAccessCheck
方法必须声明一个名为retVal
的参数。此参数的类型以与@AfterReturning
描述相同的方式约束匹配。例如,您可以如下声明方法签名:
public void doAccessCheck(Object retVal) {...
异常通知
当匹配的方法执行通过抛出异常退出时,抛出通知运行后。它通过在 <aop:aspect>
内声明使用after-throwing
元素,如以下示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>
与@AspectJ 风格一样,您可以在通知正文中获取抛出的异常。为此,请使用throwing
属性指定应将异常传递到的参数的名称,如以下示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions
方法必须声明一个名为dataAccessEx
的参数。此参数的类型以与@AfterThrowing
描述相同的方式约束匹配 。例如,方法签名可以声明如下:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
(最终)通知之后
无论匹配的方法执行如何退出,(最终)通知都会运行。您可以使用after
元素来声明它,如以下示例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
环绕通知
最后一种通知是环绕通知。环绕通知“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),则通常使用环绕通知。
始终使用满足您要求的最不强大的通知形式。例如,如果之前的通知足以满足您的需求,请不要使用环绕通知。
您可以使用aop:around
元素声明环绕通知。通知方法应该声明Object
为它的返回类型,并且方法的第一个参数必须是 ProceedingJoinPoint
。在通知方法的主体中,您必须调用ProceedingJoinPoint
的 proceed()
以使底层方法运行。不带参数调用proceed()
将导致调用者的原始参数在调用时提供给底层方法。对于高级用例,该proceed()
方法有一个重载变体,它接受参数数组 ( Object[]
)。调用时,数组中的值将用作底层方法的参数。有关proceed使用.
以下示例展示了如何在 XML 中声明环绕通知:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
通知的实现doBasicProfiling
可以与@AspectJ 示例中的完全相同(当然,要减去注解),如以下示例所示:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
通知参数
基于模式的声明风格以与@AspectJ 支持相同的方式支持完全类型化的通知——通过按名称匹配切入点参数与通知方法参数。有关详细信息,请参阅通知参数。如果您希望为通知方法显式指定参数名称(不依赖于前面描述的检测策略),您可以使用 通知元素的arg-names
属性来实现,该属性的处理方式与 通知注解中的argNames
属性相同(如确定参数名称中所述)。以下示例显示如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
该arg-names
属性接受以逗号分隔的参数名称列表。
以下基于 XSD 的方法稍微复杂一些的示例显示了一些与许多强类型参数结合使用的环绕通知:
package x.y.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
接下来是切面。请注意,profile(..)
方法接受许多强类型参数,其中第一个参数恰好是用于继续进行方法调用的连接点。此参数的存在表明 profile(..)
将用作around
通知,如以下示例所示:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
最后,以下示例 XML 配置会影响对特定连接点的上述通知的执行:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
考虑以下驱动程序脚本:
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;
public final class Boot {
public static void main(final String[] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
PersonService person = (PersonService) ctx.getBean("personService");
person.getPerson("Pengo", 12);
}
}
使用这样的 Boot 类,我们将在标准输出中获得类似于以下内容的输出:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms % Task name
-----------------------------------------
00000 ? execution(getFoo)
通知优先级
当多条通知需要在同一个连接点(执行方法)运行时,排序规则如Advice Ordering中所述。切面之间的优先级通过元素order
中的属性<aop:aspect>
或通过将@Order
注解添加到支持切面的 bean 或通过让 bean 实现Ordered
接口来确定。
与同一个 @Aspect 类中定义的通知方法的优先级规则相反,当同一个<aop:aspect>
元素中定义的两条通知都需要在同一连接点运行时,优先级由以下顺序决定:其中通知元素在封闭的 <aop:aspect>
元素中声明,优先级从最高到最低。
例如,给定在同一个 <aop:aspect>
元素中定义的应用于同一连接点的 around
通知和 before
通知,为了确保 around
通知比 before
通知具有更高的优先级, 元素必须在 <aop:before>
元素之前声明。
5.5.4. 切面说明
切面说明(在 AspectJ 中称为类型间声明)让切面声明通知对象实现给定接口并代表这些对象提供该接口的实现。
您可以通过在 aop:aspect
内使用 aop:declare-parents
元素进行切面说明。您可以使用 aop:declare-parents
元素来声明匹配类型具有新的父级(因此得名)。例如,给定一个名为UsageTracked
的接口以及名为DefaultUsageTracked
的该接口的实现,以下方面声明服务接口的所有实现者也实现UsageTracked
接口。 (例如,为了通过 JMX 公开统计信息。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支持usageTracking
bean 的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
要实现的接口由implement-interface
属性决定。该types-matching
属性的值是一个 AspectJ 类型模式。任何匹配类型的 bean 都会实现该UsageTracked
接口。请注意,在前面示例的之前通知中,服务 bean 可以直接用作UsageTracked
接口的实现。要以编程方式访问 bean,您可以编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
5.5.5. 切面实例化模型
唯一受支持的模式定义切面的实例化模型是单例模型。未来版本可能支持其他实例化模型。
5.5.6. 顾问
“顾问”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。顾问就像一个独立的小切面,只有一条通知。通知本身由 bean 表示,并且必须实现 Spring 中的 Advice Types 中描述的通知接口之一。顾问可以利用 AspectJ 切入点表达式。
Spring 通过<aop:advisor>
元素支持顾问概念。您最常看到它与事务通知一起使用,后者在 Spring 中也有自己的命名空间支持。以下示例显示了一个顾问:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了pointcut-ref
前面示例中使用的属性,您还可以使用该 pointcut
属性内联定义切入点表达式。
要定义顾问的优先级以便通知可以参与排序,请使用order
属性来定义顾问的Ordered
值。
5.5.7。AOP 模式示例
本节展示了 一个 AOP 示例中的并发锁定失败重试示例在使用模式支持重写时的外观。
由于并发问题(例如,死锁失败者),业务服务的执行有时会失败。如果该操作被重试,则很可能在下一次尝试时成功。对于在这种情况下适合重试的业务服务(不需要返回给用户解决冲突的幂等操作),我们希望透明地重试操作以避免客户端看到 PessimisticLockingFailureException
. 这是一个明确跨越服务层中多个服务的要求,因此非常适合通过切面实现。
因为我们要重试操作,所以我们需要使用around通知,以便我们可以多次调用proceed
。下面的清单显示了基本的切面实现(这是一个使用模式支持的常规 Java 类):
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
请注意,切面实现了Ordered
接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。maxRetries
和order
属性都是由 Spring 配置的。主要动作doConcurrentOperation
发生在around 通知方法中。我们尝试继续。如果我们以 PessimisticLockingFailureException
失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。
此类与@AspectJ 示例中使用的类相同,但删除了注解。
对应的Spring配置如下:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
请注意,我们暂时假设所有业务服务都是幂等的。如果不是这种情况,我们可以通过引入注解并使用Idempotent
注解来注解服务操作的实现,来细化切面,使其仅重试真正的幂等操作,如以下示例所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
对仅重试幂等操作切面的更改涉及改进切入点表达式,以便仅@Idempotent
操作匹配,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
5.6. 选择要使用的 AOP 声明样式
一旦您决定一个切面是实现给定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 语言(代码)样式、@AspectJ 注解样式或 Spring XML 样式之间做出选择?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对 AOP 的熟悉程度。
5.6.1。Spring AOP 还是 Full AspectJ?
使用可以工作的最简单的东西。Spring AOP 比使用完整的 AspectJ 更简单,因为不需要将 AspectJ 编译器/编织器引入您的开发和构建过程。如果您只需要通知对 Spring bean 执行操作,那么 Spring AOP 是正确的选择。如果您需要通知不由 Spring 容器管理的对象(例如域对象,通常是),则需要使用 AspectJ。如果您希望通知连接点而不是简单的方法执行(例如,字段获取或设置连接点等),您还需要使用 AspectJ。
当您使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为“代码样式”)或 @AspectJ 注解样式。显然,如果您不使用 Java 5+,那么已经为您做出了选择:使用代码风格。如果切面在您的设计中扮演重要角色,并且您能够使用 Eclipse 的AspectJ 开发工具 (AJDT)插件,那么 AspectJ 语言语法是首选选项。它更简洁,因为该语言是专门为编写切面而设计的。如果您不使用 Eclipse 或只有几个切面在您的应用程序中没有发挥主要作用,您可能需要考虑使用 @AspectJ 样式,在您的 IDE 中坚持常规 Java 编译,并添加一个切面编织阶段你的构建脚本。
5.6.2. 用于 Spring AOP 的 @AspectJ 或 XML?
如果您选择使用 Spring AOP,您可以选择 @AspectJ 或 XML 样式。有各种权衡需要考虑。
现有 Spring 用户可能最熟悉 XML 样式,并且它由真正的 POJO 支持。当使用 AOP 作为配置企业服务的工具时,XML 可能是一个不错的选择(一个很好的测试是您是否将切入点表达式视为您可能想要独立更改的配置的一部分)。使用 XML 样式,可以说从您的配置中更清楚系统中存在哪些切面。
XML 样式有两个缺点。首先,它没有将它所解决的需求的实现完全封装在一个地方。DRY 原则说,系统内的任何知识都应该有一个单一的、明确的、权威的表示。使用 XML 样式时,如何实现需求的知识被拆分为支持 bean 类的声明和配置文件中的 XML。当您使用@AspectJ 样式时,此信息被封装在一个模块中:切面。其次,与@AspectJ 风格相比,XML 风格在表达切面稍有限制:仅支持“单例”切面实例化模型,并且无法组合 XML 中声明的命名切入点。例如,在@AspectJ 样式中,您可以编写如下内容:
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
在 XML 样式中,您可以声明前两个切入点:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
XML 方法的缺点是不能通过组合这些定义来定义 accountPropertyAccess
切入点。
@AspectJ 样式支持额外的实例化模型和更丰富的切入点组合。它具有将切面保持为模块化单元的优点。它还具有以下优点:Spring AOP 和 AspectJ 都可以理解(并因此使用)@AspectJ 切面。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,您可以轻松迁移到经典的 AspectJ 设置。总的来说,Spring 团队更喜欢 @AspectJ 风格的自定义切面,而不是简单的企业服务配置。
5.7. 混合切面类型
通过使用自动代理支持、模式定义的<aop:aspect>
切面、<aop:advisor>
声明的顾问,甚至是相同配置中其他样式的代理和拦截器,完全可以混合@AspectJ 样式切面。所有这些都是通过使用相同的底层支持机制来实现的,并且可以毫无困难地共存。
5.8. 代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。JDK 动态代理内置在 JDK 中,而 CGLIB 是一个通用的开源类定义库(重新打包到spring-core
).
如果要代理的目标对象实现了至少一个接口,则使用 JDK 动态代理。目标类型实现的所有接口都被代理。如果目标对象没有实现任何接口,则创建一个 CGLIB 代理。
如果您想强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),您可以这样做。但是,您应该考虑以下问题:
- 使用 CGLIB,不能通知
final
方法,因为它们不能在运行时生成的子类中被覆盖。 - 从 Spring 4.0 开始,代理对象的构造函数不再被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。仅当您的 JVM 不允许绕过构造函数时,您可能会看到来自 Spring 的 AOP 支持的双重调用和相应的调试日志条目。
要强制使用 CGLIB 代理,请将<aop:config>
元素的proxy-target-class
属性值设置为 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"/>
多个<aop:config/>
部分在运行时被折叠成一个统一的自动代理创建器,它应用任何 <aop:config/>
部分(通常来自不同的 XML bean 定义文件)指定的*最强代理设置。*这也适用于<tx:annotation-driven/>
和<aop:aspectj-autoproxy/>
元素。
需要明确的是,在<tx:annotation-driven/>
, <aop:aspectj-autoproxy/>
,或 <aop:config/>
元素上使用 proxy-target-class="true"
会强制对所有三个元素使用 CGLIB 代理其中。
5.8.1. 了解 AOP 代理
Spring AOP 是基于代理的。在编写自己的切面或使用 Spring Framework 提供的任何基于 Spring AOP 的切面之前,掌握最后一条语句的实际含义是非常重要的。
首先考虑您有一个普通的、未代理的、没有什么特别的、直接的对象引用的场景,如以下代码片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
如果您在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图和清单所示:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
当客户端代码的引用是代理时,情况会发生轻微变化。考虑下面的图表和代码片段:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
这里要理解的关键是Main
类的main(..)
方法内部的客户端代码有对代理的引用。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(通知)。但是,一旦调用最终到达目标对象(在这种情况下为SimplePojo
引用),它可能对自身进行的任何方法调用,例如this.bar()
或 this.foo()
,都将针对this
引用而不是代理调用。这具有重要意义。这意味着自调用不会导致与方法调用相关的通知有机会运行。
好的,那该怎么办呢?最好的方法(术语“最好”在这里被松散地使用)是重构你的代码,这样自调用就不会发生。这确实需要您做一些工作,但它是最好的、侵入性最小的方法。下一种方法绝对可怕,我们不愿指出,正是因为它太可怕了。您可以(对我们来说很痛苦)将您的类中的逻辑完全绑定到 Spring AOP,如以下示例所示:
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
这完全将您的代码与 Spring AOP 耦合在一起,并且它使类本身意识到它是在 AOP 上下文中使用的,而 AOP 上下文与 AOP 相悖。在创建代理时还需要一些额外的配置,如以下示例所示:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
最后需要注意的是,AspectJ 不存在这个自调用问题,因为它不是基于代理的 AOP 框架。
5.9. @AspectJ 代理的程序化创建
除了使用<aop:config>
或<aop:aspectj-autoproxy>
声明配置中的切面之外,还可以以编程方式创建通知目标对象的代理。有关 Spring AOP API 的完整详细信息,请参阅 下一章。在这里,我们希望专注于使用@AspectJ 切面自动创建代理的能力。
您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory
该类为一个或多个@AspectJ 切面通知的目标对象创建代理。这个类的基本用法很简单,如下例所示:
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
有关更多信息,请参阅javadoc。
5.10. 在 Spring 应用程序中使用 AspectJ
到目前为止,我们在本章中介绍的所有内容都是纯 Spring AOP。在本节中,如果您的需求超出了 Spring AOP 单独提供的功能,我们将了解如何使用 AspectJ 编译器或编织器来代替 Spring AOP 或作为 Spring AOP 的补充。
Spring 附带了一个小的 AspectJ 切面库,它在您的发行版中为spring-aspects.jar
. 您需要将其添加到您的类路径中才能使用其中的切面。Using AspectJ to Dependency Inject Domain Objects with Spring和AspectJ 的其他 Spring 切面讨论了这个库的内容以及如何使用它。使用 Spring IoC 配置 AspectJ 切面讨论了如何依赖注入使用 AspectJ 编译器编织的 AspectJ 切面。最后, 在 Spring Framework中使用 AspectJ 进行加载时编织介绍了使用 AspectJ 的 Spring 应用程序的加载时编织。
5.10.1. 使用 AspectJ 通过 Spring 依赖注入域对象
Spring 容器实例化和配置应用程序上下文中定义的 bean。也可以要求 bean 工厂配置一个预先存在的对象,给定包含要应用的配置的 bean 定义的名称。 spring-aspects.jar
包含一个注解驱动的切面,它利用此功能允许对任何对象进行依赖注入。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用 new
操作符以编程方式创建的,或者作为数据库查询的结果由 ORM 工具创建。
@Configurable
注解将一个类标记为符合 Spring 驱动配置的条件。在最简单的情况下,您可以将其纯粹用作标记注解,如以下示例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
当以这种方式用作标记接口时,SpringAccount
通过使用与完全限定类型名称com.xyz.myapp.domain.Account
(由于 bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略id
属性,如以下示例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果要显式指定要使用的原型 bean 定义的名称,可以直接在注解中这样做,如以下示例所示:
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
Spring 现在查找名为的 bean 定义并将account
其用作配置新Account
实例的定义。
您还可以使用自动装配来避免指定专用的 bean 定义。要让 Spring 应用自动装配,请使用@Configurable
注释的 autowire
属性。您可以分别指定 @Configurable(autowire=Autowire.BY_TYPE)
或 @Configurable(autowire=Autowire.BY_NAME)
来按类型或按名称进行自动装配。作为替代方案,最好在字段或方法级别通过 @Autowired
或 @Inject
为 @Configurable beans
指定显式的、注释驱动的依赖项注入(有关更多详细信息,请参阅基于注释的容器配置)。
最后,您可以使用 dependencyCheck
属性(例如 @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)
)为新创建和配置的对象中的对象引用启用 Spring 依赖项检查。如果此属性设置为 true,Spring 将在配置后验证是否已设置所有属性(不是基元或集合)。
请注意,单独使用注释不会产生任何作用。 spring-aspects.jar
中的 AnnotationBeanConfigurerAspect
作用于注释的存在。实质上,该方面说,“从使用 @Configurable
注解的类型的新对象初始化返回后,根据注解的属性使用 Spring 配置新创建的对象”。在此上下文中,“初始化”指的是新实例化的对象(例如,使用 new 运算符实例化的对象)以及正在进行反序列化(例如,通过 readResolve())
的可序列化对象。
上一段中的关键词之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。在这种情况下,“初始化之后”意味着依赖项是在对象构建之后注入的。这意味着依赖项不能在类的构造函数体中使用。如果您希望在构造函数主体运行之前注入依赖项,从而可以在构造函数主体中使用,则需要在 @Configurable
声明中定义这个,如下所示:@Configurable(preConstruction = true)
您可以在AspectJ Programming Guide的这个附录中找到有关 AspectJ 中各种切入点类型的语言语义的更多信息。
为此,必须使用 AspectJ 编织器编织带注解的类型。您可以使用构建时 Ant 或 Maven 任务来执行此操作(例如,参见 AspectJ 开发环境指南)或加载时编织(参见Spring Framework 中使用 AspectJ 的加载时编织)。 AnnotationBeanConfigurerAspect
本身需要由Spring 配置(为了获得对用于配置新对象的 bean 工厂的引用)。如果使用基于 Java 的配置,则可以添加@EnableSpringConfigured
到任何 @Configuration
类中,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
如果您更喜欢基于 XML 的配置,Spring context
命名空间 定义了一个方便的context:spring-configured
元素,您可以按如下方式使用它:
<context:spring-configured/>
在配置切面之前创建的对象实例会@Configurable
导致向调试日志发出消息,并且不会进行对象配置。一个示例可能是 Spring 配置中的 bean,它在 Spring 初始化时创建域对象。在这种情况下,您可以使用 depends-on
bean 属性手动指定 bean 依赖于配置切面。以下示例显示了如何使用该depends-on
属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
不要通过 bean 配置器切面激活@Configurable
处理,除非您真的想在运行时依赖它的语义。特别是,请确保不要在容器中注册为常规 Spring bean 的 bean 类上使用@Configurable
。这样做会导致双重初始化,一次通过容器,一次通过切面。
单元测试@Configurable
对象
@Configurable
支持的目标之一是启用域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果AspectJ 没有编织@Configurable
类型,则注解在单元测试期间没有影响。您可以在被测对象中设置模拟或存根属性引用并正常进行。如果@Configurable
类型已由 AspectJ 编织,您仍然可以像往常一样在容器外进行单元测试,但每次构造@Configurable
对象时都会看到一条警告消息,指示它尚未由 Spring 配置。
使用多个应用程序上下文
AnnotationBeanConfigurerAspect
用于实现支持的@Configurable
是 AspectJ 单例切面。单例切面的范围与static
成员的范围相同:每个类加载器都有一个切面实例来定义类型。这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,您需要考虑在哪里定义@EnableSpringConfigured
bean 以及在spring-aspects.jar
类路径中放置的位置。
考虑一个典型的 Spring Web 应用程序配置,它具有一个共享的父应用程序上下文,它定义了公共业务服务、支持这些服务所需的一切,以及每个 servlet 的一个子应用程序上下文(其中包含特定于该 servlet 的定义)。所有这些上下文共存于同一个类加载器层次结构中,因此AnnotationBeanConfigurerAspect
只能保存对其中一个的引用。在这种情况下,我们推荐在共享(父)应用程序上下文中定义@EnableSpringConfigured
bean。这定义了您可能想要注入到域对象中的服务。结果是您无法使用@Configurable 机制(这可能不是您想要做的事情)来配置域对象,并引用在子(特定于servlet)上下文中定义的bean。
在同一个容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序都使用自己的 ClassLoader 加载 spring-aspects.jar 中的类型(例如,将 spring-aspects.jar 放在 WEB-INF/lib 中)。如果 spring-aspects.jar 仅添加到容器范围的类路径(因此由共享父类加载器加载),则所有 Web 应用程序共享相同的切面实例(这可能不是您想要的)。
5.10.2. AspectJ 的其他 Spring 切面
除了@Configurable
切面之外,spring-aspects.jar
还包含一个 AspectJ 切面,您可以使用它来驱动 Spring 的事务管理,以处理使用注解进行@Transactional
注解的类型和方法。这主要适用于希望在 Spring 容器之外使用 Spring Framework 的事务支持的用户。
@Transactional
注解的切面是 AnnotationTransactionAspect
. 当您使用此切面时,您必须注解实现类(或该类中的方法或两者),而不是该类实现的接口(如果有)。AspectJ 遵循 Java 的规则,即不继承接口上的注解。
类上的@Transactional
注解指定执行类中任何公共操作的默认事务语义。
类中方法的@Transactional
注解会覆盖类注解(如果存在)给出的默认事务语义。可以注解任何可见性的方法,包括私有方法。直接注解非公共方法是获得执行此类方法的事务分界的唯一方法。
从 Spring Framework 4.2 开始,spring-aspects
提供了一个类似的切面,为标准注解javax.transaction.Transactional
提供完全相同的功能。检查 JtaAnnotationTransactionAspect
更多细节。
对于想要使用 Spring 配置和事务管理支持但不想(或不能)使用注解的 AspectJ 程序员,spring-aspects.jar
还包含可以扩展以提供自己的切入点定义的abstract
切面。有关更多信息,请参阅AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
切面的来源。例如,以下摘录显示了如何编写一个切面来配置域模型中定义的所有对象实例,方法是使用与完全限定类名匹配的原型 bean 定义:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
CommonPointcuts.inDomainModel() &&
this(beanInstance);
}
5.10.3. 使用 Spring IoC 配置 AspectJ 切面
当您将 AspectJ 切面与 Spring 应用程序一起使用时,自然希望并期望能够使用 Spring 配置这些切面。AspectJ 运行时本身负责切面创建,通过 Spring 配置 AspectJ 创建的切面的方式取决于切面使用的 AspectJ 实例化模型(per-xxx
子句)。
大多数 AspectJ 切面都是单例切面。这些切面的配置很容易。您可以创建一个引用切面类型的 bean 定义,并包含factory-method="aspectOf"
bean 属性。这确保 Spring 通过向 AspectJ 请求它而不是尝试自己创建实例来获取切面实例。以下示例显示了如何使用factory-method="aspectOf"
属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
非单例方面更难配置。但是,可以通过创建原型 bean 定义并使用 spring-aspects.jar 中的 @Configurable 支持来配置方面实例(一旦 AspectJ 运行时创建了 bean),就可以实现这一点。
如果你有一些@AspectJ 切面想用AspectJ 编织(例如,对域模型类型使用加载时编织)和其他@AspectJ 切面想和Spring AOP 一起使用,并且这些切面都在Spring 中配置,您需要告诉 Spring AOP @AspectJ 自动代理支持配置中定义的 @AspectJ 切面的确切子集应该用于自动代理。 您可以通过在<aop:aspectj-autoproxy/>
声明中使用一个或多个<include/>
元素来做到这一点。每个<include/>
元素指定一个名称模式,只有名称与至少一个模式匹配的 bean 才会用于 Spring AOP 自动代理配置。以下示例显示了如何使用<include/>
元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被<aop:aspectj-autoproxy/>
元素的名称误导。使用它会导致创建 Spring AOP 代理。这里使用了@AspectJ 样式的切面声明,但不涉及 AspectJ 运行时。
5.10.4. 在 Spring 框架中使用 AspectJ 进行加载时编织
加载时编织 (LTW) 是指将 AspectJ 切面编织到应用程序的类文件中的过程,因为它们正在加载到 Java 虚拟机 (JVM) 中。本节的重点是在 Spring Framework 的特定上下文中配置和使用 LTW。本节不是对 LTW 的一般介绍。有关 LTW 的详细信息以及仅使用 AspectJ 配置 LTW(根本不涉及 Spring)的详细信息,请参阅 AspectJ 开发环境指南的 LTW 部分。
Spring Framework 为 AspectJ LTW 带来的价值在于能够对编织过程进行更细粒度的控制。‘Vanilla’ AspectJ LTW 是通过使用 Java (5+) 代理来实现的,该代理在启动 JVM 时通过指定 VM 参数来打开。因此,它是一个 JVM 范围的设置,在某些情况下可能很好,但通常有点过于粗糙。启用 Spring 的 LTW 允许您逐个打开 LTW ClassLoader
,这更细粒度,并且在“单 JVM 多应用程序”环境中更有意义(例如在典型的应用程序服务器环境中) )。
此外,在某些环境中,此支持支持加载时编织,而无需对需要添加-javaagent:path/to/aspectjweaver.jar
或的应用程序服务器的启动脚本进行任何修改(正如我们在本节后面描述的那样)-javaagent:path/to/spring-instrument.jar
。开发人员配置应用程序上下文以启用加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。
现在推销已经结束,让我们先来看一个使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中介绍的元素。有关完整示例,请参阅 Petclinic 示例应用程序。
第一个例子
假设您是一名应用程序开发人员,他的任务是诊断系统中某些性能问题的原因。与其打破一个分析工具,我们将打开一个简单的分析切面,让我们快速获得一些性能指标。然后,我们可以立即将更细粒度的分析工具应用于该特定区域。
此处提供的示例使用 XML 配置。您还可以通过Java 配置配置和使用 @AspectJ 。具体来说,您可以使用 @EnableLoadTimeWeaving
注解作为<context:load-time-weaver/>
替代 (详见下文)。
以下示例显示了分析切面,这并不花哨。它是一个基于时间的分析器,使用@AspectJ 风格的切面声明:
package foo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;
@Aspect
public class ProfilingAspect {
@Around("methodsToBeProfiled()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
sw.start(pjp.getSignature().getName());
return pjp.proceed();
} finally {
sw.stop();
System.out.println(sw.prettyPrint());
}
}
@Pointcut("execution(public * foo..*.*(..))")
public void methodsToBeProfiled(){}
}
我们还需要创建一个META-INF/aop.xml
文件,通知 AspectJ 编织器我们想要将我们的类编织ProfilingAspect
到我们的类中。这种文件约定,即在 Java 类路径中存在一个文件(或多个文件)称为META-INF/aop.xml
标准 AspectJ。以下示例显示了该aop.xml
文件:
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<!-- only weave classes in our application-specific packages -->
<include within="foo.*"/>
</weaver>
<aspects>
<!-- weave in just this aspect -->
<aspect name="foo.ProfilingAspect"/>
</aspects>
</aspectj>
现在我们可以继续进行配置的特定于 Spring 的部分。我们需要配置一个LoadTimeWeaver
(稍后解释)。这个加载时编织器是负责将一个或多个META-INF/aop.xml
文件中的切面配置编织到应用程序中的类中的基本组件。好处是它不需要太多的配置(还有一些选项可以指定,但后面会详细介绍),如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- a service object; we will be profiling its methods -->
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>
<!-- this switches on the load-time weaving -->
<context:load-time-weaver/>
</beans>
现在所有必需的工件(切面、META-INF/aop.xml
文件和 Spring 配置)都已就位,我们可以创建以下驱动程序类,并使用一个main(..)
方法来演示 LTW 的实际操作:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
(EntitlementCalculationService) ctx.getBean("entitlementCalculationService");
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
我们还有最后一件事要做。ClassLoader
本节的介绍确实说过,可以根据 Spring有选择地打开 LTW ,这是真的。但是,对于本示例,我们使用 Java 代理(随 Spring 提供)来打开 LTW。我们使用以下命令来运行Main
前面显示的类:
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
-javaagent
是一个标志,用于指定并启用代理来检测在 JVM 上运行的程序。 Spring 框架附带了这样一个代理,InstrumentationSavingAgent
,它打包在 spring-instrument.jar
中,该 jar 在前面的示例中作为-javaagent
参数的值提供。
程序执行的输出Main
类似于下一个示例。(我在实现中引入了一条Thread.sleep(..)
语句,calculateEntitlement()
以便探查器实际上捕获 0 毫秒以外的时间(01234
毫秒不是 AOP 引入的开销)。以下清单显示了我们在运行探查器时得到的输出:
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
由于这个 LTW 是通过使用成熟的 AspectJ 来实现的,因此我们不仅限于通知 Spring bean。该Main
程序的以下细微变化会产生相同的结果:
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
请注意,在前面的程序中,我们如何引导 Spring 容器,然后创建一个完全在 Spring 上下文之外的新StubEntitlementCalculationService
实例。分析通知仍然被融入其中。
诚然,这个例子很简单。但是,Spring 中 LTW 支持的基础知识已经在前面的示例中介绍过,本节的其余部分将详细解释每一位配置和使用背后的“原因”。
这个ProfilingAspect
例子中使用的可能是基本的,但它非常有用。这是开发时切面的一个很好的例子,开发人员可以在开发期间使用它,然后轻松地从部署到 UAT 或生产中的应用程序的构建中排除。
切面
您在 LTW 中使用的切面必须是 AspectJ 切面。您可以使用 AspectJ 语言本身编写它们,也可以使用 @AspectJ 样式编写切面。那么你的切面都是有效的 AspectJ 和 Spring AOP 切面。此外,编译的切面类需要在类路径上可用。
‘META-INF/aop.xml’
AspectJ LTW 基础结构是通过使用META-INF/aop.xml
Java 类路径中的一个或多个文件(直接或者更典型地在 jar 文件中)来配置的。
该文件的结构和内容在 AspectJ 参考文档的 LTW 部分中有详细说明。因为该aop.xml
文件是 100% AspectJ,所以我们在此不再赘述。
所需的库 (JARS)
至少,您需要以下库来使用 Spring Framework 对 AspectJ LTW 的支持:
spring-aop.jar
aspectjweaver.jar
如果使用Spring 提供的代理来启用检测,还需要:
spring-instrument.jar
spring 配置
Spring 的 LTW 支持中的关键组件是LoadTimeWeaver
接口(在 org.springframework.instrument.classloading
包中),以及 Spring 发行版附带的众多实现。LoadTimeWeaver
负责在运行时向 java.lang.instrument.ClassFileTransformers
添加一个或多个ClassLoader
,这为各种有趣的应用程序打开了大门,其中之一恰好是切面的 LTW。
如果您不熟悉运行时类文件转换的想法,请java.lang.instrument
在继续之前查看包的 javadoc API 文档。虽然该文档并不全面,但至少您可以看到关键接口和类(供您阅读本节时参考)。
LoadTimeWeaver
为特定配置 ApplicationContext
可以像添加一行一样简单。(请注意,您几乎肯定需要使用 ApplicationContext
作为 Spring 容器——通常, BeanFactory
是不够的,因为 LTW 支持使用BeanFactoryPostProcessors
.)
要启用 Spring Framework 的 LTW 支持,您需要配置一个LoadTimeWeaver
,这通常通过使用@EnableLoadTimeWeaving
注解来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
或者,如果您更喜欢基于 XML 的配置,请使用该 <context:load-time-weaver/>
元素。请注意,该元素是在 context
命名空间中定义的。下面的例子展示了如何使用<context:load-time-weaver/>
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver/>
</beans>
前面的配置会自动为您定义和注册许多 LTW 特定的基础设施 bean,例如 LoadTimeWeaver
和 AspectJWeavingEnabler
。默认LoadTimeWeaver
是DefaultContextLoadTimeWeaver
类,它试图装饰一个自动检测到的LoadTimeWeaver
. “自动检测”的确切类型LoadTimeWeaver
取决于您的运行时环境。下表总结了各种LoadTimeWeaver
实现:
运行环境 | LoadTimeWeaver 执行 |
---|---|
在Apache Tomcat中运行 | TomcatLoadTimeWeaver |
在GlassFish中运行(仅限于 EAR 部署) | GlassFishLoadTimeWeaver |
在 Red Hat 的JBoss AS或WildFly中运行 | JBossLoadTimeWeaver |
在 IBM 的WebSphere中运行 | WebSphereLoadTimeWeaver |
在 Oracle 的 WebLogic中运行 | WebLogicLoadTimeWeaver |
JVM 始于 Spring InstrumentationSavingAgent ( java -javaagent:path/to/spring-instrument.jar ) |
InstrumentationLoadTimeWeaver |
回退,期望底层的 ClassLoader 遵循通用约定(即addTransformer ,可选的getThrowawayClassLoader 方法) |
ReflectiveLoadTimeWeaver |
请注意,该表仅列出了使用 DefaultContextLoadTimeWeaver
时自动检测到的 LoadTimeWeaver
。您可以准确指定要使用的 LoadTimeWeaver
实现。
要指定特定LoadTimeWeaver
的 Java 配置,请实现 LoadTimeWeavingConfigurer
接口并覆盖getLoadTimeWeaver()
方法。以下示例指定了一个ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
如果使用基于 XML 的配置,则可以将完全限定的类名指定为 <context:load-time-weaver/>
元素的weaver-class
属性值。同样,以下示例指定了一个ReflectiveLoadTimeWeaver
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
LoadTimeWeaver
稍后可以使用众所周知的名称从 Spring 容器中检索由配置定义和注册的loadTimeWeaver
. 请记住,它LoadTimeWeaver
仅作为 Spring 的 LTW 基础架构添加一个或多个ClassFileTransformers
. 执行 LTW的实际 ClassFileTransformer
是ClassPreProcessorAgentAdapter
(来自org.aspectj.weaver.loadtime
包的)类。有关更多详细信息,请参阅该类的类级别 javadoc ClassPreProcessorAgentAdapter
,因为实际如何实现编织的细节超出了本文档的范围。
还有一个配置的最后一个属性需要讨论:aspectjWeaving
属性(或者aspectj-weaving
如果您使用 XML)。此属性控制是否启用 LTW。它接受三个可能的值之一,默认值是 autodetect
如果属性不存在。下表总结了三个可能的值:
注解值 | XML 值 | 解释 |
---|---|---|
ENABLED |
on |
AspectJ weaving 已打开,并且在加载时适当地编织切面。 |
DISABLED |
off |
LTW 已关闭。在加载时没有编织任何切面。 |
AUTODETECT |
autodetect |
如果 Spring LTW 基础结构可以找到至少一个META-INF/aop.xml 文件,则 AspectJ weaving 处于打开状态。否则,它会关闭。这是默认值。 |
特定于环境的配置
最后一部分包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的任何其他设置和配置。
Tomcat、JBoss、WebSphere、WebLogic
Tomcat、JBoss/WildFly、IBM WebSphere Application Server 和 Oracle WebLogic Server 都提供了一个ClassLoader
能够进行本地检测的通用应用程序。Spring 的本机 LTW 可以利用这些 ClassLoader 实现来提供 AspectJ 编织。如前所述,您可以简单地启用加载时编织。具体来说,您无需修改 JVM 启动脚本即可添加: -javaagent:path/to/spring-instrument.jar
.
请注意,在 JBoss 上,您可能需要禁用应用服务器扫描,以防止它在应用程序实际启动之前加载类。一个快速的解决方法是向您的工件添加一个文件,WEB-INF/jboss-scanning.xml
文件以以下内容命名:
<scanning xmlns="urn:jboss:scanning:1.0"/>
通用 Java 应用程序
当在特定实现不支持的环境中需要类检测时LoadTimeWeaver
,JVM 代理是通用解决方案。对于这种情况,Spring 提供了 InstrumentationLoadTimeWeaver
,它需要特定于 Spring 的(但非常通用的)JVM 代理 spring-instrument.jar,由常见的 @EnableLoadTimeWeaving
和 <context:load-time-weaver/>
设置自动检测。
要使用它,您必须通过提供以下 JVM 选项来使用 Spring 代理启动虚拟机:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改 JVM 启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的服务器和操作策略)。也就是说,对于每个 JVM 一个应用程序的部署,例如独立的 Spring Boot 应用程序,您通常在任何情况下都可以控制整个 JVM 设置。
5.11.更多资源
有关 AspectJ 的更多信息,请访问 AspectJ 网站。
Adrian Colyer 等人的Eclipse AspectJ。人。(Addison-Wesley, 2005) 为 AspectJ 语言提供了全面的介绍和参考。
强烈推荐 Ramnivas Laddad(Manning,2009 年)的*AspectJ in Action ,第二版。*本书的重点是 AspectJ,但也探讨了很多通用的 AOP 主题(在一定程度上)。
评论区