Declaring Advice
通知与切点表达式关联,并在与切点匹配的方法执行之前、之后或前后运行。切点表达式可以是 inline pointcut,也可以是对 named pointcut 的引用。
Before 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() {
// ...
}
}
如果我们使用 named pointcut,我们可以按如下方式重写前一个示例:
-
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() {
// ...
}
}
您可以有多个advice声明(以及其他成员),全部在同一个aspect中。我们在这些示例中仅显示一个advice声明,以重点关注每个advice的效果。 |
有时,你需要在通知正文中访问实际的返回值。你可以使用 @AfterReturning
的形式将返回值与 getthataccess 绑定,如下面的示例所示:
-
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 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() {
// ...
}
}
通常,你希望仅在抛出给定类型的异常时才运行通知,并且还经常需要在通知正文中访问抛出的异常。你可以使用 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
当匹配的方法执行退出时,最终(finally)通知运行。它通过使用 @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 Advice
最后一种通知是 around 通知。Around 通知在匹配方法的执行周围运行。它有机会在方法运行之前和之后都进行工作,并确定方法何时、如何,甚至是否真正运行。如果您需要在多线程方式下在方法执行之前和之后共享状态(例如,启动和停止计时器),则经常使用 around 通知。
始终使用满足您要求中最不强大的通知形式。 例如,如果 before 通知足以满足您的需要,请勿使用 around 通知。 |
通过使用 @Around
注解为方法添加注释来声明周围建议。该方法应声明 Object
作为其返回类型,并且该方法的第一个参数的类型必须是 ProceedingJoinPoint
。在建议方法的主体中,你必须在 ProceedingJoinPoint
上调用 proceed()
才能让底层方法运行。在没有参数的情况下调用 proceed()
将导致在调用底层方法时将调用者的原始参数提供给底层方法。对于高级用例,proceed()
方法有一个过载变量,它接受一个参数数组 (Object[]
)。数组中的值将在调用底层方法时用作底层方法的参数。
在使用 |
环绕通知返回的值是由方法的调用者看到的返回值。例如,一个简单的缓存方面可以返回缓存中的值(如果有),如果没有,则调用 proceed()
(并返回该值)。请注意,可以在环绕通知的主体中一次、多次或根本不调用 proceed
。所有这些都是合法的。
如果您将 around advice 方法的返回类型声明为 void
,null
总是会被返回给调用者,进而忽略了对 proceed()
的任何调用结果。因此,建议 around advice 方法声明 Object
的返回类型。advice 方法通常应该返回从对 proceed()
的调用中返回的值,即使底层方法具有 void
返回类型。然而,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
}
}
Advice Parameters
Spring 提供全类型建议,这意味着您在建议签名中声明所需的用于建议签名的参数(如我们之前看到的返回和抛出示例)而不是始终处理 Object[]
数组。我们之后在本文中了解如何将参数和其他上下文值提供给建议主体。首先,我们了解如何编写通用建议,以查找建议当前建议的方法。
Access to the Current JoinPoint
任何通知方法都可以将其第一个参数声明为类型为 org.aspectj.lang.JoinPoint
的参数。请注意,环绕通知必须声明类型为 ProceedingJoinPoint
的第一个参数,后者是 JoinPoint
的子类。
JoinPoint
接口提供了许多有用的方法:
-
getArgs()
:返回方法参数。 -
getThis()
:返回代理对象。 -
getTarget()
:返回目标对象。 -
getSignature()
:返回经过建议的方法说明。 -
toString()
:打印经过建议的方法的有用说明。
请参阅 javadoc 了解更多详细信息。
Passing Parameters to Advice
我们已经了解了如何绑定返回的值或异常值(使用 afterreturning 和 after throwing 建议)。要使参数值可用于建议正文,您可以使用 args
的绑定形式。如果您在 args
表达式中使用参数名称来代替 atype 名称,那么在调用建议时将值与对应的参数一起传递。一个示例可以使这一点更清楚。假设您希望建议执行将 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)") (1) public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
1 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
|
2 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
Advice Parameters and Generics
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<?>
并手动检查元素的类型。
Determining Argument Names
建议调用中的参数绑定依赖于将切入点表达式中使用的名称与建议和切入点方法签名中声明的参数名称相匹配。
本部分可互换地使用术语 argument 和 parameter,因为 AspectJ API 将参数名称为变量名。 |
Spring AOP 使用以下 ParameterNameDiscoverer
实现来确定参数名称。每个发现者将有机会发现参数名称,第一个成功的发现者获胜。如果未注册的发现者能够确定参数名称,则会抛出异常。
AspectJAnnotationParameterNameDiscoverer
-
Uses parameter names that have been explicitly specified by the user via the
argNames
attribute in the corresponding advice or pointcut annotation. See Explicit Argument Names for details. KotlinReflectionParameterNameDiscoverer
-
Uses Kotlin reflection APIs to determine parameter names. This discoverer is only used if such APIs are present on the classpath.
StandardReflectionParameterNameDiscoverer
-
Uses the standard
java.lang.reflect.Parameter
API to determine parameter names. Requires that code be compiled with the-parameters
flag forjavac
. Recommended approach on Java 8+. AspectJAdviceParameterNameDiscoverer
-
Deduces parameter names from the pointcut expression,
returning
, andthrowing
clauses. See the javadoc for details on the algorithm used.
Explicit Argument Names
@AspectJ 建议和切入点注释具有可选的 argNames
属性,可以用来指定带注释的方法的参数名称。
如果 @AspectJ 方面是由 AspectJ 编译器 ( |
以下示例展示了如何使用 argNames
属性:
- Java
-
@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 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
2 | 将 `bean`和 `auditable`声明为参数名称。
|
3 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
4 | 将 `bean`和 `auditable`声明为参数名称。 |
如果第一个参数的类型是 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
,则可以从 argNames
属性的值中省略参数的名称。例如,如果您修改前面的建议以接收连接点对象,则 argNames
属性不必包含它:
- Java
-
@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 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
2 | 将 `bean`和 `auditable`声明为参数名称。
|
3 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
4 | 将 `bean`和 `auditable`声明为参数名称。 |
对于类型为 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
的第一个参数给予的特殊处理对于不收集任何其他连接点上下文的建议方法来说特别方便。在这种情况下,您可以省略 argNames
属性。例如,以下建议不必声明 argNames
属性:
- Java
-
@Before("com.xyz.Pointcuts.publicMethod()") (1) public void audit(JoinPoint jp) { // ... use jp }
1 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。
|
2 | 引用 Combining Pointcut Expressions 中定义的 publicMethod 命名切入点。 |
Proceeding with Arguments
我们前面说过将描述如何编写 proceed
调用,它的参数可以在 Spring AOP 和 AspectJ 中保持一致。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例展示了如何执行此操作:
- Java
-
@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 | 引用 Sharing Named Pointcut Definitions中定义的 `inDataAccessLayer`命名切入点。
|
2 | 引用 Sharing Named Pointcut Definitions中定义的 `inDataAccessLayer`命名切入点。 |
在许多情况下,你无论如何都会进行此绑定(如前面的示例所示)。
Advice Ordering
当多条建议都希望在同一连接点运行时会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则,以确定建议执行的顺序。优先级最高的建议在“进入”时首先运行(因此,给定两条 before 建议,优先级最高的建议首先运行)。从连接点“退出”时,优先级最高的建议最后执行(因此,给定两条 after 建议,优先级最高的建议将第二个运行)。
当定义在不同方面中的两条建议都需要在同一连接点运行时,除非你另有指定,否则执行顺序是不确定的。你可以通过指定优先级来控制执行顺序。这是通过正常 Spring 方式完成的,即在方面类中实现 org.springframework.core.Ordered
接口或对其进行 @Order
注释。给定两个方面,从 Ordered.getOrder()
(或注释值)返回较低值的方面具有较高的优先级。
某个方面的不同建议类型在概念上打算直接应用于连接点。因此, |