Appearance
Spring 核心:AOP - Part1
Tip:基于 Spring Core 5.3.30 版本。
1. AOP 概念
让我们首先定义一些 AOP 的核心概念和术语。这些术语并不特定于 Spring。不幸的是,AOP 的术语并不特别直观。然而,如果 Spring 使用自己的术语,那就会更加混乱。
切面(Aspect):一个关注点的模块化,它横切多个类。事务管理是企业 Java 应用中一个很好的横切关注点的例子。在 Spring AOP 中,切面是通过使用常规类(基于 Schema 的方法)或用
@Aspect注解标注的常规类来实现的(@AspectJ风格)。连接点(Join Point):程序执行期间的一个点,比如一个方法的执行或一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法的执行。
增强(Advice):切面在特定连接点采取的行动。不同类型的增强包括 “环绕”、“前置” 和 “后置” 增强(增强类型将在后面讨论)。许多 AOP 框架,包括 Spring,将增强模型化为拦截器,并在连接点周围维护一个拦截器链。
切点(Pointcut):匹配连接点的谓词。增强与切点表达式相关联,并在任何由切点匹配的连接点(例如,执行具有某个名称的方法)运行。连接点作为由切点表达式匹配的概念是 AOP 的核心,Spring 默认使用 AspectJ 切点表达式语言。
引入(Introduction):代表一个类型声明额外的方法或字段。Spring AOP 允许你向任何被增强的对象引入新的接口(以及相应的实现)。例如,你可以使用引入使一个 Bean 实现
IsModified接口,以简化缓存。(在 AspectJ 社区,引入被称为类型间声明。)目标对象(Target Object):被一个或多个切面增强的对象。也被称为 “被增强对象”。由于 Spring AOP 是通过使用运行时代理来实现的,所以这个对象总是一个被代理的对象。
AOP 代理(AOP Proxy):由 AOP 框架创建的对象,用于实现切面契约(增强方法执行等)。在 Spring 框架中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
织入(Weaving):将切面与其他应用类型或对象链接,以创建一个被增强的对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP,像其他纯 Java AOP 框架一样,在运行时进行织入。
Spring AOP 包括以下类型的增强:
前置增强(Before Advice):在连接点之前运行的增强,但它没有能力阻止执行流程继续到连接点(除非它抛出一个异常)。
返回后增强(After Returning Advice):在连接点正常完成后运行的增强(例如,如果一个方法在没有抛出异常的情况下返回)。
抛出后增强(After Throwing Advice):如果一个方法通过抛出异常退出,则运行的增强。
最终增强(After/Finally Advice):无论连接点通过何种方式退出(正常返回或异常返回),都要运行的增强。
环绕增强(Around Advice):环绕连接点(如方法调用)的增强。这是最强大的一种增强。环绕增强可以在方法调用之前和之后执行自定义行为。它还负责选择是否继续到连接点,或者通过返回其自己的返回值或抛出异常来直接结束被增强的方法执行。
环绕增强是最通用的一种增强。由于 Spring AOP 和 AspectJ 一样,提供了全套的增强类型,我们建议你使用能实现所需行为的最弱的增强类型。例如,如果你只需要用一个方法的返回值更新缓存,那么实现一个返回后增强比实现一个环绕增强更好,尽管环绕增强也可以完成同样的事情。使用最具体的增强类型提供了一个更简单的编程模型,减少了出错的可能性。例如,你不需要在用于环绕增强的 JoinPoint 上调用 proceed() 方法,因此,你不可能忘记调用它。
所有的增强参数(Advice Parameters)都是静态类型的,这样你就可以使用适当类型的增强参数(例如,从方法执行中返回的类型)而不是 Object 数组。
由切点匹配的连接点的概念是 AOP 的关键,这使得它与只提供拦截的旧技术区别开来。切点使得增强可以独立于面向对象的层次结构进行定位。例如,你可以将提供声明式事务管理的环绕增强应用到跨多个对象的一组方法上(如服务层的所有业务操作)。
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 为大多数适合 AOP 的企业 Java 应用问题提供了出色的解决方案。
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。
Note
Spring 框架的核心原则之一是非侵入性。这是一种观念,即你不应被迫在你的业务或领域模型中引入特定于框架的类和接口。然而,在某些地方,Spring 框架确实给你选择引入特定于 Spring 框架的依赖项到你的代码库的选项。给你这样的选项的理由是因为,在某些场景下,以这样的方式阅读或编码某些特定的功能可能会更简单。然而,Spring 框架(几乎)总是给你选择:你有自由做出知情决策,以确定哪个选项最适合你特定的用例或场景。
与本章相关的一个选择是选择哪个 AOP 框架(以及哪种 AOP 风格)。你可以选择 AspectJ,Spring AOP,或者两者都选择。你还可以选择
@AspectJ注解风格的方法或者 Spring XML 配置风格的方法。这一章选择首先介绍@AspectJ风格的方法,这并不应被视为 Spring 团队更倾向于@AspectJ注解风格的方法,而不是 Spring XML 配置风格。请参阅选择使用哪种 AOP 声明风格以获取关于每种风格的 “为什么和怎么做” 的更完整讨论。
3. AOP 代理
Spring AOP 默认使用标准的 JDK 动态代理作为 AOP 代理。这使得任何接口(或接口集)都可以被代理。
Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象没有实现接口,就会使用 CGLIB。由于按照接口而不是类进行编程是一种好的实践,因此业务类通常会实现一个或多个业务接口。在那些(希望是罕见的)情况下,你需要增强一个没有在接口上声明的方法,或者你需要将一个被代理的对象作为具体类型传递给方法,你可以强制使用 CGLIB。
理解 Spring AOP 是基于代理的这个事实是很重要的。请参阅理解 AOP 代理以深入了解这个实现细节究竟意味着什么。
4. @AspectJ 支持
@AspectJ 指的是一种以注解标注的常规 Java 类来声明切面的风格。@AspectJ 风格是由 AspectJ 项目在 AspectJ 5 发布时引入的。Spring 解析的注解与 AspectJ 5 相同,使用由 AspectJ 提供的库进行切点解析和匹配。尽管如此,AOP 运行时仍然是纯粹的 Spring AOP,并且不依赖于 AspectJ 编译器或织入器。
Note:使用 AspectJ 编译器和织入器可以使用完整的 AspectJ 语言,这在在 Spring 应用中使用 AspectJ 中有讨论。
4.1. 启用 @AspectJ 支持
要在 Spring 配置中使用 @AspectJ 切面,你需要启用 Spring 对基于 @AspectJ 切面的 Spring AOP 的配置支持,并根据 Bean 是否被这些切面所增强来自动代理 Bean。所谓自动代理,我们的意思是,如果 Spring 确定一个 Bean 被一个或多个切面所增强,它会自动为该 Bean 生成一个代理,以拦截方法调用,并确保在需要时运行增强。
@AspectJ 支持可以通过 XML 或 Java 风格的配置来启用。无论哪种情况,你都需要确保 AspectJ 的 aspectjweaver.jar 库在你的应用程序的类路径上(版本 1.8 或更高)。这个库可以在 AspectJ 分发的 lib 目录中找到,或者从 Maven Central 仓库获取。
4.1.1. 使用 Java 配置启用 @AspectJ 支持
要使用 Java 的 @Configuration 启用 @AspectJ 支持,添加 @EnableAspectJAutoProxy 注解,如下面的示例所示:
Java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}1
2
3
4
5
2
3
4
5
4.1.2. 使用 XML 配置启用 @AspectJ 支持
要使用基于 XML 的配置启用 @AspectJ 支持,使用 aop:aspectj-autoproxy 元素,如下面的示例所示:
XML
<aop:aspectj-autoproxy/>这假设你使用了如基于 XML Schema 的配置中描述的 Schema 支持。请参阅 AOP Schema,了解如何导入 aop 命名空间中的标签。
4.2. 声明切面
启用了 @AspectJ 支持后,你的应用程序上下文中定义的任何 Bean,只要它的类是一个 @AspectJ 切面(有 @Aspect 注解),都会被 Spring 自动检测并用于配置 Spring AOP。接下来的两个示例展示了一个不太有用的切面所需的最小定义。
这两个示例中的第一个示例展示了应用程序上下文中的一个常规 Bean 定义,该定义指向一个具有 @Aspect 注解的 Bean 类:
XML
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>1
2
3
2
3
这两个示例中的第二个示例展示了 NotVeryUsefulAspect 类的定义,该定义使用了 org.aspectj.lang.annotation.Aspect 注解进行注解。
Java
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}1
2
3
4
5
6
7
2
3
4
5
6
7
切面(用 @Aspect 注解的类)可以拥有方法和字段,就像其他任何类一样。它们还可以包含切点、增强和引入(类型间)声明。
Tip:通过组件扫描自动检测切面
你可以将切面类注册为 Spring XML 配置中的常规 Bean,通过
@Configuration类中的@Bean方法,或者让 Spring 通过类路径扫描自动检测它们 —— 就像任何其他 Spring 管理的 Bean 一样。然而,请注意,@Aspect注解对于类路径中的自动检测是不够的。为此,你需要添加一个单独的@Component注解(或者,根据 Spring 的组件扫描器的规则,选择一个合格的自定义原型注解)。
Note:切面可以用其他切面进行增强吗?
在 Spring AOP 中,切面本身不能成为其他切面的增强目标。在类上的
@Aspect注解将其标记为切面,因此,它被排除在自动代理之外。
4.3. 声明切点
切点确定了我们感兴趣的连接点,从而使我们能够控制何时运行增强。Spring AOP 只支持 Spring Bean 的方法执行连接点,所以你可以将切点看作是匹配 Spring Bean 上的方法执行。切点声明有两部分:由名称和任何参数组成的签名,以及确定我们对哪些方法执行感兴趣的切点表达式。在 @AspectJ 注解风格的 AOP 中,切点签名由常规方法定义提供,切点表达式通过使用 @Pointcut 注解来指示(作为切点签名的方法必须有 void 返回类型)。
一个例子可能有助于明确切点签名和切点表达式之间的区别。下面的例子定义了一个名为 anyOldTransfer 的切点,它匹配任何名为 transfer 的方法的执行:
Java
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature1
2
2
构成 @Pointcut 注解值的切点表达式是一个常规的 AspectJ 切点表达式。要全面讨论 AspectJ 的切点语言,请参阅 AspectJ 编程指南(及扩展,AspectJ 5 开发者笔记)或者 AspectJ 的一本书(如 Colyer 等人的《Eclipse AspectJ》,或者 Ramnivas Laddad 的《AspectJ 实战》)。
4.3.1. 支持的切点指示器
Spring AOP 支持以下 AspectJ 切点指示器(PCD,Pointcut Designators)用于切点表达式:
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 中运行的方法)具有给定注解的地方。
Note:其他类型的切点
完整的 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)。
Note
由于 Spring 的 AOP 框架基于代理,因此在目标对象内部的调用在定义上不会被拦截。对于 JDK 代理,只有代理上的公共接口方法调用可以被拦截。使用 CGLIB,代理上的公共和受保护的方法调用会被拦截(如果必要,甚至包可见的方法也会被拦截)。然而,通过代理的常见交互应始终通过公共签名设计。
注意,切点定义通常与任何被拦截的方法匹配。如果一个切点严格意味着只能是公共的,即使在 CGLIB 代理场景中可能存在通过代理的非公共交互,也需要相应地定义它。
如果你的拦截需求包括目标类中的方法调用甚至构造函数,那么你应该考虑使用 Spring 驱动的原生 AspectJ 织入,而不是 Spring 的基于代理的 AOP 框架。这构成了 AOP 使用的不同模式,具有不同的特性,所以在做决定之前,一定要熟悉织入。
Spring AOP 还支持一个名为 bean 的额外 PCD。这个 PCD 允许你将连接点的匹配限制在特定的命名 Spring Bean 或一组命名的 Spring Bean(使用通配符时)上。bean PCD 的形式如下:
Java
bean(idOrNameOfBean)idOrNameOfBean 令牌可以是任何 Spring Bean 的名称。提供了使用 * 字符的有限通配符支持,所以,如果你为你的 Spring Bean 建立了一些命名规则,你可以编写一个 bean PCD 表达式来选择它们。就像其他切点指示器一样,bean PCD 也可以与 &&(和)、||(或)和 !(否)运算符一起使用。
Note
beanPCD 仅在 Spring AOP 中受支持,而不在原生 AspectJ 织入中受支持。它是一个针对 AspectJ 定义的标准 PCD 的 Spring 特定扩展,因此,在@Aspect模型中声明的切面中不可用。
beanPCD 在实例级别运行(基于 Spring Bean 名称概念),而不仅仅是在类型级别(这是基于织入的 AOP 所限制的)。基于实例的切点指示器是 Spring 的基于代理的 AOP 框架和其与 Spring Bean 工厂的紧密集成的特殊能力,在其中,通过名称识别特定的 Bean 是自然和直接的。
4.3.2. 组合切点表达式
你可以使用 &&、|| 和 ! 来组合切点表达式。你也可以通过名称引用切点表达式。以下示例显示了三个切点表达式:
Java
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
anyPublicOperation匹配:如果一个方法的执行连接点代表任何公共方法的执行。inTrading匹配:如果一个方法的执行在交易模块中。tradingOperation匹配:如果一个方法的执行代表交易模块中的任何公共方法。
如前所示,将复杂的切点表达式分解为较小的命名组件是一种最佳实践。在通过名称引用切点时,会应用常规的 Java 可见性规则(在同一类型中可以看到私有的切点,在层次结构中可以看到受保护的切点,在任何地方都可以看到公共的切点,等等)。可见性并不会影响切点的匹配。
4.3.3. 共享常见的切点定义
在处理企业应用程序时,开发者通常希望从几个方面引用应用程序的模块和特定的操作集。我们建议定义一个 CommonPointcuts 切面,用于捕获常见的切点表达式。这样的切面通常如下例所示:
Java
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() {}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
你可以在任何需要切点表达式的地方引用这样一个切面中定义的切点。例如,要使服务层具有事务性,你可以编写如下内容:
XML
<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>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<aop:config> 和 <aop:advisor> 元素在基于 Schema 的 AOP 支持中有所讨论。事务元素在事务管理中有所讨论。
4.3.4. 示例
Spring AOP 的用户最常使用的可能是 execution 切点指示器。execution 表达式的格式如下:
ABNF
execution-pointcut = "execution(" [modifiers-pattern wsp] ret-type-pattern wsp
[declaring-type-pattern "."] name-pattern "(" param-pattern ")"
[throws-pattern] ")"1
2
3
2
3
除了返回类型模式(前述片段中的 ret-type-pattern)、名称模式和参数模式之外,所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,才能匹配到连接点。* 是最常用的返回类型模式,它匹配任何返回类型。只有当方法返回给定类型时,完全限定的类型名称才会匹配。名称模式匹配方法名称。你可以使用 * 通配符作为名称模式的全部或部分。如果你指定了声明类型模式,包括一个尾随的 . 来将其连接到名称模式组件。参数模式稍微复杂一些:() 匹配一个不带参数的方法,而 (..) 匹配任意数量(零个或多个)的参数。(*) 模式匹配一个接受任意类型一个参数的方法。(*,String) 匹配一个接受两个参数的方法。第一个参数可以是任意类型,而第二个参数必须是 String。有关更多信息,请参阅 AspectJ 编程指南的语言语义部分。
以下示例展示了一些常见的切点表达式:
执行任何公共方法:
Textexecution(public * *(..))执行任何以
set开头的方法:Textexecution(* set*(..))执行
AccountService接口定义的任何方法:Textexecution(* com.xyz.service.AccountService.*(..))执行在
service包中定义的任何方法:Textexecution(* com.xyz.service.*.*(..))执行在
service包或其子包中定义的任何方法:Textexecution(* com.xyz.service..*.*(..))在
service包中的任何连接点(仅在 Spring AOP 中的方法执行):Textwithin(com.xyz.service.*)在
service包或其子包中的任何连接点(仅在 Spring AOP 中的方法执行):Textwithin(com.xyz.service..*)代理实现了
AccountService接口的任何连接点(仅在 Spring AOP 中的方法执行):Textthis(com.xyz.service.AccountService)Note:
this更常用于绑定形式。请参阅声明增强部分,了解如何在增强主体中使代理对象可用。目标对象实现了
AccountService接口的任何连接点(仅在 Spring AOP 中的方法执行):Texttarget(com.xyz.service.AccountService)Note:
target更常用于绑定形式。请参阅声明增强部分,了解如何在增强主体中使用目标对象。接受单个参数且在运行时传递的参数是
Serializable的任何连接点(仅在 Spring AOP 中的方法执行):Textargs(java.io.Serializable)Note:
args更常用于绑定形式。请参阅声明增强部分,了解如何在增强主体中使用方法参数。Warning:这个示例中给出的切点与
execution(* *(java.io.Serializable))不同。args版本匹配的是运行时传递的参数是Serializable,而execution版本匹配的是方法签名声明了一个类型为Serializable的单个参数。目标对象具有
@Transactional注解的任何连接点(仅在 Spring AOP 中的方法执行):Text@target(org.springframework.transaction.annotation.Transactional)Note:你也可以在绑定形式中使用
@target。请参阅声明增强部分,了解如何在增强主体中使用注解对象。目标对象的声明类型具有
@Transactional注解的任何连接点(仅在 Spring AOP 中的方法执行):Text@within(org.springframework.transaction.annotation.Transactional)Note:你也可以在绑定形式中使用
@within。请参阅声明增强部分,了解如何在增强主体中使用注解对象。执行方法具有
@Transactional注解的任何连接点(仅在 Spring AOP 中的方法执行):Text@annotation(org.springframework.transaction.annotation.Transactional)Note:你也可以在绑定形式中使用
@annotation。请参阅声明增强部分,了解如何在增强主体中使用注解对象。接受单个参数且运行时传递的参数类型具有
@Classified注解的任何连接点(仅在 Spring AOP 中的方法执行):Text@args(com.xyz.security.Classified)Note:你也可以在绑定形式中使用
@args。请参阅声明增强部分,了解如何在增强主体中使用注解对象。在 Spring Bean 名为
tradeService上的任何连接点(仅在 Spring AOP 中的方法执行):Textbean(tradeService)在 Spring Bean 的名称匹配通配符表达式
*Service的任何连接点(仅在 Spring AOP 中的方法执行):Textbean(*Service)
4.3.5. 编写良好的切点
在编译过程中,AspectJ 处理切点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态地)匹配给定的切点是一个耗费资源的过程。(动态匹配意味着无法完全通过静态分析确定匹配,需要在代码中放置一个测试来确定代码运行时是否真正匹配)。在首次遇到切点声明时,AspectJ 将其重写为匹配过程的最优形式。这是什么意思呢?基本上,切点被重写为 DNF(Disjunctive Normal Form,析取范式),并且切点的组件被排序,以便首先检查那些评估成本较低的组件。这意味着你不必担心理解各种切点指示器的性能,并且可以在切点声明中以任何顺序提供它们。
然而,AspectJ 只能处理它被告知的内容。为了优化匹配性能,你应该思考他们试图实现什么,并尽可能在定义中缩小匹配的搜索空间。现有的指示器自然分为三组:种类、范围和上下文:
- 种类指示器选择特定种类的连接点:
execution、get、set、call和handler; - 范围指示器选择一组感兴趣的连接点(可能有很多种):
within和withincode; - 上下文指示器基于上下文匹配(并可选地绑定):
this、target和@annotation;
一个写得好的切点应该至少包括前两种类型(种类和范围)。你可以包括上下文指示器来基于连接点上下文进行匹配或绑定该上下文以在增强中使用。只提供一种种类指示器或只提供一种上下文指示器是可以的,但可能会影响织入性能(耗费时间和内存),因为需要额外的处理和分析。范围指示器匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地排除不应进一步处理的连接点组,一个好的切点应该尽可能总是包括一个。
4.4. 声明增强
增强与切点表达式相关联,并在与切点匹配的方法执行之前、之后或者围绕其执行。切点表达式可以是对命名切点的简单引用,也可以是在原地声明的切点表达式。
4.4.1. 前置增强
你可以使用 @Before 注解在切面中声明前置增强:
Java
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() {
// ...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
如果我们使用原地切点表达式,我们可以将前面的示例重写为以下示例:
Java
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() {
// ...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
4.4.2. 返回后增强
当匹配的方法执行正常返回时,返回后增强会运行。你可以使用 @AfterReturning 注解来声明它:
Java
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() {
// ...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Note:你可以在同一个切面中有多个增强声明(以及其他成员)。在这些示例中,我们只展示了一个增强声明,以便集中展示每一个的效果。
有时,你需要在增强体中访问实际返回的值。你可以使用绑定返回值的 @AfterReturning 形式来获取该访问权限,如下面的示例所示:
Java
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) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在 returning 属性中使用的名称必须对应增强方法中的一个参数的名称。当方法执行返回时,返回值作为相应的参数值传递给增强方法。returning 子句还限制只匹配那些返回指定类型值的方法执行(在这种情况下 Object 匹配任何返回值)。
请注意,使用返回后增强时,不可能返回一个完全不同的引用。
4.4.3. 抛出异常后增强
当匹配的方法执行通过抛出异常退出时,抛出异常后增强会运行。你可以使用 @AfterThrowing 注解来声明它,如下面的示例所示:
Java
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() {
// ...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
通常,你希望只有在抛出给定类型的异常时增强才会运行,并且你经常需要在增强体中访问抛出的异常。你可以使用 throwing 属性来既限制匹配(如果需要的话,否则使用 Throwable 作为异常类型)又将抛出的异常绑定到增强参数。以下示例展示了如何做到这一点:
Java
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) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在 throwing 属性中使用的名称必须对应增强方法中的一个参数的名称。当方法执行通过抛出异常退出时,异常作为相应的参数值传递给增强方法。throwing 子句还限制只匹配那些抛出指定类型异常的方法执行(在这种情况下,为 DataAccessException)。
Note:请注意,
@AfterThrowing并不表示一般的异常处理回调。具体来说,@AfterThrowing增强方法只应接收来自连接点(用户声明的目标方法)本身的异常,而不是来自伴随的@After/@AfterReturning方法的异常。
4.4.4. 最终增强
最终增强在匹配的方法执行退出时运行。它是通过使用 @After 注解来声明的。最终增强必须准备好处理正常和异常返回条件。它通常用于释放资源和类似的目的。以下示例展示了如何使用最终增强:
Java
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() {
// ...
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Note:请注意,AspectJ 中的
@After增强被定义为 “最终增强”,类似于 try-catch 语句中的finally块。无论结果如何,正常返回还是从连接点(用户声明的目标方法)抛出的异常,它都会被调用,这与@AfterReturning形成对比,后者只适用于成功的正常返回。
4.4.5. 环绕增强
最后一种增强是环绕增强。环绕增强 “环绕” 着匹配的方法的执行运行。它有机会在方法运行之前和之后进行工作,并决定方法何时、如何,甚至是否真的运行。如果你需要以线程安全的方式在方法执行之前和之后共享状态,那么通常会使用环绕增强 —— 例如,启动和停止计时器。
Note
始终使用满足你需求的最弱的增强形式。
例如,如果前置增强足以满足你的需求,那么就不要使用环绕增强。
环绕增强是通过对方法使用 @Around 注解来声明的。该方法应声明 Object 为其返回类型,而且方法的第一个参数必须是 ProceedingJoinPoint 类型。在增强方法的主体中,你必须在 ProceedingJoinPoint 上调用 proceed(),以便底层方法运行。不带参数地调用 proceed() 将导致在调用底层方法时提供调用者的原始参数。对于高级用例,proceed() 方法有一个接受参数数组(Object[])的重载变体。数组中的值将被用作调用底层方法时的参数。
Warning
当使用
Object[]调用proceed时,其行为与 AspectJ 编译器编译的环绕增强的proceed行为略有不同。对于使用传统 AspectJ 语言编写的环绕增强,传递给proceed的参数数量必须与传递给环绕增强的参数数量相匹配(而不是底层连接点接受的参数数量),并且在给定参数位置传递给proceed的值会取代连接点实体原始值(如果现在这还没有意义,不用担心)。Spring 采取的方法更简单,更符合其基于代理的、仅执行的语义。只有在你使用 AspectJ 编译器和织入器编译为 Spring 编写的
@AspectJ切面并使用带参数的proceed时,你才需要注意这个区别。有一种编写这样的切面的方法,它在 Spring AOP 和 AspectJ 之间是 100% 兼容的,这将在以下关于增强参数的部分中讨论。
环绕增强返回的值是方法的调用者看到的返回值。例如,一个简单的缓存切面可以从缓存中返回一个值(如果有的话),或者如果没有,就调用 proceed()(并返回该值)。请注意,proceed 可能在环绕增强的主体内被调用一次、多次,或者根本不被调用。所有这些都是合法的。
Tip:如果你声明你的环绕增强方法的返回类型为
void,那么将始终向调用者返回null,有效地忽略了任何proceed()调用的结果。因此,建议环绕增强方法声明一个Object的返回类型。增强方法通常应返回proceed()调用的返回值,即使底层方法的返回类型为void。然而,根据使用情况,增强可以选择返回缓存值、包装值或其他一些值。
以下示例展示了如何使用环绕增强:
Java
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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
4.4.6. 增强参数
Spring 提供了完全类型化的增强,这意味着你在增强签名中声明你需要的参数(正如我们在前面的返回和抛出示例中看到的),而不是一直使用 Object[] 数组。我们将在本节后面看到如何将参数和其他上下文值提供给增强主体。首先,我们来看看如何编写通用增强,这种增强可以找出当前正在提供增强的方法的信息。
4.4.6.1. 访问当前的 JoinPoint
任何增强方法都可以声明其第一个参数为 org.aspectj.lang.JoinPoint 类型的参数。注意,环绕增强需要声明第一个参数为 ProceedingJoinPoint 类型,这是 JoinPoint 的子类。
JoinPoint 接口提供了一些有用的方法:
getArgs():返回方法参数;getThis():返回代理对象;getTarget():返回目标对象;getSignature():返回正在被增强的方法的描述;toString():打印正在被增强的方法的有用描述;
有关更多详细信息,请参阅 JavaDoc。
4.4.6.2. 向增强传递参数
我们已经看到如何绑定返回值或异常值(使用返回后和抛出后增强)。为了使参数值在增强主体中可用,你可以使用 args 的绑定形式。如果你在 args 表达式中使用参数名代替类型名,那么当增强被调用时,相应参数的值将作为参数值传递。一个例子应该能让这个更清楚。假设你想对执行 DAO 操作的增强,这些操作将 Account 对象作为第一个参数,而你需要在增强主体中访问该账户。你可以这样写:
Java
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}1
2
3
4
2
3
4
args(account,..) 部分的切点表达式有两个目的。首先,它限制匹配只对那些方法执行,其中方法至少需要一个参数,并且传递给该参数的参数是 Account 的实例。其次,它通过 account 参数使实际的 Account 对象对增强可用。
另一种写法是声明一个切点,当它匹配到一个连接点时,“提供” Account 对象的值,然后从增强中引用命名的切点。这将如下所示:
Java
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}1
2
3
4
5
6
7
2
3
4
5
6
7
有关更多详细信息,请参阅 AspectJ 编程指南。
代理对象(this)、目标对象(target)和注解(@within、@target、@annotation 和 @args)都可以以类似的方式进行绑定。接下来的两个示例展示了如何匹配使用 @Auditable 注解的方法的执行,并提取审计代码(AuditCode):
这两个示例中的第一个示例展示了 @Auditable 注解的定义:
Java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}1
2
3
4
5
2
3
4
5
这两个示例中的第二个示例展示了匹配执行 @Auditable 方法的增强:
Java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}1
2
3
4
5
2
3
4
5
4.4.6.3. 增强参数和泛型
Spring AOP 可以处理在类声明和方法参数中使用的泛型。假设你有一个像下面这样的泛型类型:
Java
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}1
2
3
4
2
3
4
你可以通过将增强参数与你想要拦截的方法的参数类型绑定,以便仅对特定参数类型的方法类型进行拦截:
Java
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}1
2
3
4
2
3
4
这种方法不适用于泛型集合。所以你不能定义如下的切点:
Java
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}1
2
3
4
2
3
4
要使这个工作,我们必须检查集合的每一个元素,这是不合理的,因为我们也不能决定如何一般性地处理 null 值。要实现类似的效果,你必须将参数类型设为 Collection<?>,并手动检查元素的类型。
4.4.6.4. 确定参数名称
增强调用中的参数绑定依赖于将切点表达式中使用的名称与在增强和切点方法签名中声明的参数名称进行匹配。
Spring AOP 使用以下 ParameterNameDiscoverer 实现来确定参数名称。每个发现器都将有机会发现参数名称,第一个成功的发现器将胜出。如果所有注册的发现器都无法确定参数名称,将会抛出异常。
AspectJAnnotationParameterNameDiscoverer:使用用户通过相应的增强或切点注解中的
argNames属性显式指定的参数名称。详见显式参数名称。KotlinReflectionParameterNameDiscoverer:使用 Kotlin 反射 API 来确定参数名称。只有当这些 API 存在于类路径中时,才会使用此发现器。在 GraalVM 原生镜像中不受支持。
StandardReflectionParameterNameDiscoverer:使用标准的
java.lang.reflect.ParameterAPI 来确定参数名称。要求代码使用-parameters标志进行编译。推荐在 Java 8+ 上使用此方法。LocalVariableTableParameterNameDiscoverer:分析增强类的字节码中可用的局部变量表,从调试信息中确定参数名称。要求代码使用调试符号(至少是
-g:vars)进行编译。自 Spring Framework 6.0 起已被弃用,将在 Spring Framework 6.1 中移除,以支持使用-parameters编译代码。在 GraalVM 原生镜像中不受支持,除非相应的类文件作为资源存在于镜像中。AspectJAdviceParameterNameDiscoverer:从切点表达式、
returning和throwing子句中推导出参数名称。有关所使用的算法的详细信息,请参阅 JavaDoc。
4.4.6.5. 显式参数名称
@AspectJ 的增强和切点注解有一个可选的 argNames 属性,你可以使用它来指定被注解方法的参数名称。
Note
如果一个
@AspectJ切面已经被 AspectJ 编译器(ajc)编译,即使没有调试信息,你也不需要添加argNames属性,因为编译器会保留所需的信息。同样,如果一个
@AspectJ切面已经使用-parameters标志通过javac编译,你也不需要添加argNames属性,因为编译器会保留所需的信息。
以下示例展示了如何使用 argNames 属性:
Java
@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
}1
2
3
4
5
6
2
3
4
5
6
如果第一个参数的类型是 JoinPoint、ProceedingJoinPoint 或 JoinPoint.StaticPart,你可以从 argNames 属性的值中省略参数的名称。例如,如果你修改前面的增强以接收连接点对象,argNames 属性就不需要包含它:
Java
@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
}1
2
3
4
5
6
2
3
4
5
6
对 JoinPoint、ProceedingJoinPoint 或 JoinPoint.StaticPart 类型的第一个参数的特殊处理,对于不收集任何其他连接点上下文的增强方法特别方便。在这种情况下,你可以省略 argNames 属性。例如,以下增强不需要声明 argNames 属性:
Java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}1
2
3
4
2
3
4
4.4.6.6. 带参数进行处理
我们之前提到过,我们会描述如何编写一个带参数的 proceed 调用,使其在 Spring AOP 和 AspectJ 中始终一致。解决方案是确保增强签名按顺序绑定每个方法参数。以下示例展示了如何做到这一点:
Java
@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});
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在许多情况下,你都会进行这样的绑定(就像在前面的示例中一样)。
4.4.7. 增强排序
当多个增强都想在同一个连接点运行时会发生什么呢?Spring AOP 遵循与 AspectJ 相同的优先级规则来确定增强执行的顺序。优先级最高的增强在 “进入” 时首先运行(所以,给定两个前置增强,优先级最高的增强首先运行)。在从连接点 “退出” 时,优先级最高的增强最后运行(所以,给定两个后置增强,优先级最高的增强将第二个运行)。
当在不同切面中定义的两个增强都需要在同一个连接点运行时,除非你另行指定,否则执行顺序是未定义的。你可以通过指定优先级来控制执行顺序。这是通过在切面类中实现 org.springframework.core.Ordered 接口或用 @Order 注解来注解它,以常规的 Spring 方式完成的。给定两个切面,从 Ordered.getOrder()(或注解值)返回较低值的切面具有更高的优先级。
Note
特定切面的每种不同的增强类型在概念上都是直接应用于连接点的。因此,一个
@AfterThrowing增强方法不应该从伴随的@After/@AfterReturning方法中接收异常。在 Spring Framework 5.2.7 中,需要在同一连接点运行的在同一
@Aspect类中定义的增强方法根据其增强类型的优先级进行排序,从最高到最低优先级为:@Around、@Before、@After、@AfterReturning、@AfterThrowing。但是,请注意,一个@After增强方法将在同一切面中的任何@AfterReturning或@AfterThrowing增强方法之后有效地被调用,遵循 AspectJ 的@After的 “after finally advice” 语义。当在同一
@Aspect类中定义的两个相同类型的增强(例如,两个@After增强方法)都需要在同一连接点运行时,排序是未定义的(因为没有办法通过反射来检索javac编译的类的源代码声明顺序)。考虑将这样的增强方法合并到每个@Aspect类的每个连接点的一个增强方法中,或者将增强部分重构到可以通过Ordered或@Order在切面级别排序的单独的@Aspect类中。
4.5. 引入(Introduction)
引入(Introduction,在 AspectJ 中被称为 inter-type 声明)允许一个切面声明被增强的对象实现了给定的接口,并代表这些对象提供该接口的实现。
你可以通过使用 @DeclareParents 注解来进行引入。这个注解被用来声明匹配的类型有一个新的父类(因此得名)。例如,给定一个名为 UsageTracked 的接口和一个名为 DefaultUsageTracked 的该接口的实现,以下切面声明所有服务接口的实现者也实现了 UsageTracked 接口(例如,通过 JMX 进行统计):
Java
@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();
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
要实现的接口取决于被注解字段的类型。@DeclareParents 注解的 value 属性是一个 AspectJ 类型模式。任何类型匹配的 Bean 都会实现 UsageTracked 接口。注意,在前述示例的 before 增强中,服务 Bean 可以直接作为 UsageTracked 接口的实现。如果你需要以编程方式访问 Bean,你可以编写以下内容:
Java
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");4.6. 切面实例化模型
Note:这是一个高级主题。如果你刚开始接触 AOP,你可以安全地跳过它,等到以后再学习。
默认情况下,应用程序上下文中的每个切面都只有一个实例。AspectJ 将此称为单例实例化模型。也可以定义具有不同生命周期的切面。Spring 支持 AspectJ 的 perthis 和 pertarget 实例化模型,但目前不支持 percflow、percflowbelow 和 pertypewithin。
你可以在 @Aspect 注解中指定 perthis 子句来声明一个 perthis 切面。请参考以下示例:
Java
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
在上述示例中,perthis 子句的作用是,对于每一个执行业务服务的独特服务对象(每一个由切点表达式匹配的连接点绑定的独特对象),都会创建一个切面实例。当服务对象上的方法首次被调用时,切面实例就会被创建。当服务对象超出作用范围时,切面也会超出作用范围。在切面实例创建之前,其中的任何增强都不会执行。一旦切面实例被创建,只要服务对象是与此切面关联的那个,它内部声明的增强就会在匹配的连接点上执行。有关 per 子句的更多信息,请参阅 AspectJ 编程指南。
pertarget 实例化模型的工作方式与 perthis 完全一样,但它会为每一个在匹配的连接点的独特目标对象创建一个切面实例。
4.7. AOP 示例
现在你已经看到所有组成部分是如何工作的,我们可以将它们组合起来做一些有用的事情。
业务服务的执行有时会因为并发问题(例如,死锁失败者)而失败。如果重试操作,那么下一次尝试很可能会成功。对于在这种情况下适合重试的业务服务(幂等操作,不需要回到用户进行冲突解决),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明显横跨服务层中多个服务的需求,因此,通过切面来实现是理想的。
因为我们想要重试操作,所以我们需要使用环绕增强,这样我们就可以多次调用 proceed。以下代码片段显示了基本的切面实现:
Java
@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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
请注意,切面实现了 Ordered 接口,这样我们就可以将切面的优先级设置得比事务增强更高(我们希望每次重试时都有一个新的事务)。maxRetries 和 order 属性都由 Spring 配置。主要的操作发生在 doConcurrentOperation 的环绕增强中。请注意,目前,我们将重试逻辑应用到每个 businessService()。我们尝试继续执行,如果我们因 PessimisticLockingFailureException 失败,我们会再试一次,除非我们已经用尽了所有的重试次数。
以下是相应的 Spring 配置:
XML
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>1
2
3
4
5
6
2
3
4
5
6
为了精细化切面,使其只重试幂等操作,我们可能会定义以下的 Idempotent 注解:
Java
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}1
2
3
4
2
3
4
然后,我们可以使用该注解来注解服务操作的实现。只重试幂等操作的切面的更改涉及到精细化切点表达式,以便只有 @Idempotent 操作匹配,如下所示:
Java
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}1
2
3
4
5
2
3
4
5