|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
声明通知
通知(Advice)与一个切入点表达式相关联,并在该切入点匹配的方法执行之前、之后或周围运行。该切入点表达式可以是内联切入点,也可以是对命名切入点的引用。
前置通知
你可以通过使用 @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() {
// ...
}
}
返回后通知
返回后通知(After returning advice)在匹配的方法正常返回时执行。
你可以通过使用 @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,它可以匹配任何返回值)。
请注意,使用返回后通知(after returning advice)时,无法返回一个完全不同的引用。
抛出异常后通知
异常抛出后通知(After throwing advice)在匹配的方法执行因抛出异常而退出时运行。你可以使用 @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() {
// ...
}
}
通常,你希望通知(advice)仅在抛出特定类型异常时才执行,并且通常还需要在通知体中访问所抛出的异常。你可以使用 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 (finally) advice)在匹配的方法执行退出时运行。它通过使用 @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 中的 |
环绕通知
最后一种通知是环绕通知。环绕通知在匹配方法的执行“周围”运行,它有机会在方法执行前后都进行操作,并且可以决定何时、如何,甚至是否真正执行该方法。 环绕通知通常用于需要以线程安全的方式在方法执行前后共享状态的场景——例如,启动和停止计时器。
|
始终使用满足您需求的最弱形式的通知(advice)。 例如,如果前置通知已能满足您的需求,请不要使用环绕通知。 |
环绕通知(Around advice)通过使用 @Around 注解标注一个方法来声明。
该方法应声明返回类型为 Object,并且其第一个参数必须是 ProceedingJoinPoint 类型。
在通知方法的主体中,你必须调用 proceed() 上的 ProceedingJoinPoint 方法,
以使底层目标方法得以执行。调用无参的 proceed() 方法时,
会将调用方的原始参数传递给底层方法。对于高级用例,
proceed() 方法还提供了一个重载版本,该版本接受一个参数数组(Object[])。
当底层方法被调用时,数组中的值将作为其参数使用。
|
当使用 Spring 所采用的方法更为简单,并且更符合其基于代理、仅在执行时生效的语义。只有当你编译为 Spring 编写的 |
环绕通知(around advice)返回的值就是方法调用者所看到的返回值。例如,一个简单的缓存切面可以在缓存中存在值时直接返回该值;如果缓存中没有,则调用 proceed() 方法(并返回其返回值)。请注意,在环绕通知的主体中,proceed 可以被调用一次、多次,或者完全不调用,所有这些情况都是合法的。
如果你将环绕通知(around advice)方法的返回类型声明为 void,那么调用者将始终收到 null,这实际上会忽略对 proceed() 的任何调用结果。因此,建议环绕通知方法将返回类型声明为 Object。即使被代理的底层方法返回类型为 proceed(),该通知方法通常也应返回从 void 调用中获得的值。不过,根据具体使用场景,通知方法也可以选择性地返回一个缓存值、包装后的值或其他值。 |
以下示例展示了如何使用环绕通知(around advice):
-
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 提供完全类型化的通知(advice),这意味着你可以在通知的签名中声明所需的参数(正如我们之前在 returning 和 throwing 示例中所看到的那样),而无需始终使用 Object[] 数组。本节稍后我们将介绍如何将参数及其他上下文值传递给通知体。首先,我们来看看如何编写通用的通知,使其能够获知当前正在被通知的方法。
访问当前JoinPoint
任何通知方法都可以将其第一个参数声明为 org.aspectj.lang.JoinPoint 类型。请注意,环绕通知(around advice)必须将其第一个参数声明为 ProceedingJoinPoint 类型,该类型是 JoinPoint 的子类。
JoinPoint 接口提供了许多有用的方法:
-
getArgs():返回方法参数。 -
getThis():返回代理对象。 -
getTarget():返回目标对象。 -
getSignature():返回被通知方法的描述。 -
toString():打印被通知方法的有用描述。
有关更多详细信息,请参阅javadoc。
向通知传递参数
我们已经了解了如何绑定返回值或异常值(使用 after-returning 和 after-throwing 通知)。为了在通知体中使用参数值,你可以使用 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 对象在通知(advice)中可用。
另一种写法是声明一个切入点(pointcut),当它匹配某个连接点(join point)时“提供”Account对象的值,然后在通知(advice)中引用这个命名的切入点。其写法如下所示:
-
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 方法相匹配的通知(advice):
-
Java
-
Kotlin
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
fun audit(auditable: Auditable) {
val code = auditable.value()
// ...
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
通知参数与泛型
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-
使用用户通过相应通知(advice)或切入点(pointcut)注解中的
argNames属性显式指定的参数名称。详情请参见显式参数名称。 KotlinReflectionParameterNameDiscoverer-
使用 Kotlin 反射 API 来确定参数名称。仅当类路径中存在这些 API 时,才会使用此发现器。
StandardReflectionParameterNameDiscoverer-
使用标准的
java.lang.reflect.ParameterAPI 来确定参数名称。要求代码在编译时使用-parameters的javac标志。这是 Java 8 及以上版本的推荐做法。 LocalVariableTableParameterNameDiscoverer-
分析通知(advice)类字节码中可用的局部变量表,从调试信息中确定参数名称。 要求代码必须使用调试符号进行编译(至少需包含
-g:vars选项)。自 Spring Framework 6.0 起已弃用,并计划在 Spring Framework 6.1 中移除,建议改用-parameters选项编译代码。在 GraalVM 原生镜像中不受支持。 AspectJAdviceParameterNameDiscoverer-
从切入点表达式、
returning和throwing子句中推断参数名称。有关所用算法的详细信息,请参阅javadoc。
显式参数名称
@AspectJ 通知(advice)和切入点(pointcut)注解具有一个可选的 argNames 属性,可用于指定被注解方法的参数名称。
|
如果一个 @AspectJ 切面已经由 AspectJ 编译器( 同样地,如果一个 @AspectJ 切面在使用 |
以下示例展示了如何使用 argNames 属性:
-
Java
-
Kotlin
@Before(
value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
argNames = "bean,auditable") (2)
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
| 2 | 将 bean 和 auditable 声明为参数名称。 |
@Before(
value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
argNames = "bean,auditable") (2)
fun audit(bean: Any, auditable: Auditable) {
val code = auditable.value()
// ... use code and bean
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
| 2 | 将 bean 和 auditable 声明为参数名称。 |
如果第一个参数的类型是 JoinPoint、ProceedingJoinPoint 或
JoinPoint.StaticPart,你可以从 argNames 属性的值中省略该参数的名称。例如,如果你修改前面的通知以接收连接点对象,则 argNames 属性无需包含它:
-
Java
-
Kotlin
@Before(
value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
argNames = "bean,auditable") (2)
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
| 2 | 将 bean 和 auditable 声明为参数名称。 |
@Before(
value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)", (1)
argNames = "bean,auditable") (2)
fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) {
val code = auditable.value()
// ... use code, bean, and jp
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
| 2 | 将 bean 和 auditable 声明为参数名称。 |
对于类型为 JoinPoint、ProceedingJoinPoint 或 JoinPoint.StaticPart 的第一个参数所给予的特殊处理,对于那些不需要收集任何其他连接点上下文的通知方法来说尤为方便。在这种情况下,您可以省略 argNames 属性。例如,以下通知无需声明 argNames 属性:
-
Java
-
Kotlin
@Before("com.xyz.Pointcuts.publicMethod()") (1)
public void audit(JoinPoint jp) {
// ... use jp
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
@Before("com.xyz.Pointcuts.publicMethod()") (1)
fun audit(jp: JoinPoint) {
// ... use jp
}
| 1 | 引用在组合切入点表达式中定义的名为pointcuts.html#aop-pointcuts-combining的切入点。 |
使用参数进行过程处理
我们之前提到过,将介绍如何编写一个带有参数的 proceed 调用,使其在 Spring AOP 和 AspectJ 中都能一致地工作。解决方法是确保通知(advice)的签名按顺序绑定方法的每个参数。以下示例展示了如何实现这一点:
-
Java
-
Kotlin
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)") (1)
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
| 1 | 引用在共享命名切入点定义中定义的名为pointcuts.html#aop-common-pointcuts的切入点。 |
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)") (1)
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
accountHolderNamePattern: String): Any? {
val newPattern = preProcess(accountHolderNamePattern)
return pjp.proceed(arrayOf<Any>(newPattern))
}
| 1 | 引用在共享命名切入点定义中定义的名为pointcuts.html#aop-common-pointcuts的切入点。 |
在许多情况下,无论如何你都会进行这种绑定(如前面的示例所示)。
通知排序
当多个通知(advice)都希望在同一个连接点(join point)执行时会发生什么? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知的执行顺序。优先级最高的通知在“进入”时最先执行(因此,如果有两个前置通知(before advice),优先级最高的那个会先运行)。在从连接点“退出”时,优先级最高的通知最后执行(因此,如果有两个后置通知(after advice),优先级最高的那个将第二个运行)。
当定义在不同切面中的两条通知(advice)都需要在同一连接点(join point)执行时,除非另行指定,否则它们的执行顺序是未定义的。你可以通过指定优先级(precedence)来控制执行顺序。在 Spring 中,这可以通过常规方式实现:要么在切面类中实现 org.springframework.core.Ordered 接口,要么使用 @Order 注解对其进行标注。对于两个切面而言,从 Ordered.getOrder() 方法(或注解值)返回较小值的切面具有更高的优先级。
|
某个特定切面中的每种不同类型的通知(advice)在概念上都是直接应用于连接点(join point)的。因此, 自 Spring Framework 5.2.7 起,定义在同一个 当同一个 |