Advice 与一个切点表达式相关联,在切点匹配的方法执行之前、之后或周围运行。该切点表达式可以是对一个命名的切点的简单引用,也可以是一个就地声明的切点表达式。
Before Advice / 方法执行前
你可以通过使用 @Before
注解在一个切面方法执行前的 advice:
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() {
// ...
}
}
After Returning Advice / 方法执行正常返回后
当一个匹配的方法执行正常返回时,返回后 advice 运行。你可以通过使用 @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() {
// ...
}
}
:::info 你可以有多个 advice 声明(也可以有其他成员),都在同一个 切面。我们在这些例子中只展示了一个 advice 声明,以集中展示每个 advice 的效果。 :::
有时,你需要在 advcie 正文中访问被返回的实际值。你可以使用绑定返回值的 @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) {
// ...
}
}
返回属性中使用的名称必须与 advice 方法中的参数名称相对应。当一个方法执行返回时,返回值会作为相应的参数值传递给 advice 方法。returning 子句也限制了匹配,只匹配那些返回指定类型的值的方法执行(在这种情况下是 Object,它匹配任何返回值)。
请注意,在返回 advice 后使用时,不可能返回一个完全不同的参考。
After Throwing Advice / 抛出异常后
当匹配的方法执行通过抛出异常退出时。您可以使用 @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() {
// ...
}
}
通常情况下,你希望建议只在给定类型的异常被抛出时运行,而且你也经常需要在 advice 主体中访问被抛出的异常。你可以使用 throwing 属性来限制匹配(如果需要的话 — 否则使用 Throwable 作为异常类型),并将抛出的异常绑定到 advice 参数上。下面的例子展示了如何做到这一点:
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) { // 限制只匹配 DataAccessException 的异常
// ...
}
}
在 throwing 属性中使用的名称必须与 advice 方法中的参数名称相对应。当一个方法的执行通过抛出一个异常退出时,该异常将作为相应的参数值传递给 advice 方法。throwing 子句也限制了匹配,只能匹配那些抛出指定类型的异常的方法执行(本例中是 DataAccessException)。
:::info 请注意,@AfterThrowing 并不表示一个一般的异常处理回调。具体来说,@AfterThrowing advice 法只应该接收来自连接点(用户声明的目标方法)本身的异常,而不是来自附带的 @After/@AfterReturning 方法。 :::
After (Finally) Advice / 方法执行后(Finally)
当一个匹配的方法执行退出时,After(Finally)advice 会运行。它是通过使用 @After
注解来声明的。After advice 必须准备好处理正常和异常的返回条件。它通常被用于释放资源和类似的目的。下面的例子展示了如何使用 after finally advice。
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
:::info 请注意,AspectJ 中的 @After advice 被定义为 “after finally advice”,类似于 try-catch 语句中的 finally 块。它将对任何结果、正常返回或从连接点(用户声明的目标方法)抛出的异常进行调用,这与 @AfterReturning 不同,后者只适用于成功的正常返回。 :::
Around Advice / 环绕通知
最后一种建议是 环绕 Advice。 「环绕」一个匹配的方法的执行而运行。它有机会在方法运行之前和之后进行工作,并决定何时、如何、甚至是否真正运行该方法。如果你需要以线程安全的方式分享方法执行前后的状态(例如,启动和停止一个计时器),通常会使用 「环绕通知」。始终使用最不强大的 advice 形式来满足你的要求(也就是说,如果 before advice 也可以,就不要使用 around advice)。
环绕通知是通过使用 @Around
注解来声明的。advice 方法的第一个参数必须是 ProceedingJoinPoint
类型。在 advice 的正文中,对ProceedingJoinPoint
调用 proceed()
会导致底层方法的运行。proceed 方法也可以传入一个 Object[]
。数组中的值将作为方法执行时的参数。
:::info
当用 Object[]
调用时, proceed 的行为与 AspectJ 编译器编译的环绕通知的行为有些不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给 Proceed 的参数数必须与传递给环绕通知的参数数相匹配(而不是底层连接点的参数数),并且在给定的参数位置传递给Proceed 的值会取代该值所绑定的实体在连接点上的原始值(如果这现在没有意义,请不要担心)。Spring 采取的方法更简单,也更符合其基于代理的、只执行的语义。只有当你编译为 Spring 编写的 @AspectJ 切面并使用 AspectJ 编译器和 weaver 的参数进行时,你才需要注意这个区别。有一种方法可以在 Spring AOP 和 AspectJ 之间100%兼容地编写这些切面,这将在下面关于 advice 参数的章节中讨论。
:::
下面的例子展示了如何使用 around 通知:
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;
}
}
环绕通知返回的值是方法的调用者看到的返回值。例如,一个简单的缓存切面可以从缓存中返回一个值,如果它有缓存,则调用 proceed()
。请注意,Proceed 可以被调用一次,多次,或者根本就不在环绕通知的主体中调用。所有这些都是合法的。
Advice Parameters / advice 的参数
Spring 提供了完全类型化的 advice,这意味着你可以在 advice 签名中声明你需要的参数(就像我们在前面看到的返回和抛出的例子一样),而不是一直用 Object[]
数组工作。我们将在本节后面看到如何使参数和其他上下文值对 advice 主体可用。首先,我们看一下如何编写通用的 advice,它可以找出 advice 当前所 advice 的方法。
访问当前的 JoinPoint
任何 advice 方法都可以声明一个 org.aspectj.lang.JoinPoint
类型的参数作为它的第一个参数(注意,Arount Advice 需要声明一个 ProceedingJoinPoint 类型的第一个参数,它是 JoinPoint 的一个子类。JoinPoint 接口提供了许多有用的方法:
getArgs()
:获取方法参数getThis()
:获取代理对象getTarget()
:获取目标对象getSignature()
: 获取方法的签名toString()
: 获取对 advice 方法有用的描述
有关更多详细信息,请参阅 javadoc。
将参数传递给 advice
我们已经看到了如何绑定返回值或异常值(使用返回后和抛出后通知)。为了使参数值对 advice 主体可用,你可以使用 args 的绑定形式。如果你在 args 表达式中使用参数名来代替类型名,那么当 advice 被调用时,相应参数的值将作为参数值被传递。一个例子可以让我们更清楚地了解这一点。假设你想 advice 执行将一个 account 对象作为第一个参数的 DAO 操作,并且你需要在 advice 正文中访问该 account。你可以写如下内容:
// 这里使用了 逻辑操作符来限定参数,第一个参数必须是 account 名称,与下面声明的 Account 对应
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
pointcut 表达式的 args(account,..)
部分有两个目的:
- 首先,它将匹配限制在方法的执行上,即方法至少需要一个参数,并且传递给该参数的参数是一个 Account 的实例。
- 其次,它使实际的 Account 对象通过 account 参数对 advice 可用
另一种写法是声明一个 pointcut,当它与一个连接点匹配时 「提供 」Account 对象的值,然后从 advice 中引用命名的 pointcut。这看起来就像这样:
@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
注解的方法的执行并提取 AuditCode。
两个例子中的第一个显示了 @Auditable
注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
两个例子中的第二个显示了与执行 @Auditable
方法相匹配的 advice :
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
Advice 的参数和泛型
Spring AOP 可以处理类声明和方法参数中使用的泛型。假设你有一个像下面这样的泛型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
你可以将方法类型的拦截限制在某些参数类型上,方法是将 advice 参数打到你想拦截的参数类型上:
@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
}
为了实现这一点,我们必须检查集合中的每一个元素,这是不太合理的,因为我们也无法决定如何处理一般的空值。为了实现与此类似的东西,你必须将参数输入到 Collection<?>
中,并手动检查元素的类型。
确定参数名称
advice 调用中的参数绑定依赖于将 pointcut 表达式中使用的名称与 advice 和 pointcut 方法签名中声明的参数名称相匹配。参数名称无法通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:
如果用户已经明确指定了参数名,那么将使用指定的参数名。advice 和 pointcut 注解都有一个可选的 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 属性的值中省略参数的名称。例如,如果你修改前面的 advice 以接收连接点对象,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 类型的第一个参数的特殊处理,对于不收集任何其他连接点上下文的 advice 实例特别方便。在这种情况下,你可以省略 argNames 属性。例如,下面的 advice 不需要声明 argNames 属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
使用 argNames
属性有点笨拙,所以如果没有指定 argNames
属性,Spring AOP 会查看类的调试信息并尝试从局部变量表中确定参数名称。只要类在编译时带有调试信息(至少是 -g:vars
),这个信息就存在。使用这个标志进行编译的后果是。
- 你的代码稍微容易理解(逆向工程)
- 类文件的大小稍微大一点(通常不重要)
- 你的编译器不应用删除未使用的局部变量的优化。换句话说,使用这个标志进行构建,你应该不会遇到困难。
:::info 如果一个 @AspectJ 切面已经被 AspectJ 编译器(ajc)编译,即使没有调试信息,你也不需要添加 argNames 属性,因为编译器会保留需要的信息。 :::
如果代码在编译时没有必要的调试信息,Spring AOP 会尝试推断出绑定变量与参数的配对关系(例如,如果在 pointcut 表达式中只绑定了一个变量,而 advice 方法只接受一个参数,那么这种配对关系就很明显)。如果考虑到可用的信息,变量的绑定是不明确的,就会抛出一个 AmbiguousBindingException。
如果上述所有的策略都失败了,就会抛出一个 IllegalArgumentException。
参数的讨论
我们在前面说过,我们将描述如何编写一个带参数的程序调用,并在 Spring AOP 和 AspectJ 中一致运行。解决方案是确保建议签名按顺序绑定每个方法参数。下面的例子展示了如何做到这一点:
@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});
}
在许多情况下,你还是会做这种绑定(如前面的例子)。
Advice 顺序
当多个 advice 都想在同一个连接点上运行时会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则来决定 advice 的执行顺序。优先级最高的 advice 在 「进入」时首先运行(因此,给定两片 advice 之前,优先级最高的那片先运行)。从一个连接点 「出来」时,优先级最高的 advice 最后运行(因此,给定两片后 advice,优先级最高的 advice 将第二运行)。
当两个在不同切面定义的 advice 都需要在同一个连接点运行时,除非你另外指定,否则执行的顺序是不确定的。你可以通过指定优先级来控制执行的顺序。这可以通过在切面类中实现 org.springframework.core.Ordered
接口或用 @Order
注解来完成,这是正常的 Spring 方式。给定两个切面,从 Ordered.getOrder()
返回较低值的切面(或注解值)具有较高的优先权。
:::info 一个特定切面的每个不同的 advice 类型在概念上都是为了直接适用于连接点。因此,@AfterThrowing advoce 方法不应该从附带的@After/@AfterReturning 方法中接收异常。
从 Spring Framework 5.2.7 开始,在同一 @Aspect 类中定义的、需要在同一连接点运行的 advice 方法会根据其 advice 类型被分配优先权,优先权从高到低的顺序如下。@Around, @Before, @After, @AfterReturning, @AfterThrowing。但是请注意,@After advice 方法将有效地在同一切面的任何 @AfterReturning 或 @AfterThrowing advice 方法之后被调用,这符合 AspectJ 对 @After 的 「最后建议」语义。
当在同一个 @Aspect 类中定义的两块相同类型的 advice(例如,两个 @After advice 方法)都需要在同一个连接点运行时,排序是未定义的(因为对于 javac 编译的类,没有办法通过反射检索源代码的声明顺序)。考虑将这类 advice 方法折叠成每个 @Aspect 类中每个连接点的一个 advice 方法,或者将 advice 片断重构为单独的 @Aspect 类,你可以通过 Ordered 或 @Order 在切面级排序。 :::