声明通知
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是_内联切入点_,也可以是 命名切入点 的引用。
前置通知
您可以使用 @Before
注解在切面中声明前置通知。
以下示例使用内联切入点表达式。
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
@Before("execution(* com.xyz.dao.*.*(..))")
fun doAccessCheck() {
// ...
}
}
如果我们使用 命名切入点,我们可以将前面的示例改写如下:
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
@Aspect
class BeforeExample {
@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
fun doAccessCheck() {
// ...
}
}
后置返回通知
后置返回通知在匹配的方法执行正常返回时运行。您可以使用 @AfterReturning
注解声明它。
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
@AfterReturning("execution(* com.xyz.dao.*.*(..))")
fun doAccessCheck() {
// ...
}
}
您可以在同一个切面中包含多个通知声明(以及其他成员)。这些示例中只显示了一个通知声明,以便突出每个通知的效果。 |
有时,您需要在通知体中访问实际返回的值。您可以使用 @AfterReturning
的绑定返回值形式来获取该访问,如以下示例所示:
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="execution(* com.xyz.dao.*.*(..))",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterReturning
@Aspect
class AfterReturningExample {
@AfterReturning(
pointcut = "execution(* com.xyz.dao.*.*(..))",
returning = "retVal")
fun doAccessCheck(retVal: Any?) {
// ...
}
}
returning
属性中使用的名称必须与通知方法中参数的名称相对应。当方法执行返回时,返回值将作为相应的参数值传递给通知方法。returning
子句还将匹配限制为仅返回指定类型值(在本例中为 Object
,它匹配任何返回值)的方法执行。
请注意,在使用后置返回通知时,无法返回一个完全不同的引用。
后置异常通知
后置异常通知在匹配的方法执行因抛出异常而退出时运行。您可以使用 @AfterThrowing
注解声明它,如以下示例所示:
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
public void doRecoveryActions() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
fun doRecoveryActions() {
// ...
}
}
通常,您希望通知仅在抛出给定类型的异常时运行,并且通常还需要在通知体中访问抛出的异常。您可以使用 throwing
属性来限制匹配(如果需要——否则使用 Throwable
作为异常类型)并将抛出的异常绑定到通知参数。以下示例显示了如何执行此操作:
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="execution(* com.xyz.dao.*.*(..))",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.AfterThrowing
@Aspect
class AfterThrowingExample {
@AfterThrowing(
pointcut = "execution(* com.xyz.dao.*.*(..))",
throwing = "ex")
fun doRecoveryActions(ex: DataAccessException) {
// ...
}
}
throwing
属性中使用的名称必须与通知方法中参数的名称相对应。当方法执行因抛出异常而退出时,异常将作为相应的参数值传递给通知方法。throwing
子句还将匹配限制为仅抛出指定类型异常(在本例中为 DataAccessException
)的方法执行。
请注意, |
后置最终通知
后置最终通知在匹配的方法执行退出时运行。它通过使用 @After
注解声明。后置通知必须准备好处理正常返回和异常返回条件。它通常用于释放资源和类似目的。以下示例显示了如何使用后置最终通知:
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("execution(* com.xyz.dao.*.*(..))")
public void doReleaseLock() {
// ...
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.After
@Aspect
class AfterFinallyExample {
@After("execution(* com.xyz.dao.*.*(..))")
fun doReleaseLock() {
// ...
}
}
请注意,AspectJ 中的 |
环绕通知
最后一种通知是_环绕_通知。环绕通知在匹配方法的执行“周围”运行。它有机会在方法运行之前和之后执行工作,并决定方法何时、如何以及是否实际运行。如果您需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器),通常会使用环绕通知。
始终使用满足您需求的最低功能形式的通知。例如,如果前置通知足以满足您的需求,请不要使用_环绕_通知。 |
环绕通知通过使用 @Around
注解方法来声明。该方法应声明 Object
作为其返回类型,并且方法的第一个参数必须是 ProceedingJoinPoint
类型。在通知方法的正文中,您必须在 ProceedingJoinPoint
上调用 proceed()
以使底层方法运行。不带参数调用 proceed()
将导致在调用底层方法时向其提供调用者的原始参数。对于高级用例,proceed()
方法有一个重载变体,它接受一个参数数组 (Object[]
)。数组中的值将用作调用底层方法时的参数。
当使用 |
环绕通知返回的值是方法调用者看到的值。例如,一个简单的缓存切面可以从缓存中返回值(如果有),或者如果缓存中没有,则调用 proceed()
(并返回该值)。请注意,proceed
可以在环绕通知的正文中调用一次、多次或根本不调用。所有这些都是合法的。
如果将环绕通知方法的返回类型声明为 void
,则始终会将 null
返回给调用者,从而有效地忽略任何 proceed()
调用的结果。因此,建议环绕通知方法声明 Object
的返回类型。通知方法通常应返回从 proceed()
调用返回的值,即使底层方法具有 void
返回类型。但是,通知可以根据用例选择返回缓存值、包装值或某些其他值。
以下示例显示了如何使用环绕通知:
-
Java
-
Kotlin
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("execution(* com.xyz..service.*.*(..))")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.ProceedingJoinPoint
@Aspect
class AroundExample {
@Around("execution(* com.xyz..service.*.*(..))")
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return retVal
}
}
通知参数
Spring 提供了完全类型化的通知,这意味着您在通知签名中声明您需要的参数(正如我们前面在返回和抛出示例中看到的那样),而不是一直使用 Object[]
数组。我们将在本节后面看到如何使参数和其他上下文值可用于通知体。首先,我们来看看如何编写通用通知,它可以了解当前正在通知的方法。
访问当前 JoinPoint
任何通知方法都可以将其第一个参数声明为 org.aspectj.lang.JoinPoint
类型。请注意,环绕通知需要声明第一个参数为 ProceedingJoinPoint
类型,它是 JoinPoint
的子类。
JoinPoint
接口提供了许多有用的方法:
-
getArgs()
: 返回方法参数。 -
getThis()
: 返回代理对象。 -
getTarget()
: 返回目标对象。 -
getSignature()
: 返回被通知方法的描述。 -
toString()
: 打印被通知方法的有用描述。
有关更多详细信息,请参阅 javadoc。
将参数传递给通知
我们已经看到了如何绑定返回值或异常值(使用后置返回和后置异常通知)。要使参数值可用于通知体,您可以使用 args
的绑定形式。如果您在 args
表达式中用参数名称代替类型名称,则当调用通知时,相应参数的值将作为参数值传递。一个示例应该能更清楚地说明这一点。假设您想通知接受 Account
对象作为第一个参数的 DAO 操作的执行,并且您需要在通知体中访问该帐户。您可以编写以下内容:
-
Java
-
Kotlin
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
// ...
}
@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
fun validateAccount(account: Account) {
// ...
}
切入点表达式的 args(account,..)
部分有两个目的。首先,它将匹配限制为仅那些方法接受至少一个参数,并且传递给该参数的参数是 Account
实例的方法执行。其次,它通过 account
参数使实际的 Account
对象可用于通知。
另一种编写方式是声明一个切入点,当它匹配连接点时“提供” Account
对象值,然后从通知中引用该命名切入点。这将如下所示:
-
Java
-
Kotlin
@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}
@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
// ...
}
有关更多详细信息,请参阅 AspectJ 编程指南。
代理对象 (this
)、目标对象 (target
) 和注解 (@within
、@target
、@annotation
和 @args
) 都可以以类似的方式绑定。下一组示例展示了如何匹配用 @Auditable
注解的方法的执行并提取审计代码:
以下显示了 @Auditable
注解的定义:
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)
以下显示了匹配 @Auditable
方法执行的通知:
- Java
-
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") [id="CO1-1"][id="CO1-1"][id="CO1-1"](1) public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。
- Kotlin
-
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") [id="CO2-1"][id="CO1-2"][id="CO2-1"](1) fun audit(auditable: Auditable) { val code = auditable.value() // ... }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。
通知参数和泛型
Spring AOP 可以处理类声明和方法参数中使用的泛型。假设您有一个如下所示的泛型类型:
-
Java
-
Kotlin
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
interface Sample<T> {
fun sampleGenericMethod(param: T)
fun sampleGenericCollectionMethod(param: Collection<T>)
}
您可以通过将通知参数绑定到要拦截方法的参数类型来限制对某些参数类型的方法类型的拦截:
-
Java
-
Kotlin
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
// Advice implementation
}
这种方法不适用于泛型集合。因此,您不能按如下方式定义切入点:
-
Java
-
Kotlin
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
// Advice implementation
}
为了使其工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理一般的 null
值。为了实现类似的功能,您必须将参数类型设置为 Collection<?>
并手动检查元素的类型。
确定参数名称
通知调用中的参数绑定依赖于将切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称进行匹配。
本节互换使用_参数_和_形参_这两个术语,因为 AspectJ API 将参数名称称为形参名称。 |
Spring AOP 使用以下 ParameterNameDiscoverer
实现来确定参数名称。每个发现器都将有机会发现参数名称,并且第一个成功的发现器获胜。如果注册的发现器都无法确定参数名称,则会抛出异常。
AspectJAnnotationParameterNameDiscoverer
-
使用用户通过相应通知或切入点注解中的
argNames
属性显式指定的参数名称。有关详细信息,请参阅 显式参数名称。 KotlinReflectionParameterNameDiscoverer
-
使用 Kotlin 反射 API 来确定参数名称。此发现器仅在类路径中存在此类 API 时使用。
StandardReflectionParameterNameDiscoverer
-
使用标准的
java.lang.reflect.Parameter
API 来确定参数名称。要求代码使用-parameters
标志进行javac
编译。在 Java 8+ 上推荐使用此方法。 AspectJAdviceParameterNameDiscoverer
-
从切入点表达式、
returning
和throwing
子句推断参数名称。有关所用算法的详细信息,请参阅 javadoc。
显式参数名称
@AspectJ 通知和切入点注解有一个可选的 argNames
属性,您可以使用它来指定注解方法的参数名称。
如果 @AspectJ 切面已由 AspectJ 编译器 ( |
以下示例显示了如何使用 argNames
属性:
- Java
-
@Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", [id="CO3-1"][id="CO1-3"][id="CO3-1"](1) argNames = "bean,auditable") [id="CO3-2"][id="CO1-4"][id="CO3-2"](2) public void audit(Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code and bean }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。 <1> 声明 `bean` 和 `auditable` 作为参数名称。
- Kotlin
-
@Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", [id="CO4-1"][id="CO1-5"][id="CO4-1"](1) argNames = "bean,auditable") [id="CO4-2"][id="CO1-6"][id="CO4-2"](2) fun audit(bean: Any, auditable: Auditable) { val code = auditable.value() // ... use code and bean }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。 <1> 声明 `bean` 和 `auditable` 作为参数名称。
如果第一个参数是 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
类型,则可以从 argNames
属性的值中省略该参数的名称。例如,如果您修改前面的通知以接收连接点对象,则 argNames
属性不需要包含它:
- Java
-
@Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", [id="CO5-1"][id="CO1-7"][id="CO5-1"](1) argNames = "bean,auditable") [id="CO5-2"][id="CO1-8"][id="CO5-2"](2) public void audit(JoinPoint jp, Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code, bean, and jp }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。 <1> 声明 `bean` 和 `auditable` 作为参数名称。
- Kotlin
-
@Before( value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", [id="CO6-1"][id="CO1-9"][id="CO6-1"](1) argNames = "bean,auditable") [id="CO6-2"][id="CO1-10"][id="CO6-2"](2) fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) { val code = auditable.value() // ... use code, bean, and jp }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。 <1> 声明 `bean` 和 `auditable` 作为参数名称。
对 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
类型的第一个参数的特殊处理对于不收集任何其他连接点上下文的通知方法特别方便。在这种情况下,您可以省略 argNames
属性。例如,以下通知不需要声明 argNames
属性:
- Java
-
@Before("com.xyz.Pointcuts.publicMethod()") [id="CO7-1"][id="CO1-11"][id="CO7-1"](1) public void audit(JoinPoint jp) { // ... use jp }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。
- Kotlin
-
@Before("com.xyz.Pointcuts.publicMethod()") [id="CO8-1"][id="CO1-12"][id="CO8-1"](1) fun audit(jp: JoinPoint) { // ... use jp }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。
使用参数继续
我们前面提到过,我们将描述如何编写一个 proceed
调用,其参数在 Spring AOP 和 AspectJ 中都能一致地工作。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:
- Java
-
@Around("execution(List<Account> find*(..)) && " + "com.xyz.CommonPointcuts.inDataAccessLayer() && " + "args(accountHolderNamePattern)") [id="CO9-1"][id="CO1-13"][id="CO9-1"](1) public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern) throws Throwable { String newPattern = preProcess(accountHolderNamePattern); return pjp.proceed(new Object[] {newPattern}); }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-common-pointcuts[共享命名切入点定义] 中定义的 `inDataAccessLayer` 命名切入点。
- Kotlin
-
@Around("execution(List<Account> find*(..)) && " + "com.xyz.CommonPointcuts.inDataAccessLayer() && " + "args(accountHolderNamePattern)") [id="CO10-1"][id="CO1-14"][id="CO10-1"](1) fun preProcessQueryPattern(pjp: ProceedingJoinPoint, accountHolderNamePattern: String): Any? { val newPattern = preProcess(accountHolderNamePattern) return pjp.proceed(arrayOf<Any>(newPattern)) }
<1> 引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-common-pointcuts[共享命名切入点定义] 中定义的 `inDataAccessLayer` 命名切入点。
在许多情况下,您无论如何都会执行此绑定(如前面的示例所示)。
通知排序
当多个通知都想在同一个连接点运行会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知的执行顺序。优先级最高的通知“进入时”首先运行(因此,给定两个前置通知,优先级最高的先运行)。“离开”连接点时,优先级最高的通知最后运行(因此,给定两个后置通知,优先级最高的将第二个运行)。
当在不同切面中定义的两个通知都需要在同一个连接点运行,除非您另有指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这通过 Spring 的常规方式完成,即在切面类中实现 org.springframework.core.Ordered
接口或使用 @Order
注解对其进行注解。给定两个切面,从 Ordered.getOrder()
(或注解值)返回较低值的切面具有较高的优先级。
特定切面的每种不同通知类型在概念上都旨在直接应用于连接点。因此, |