声明建议
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() {
// ...
}
}
返回后通知
正常返回时,返回通知会在匹配的方法执行后运行。你可以使用 @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
)的方法执行。
请注意,@AfterThrowing
并不表示一个通用的异常处理回调。具体来说,@AfterThrowing
通知方法只应接收来自连接点(用户声明的目标方法)本身的异常,而不是来自附带的 @After
/@AfterReturning
方法的异常。
后置(最终)通知
After (finally) 通知在匹配的方法执行退出时运行。它通过使用 @After
注解声明。After 通知必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。以下示例展示了如何使用 after finally 通知:
- 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 中的 @After
通知被定义为“最终通知”,类似于 try-catch 语句中的 finally 块。无论结果如何,它都会在连接点(用户声明的目标方法)正常返回或抛出异常时被调用,这与仅适用于成功正常返回的 @AfterReturning
不同。
环绕通知
最后一种建议是 环绕 建议。环绕建议在匹配方法的执行“周围”运行。它有机会在方法运行之前和之后进行工作,并决定方法何时、如何,甚至是否真正运行。如果需要以线程安全的方式在方法执行前后共享状态,通常会使用环绕建议——例如,启动和停止计时器。
始终使用满足您需求的最低权限形式的通知。
例如,如果 before 通知足以满足您的需求,请不要使用 around 通知。
环绕通知通过在方法上使用 @Around
注解来声明。该方法应声明 Object
作为其返回类型,并且方法的第一个参数必须是 ProceedingJoinPoint
类型。在通知方法的主体中,必须在 ProceedingJoinPoint
上调用 proceed()
,以便底层方法运行。调用不带参数的 proceed()
将导致在调用底层方法时使用调用者的原始参数。对于高级用例,proceed()
方法有一个重载变体,它接受一个参数数组(Object[]
)。数组中的值将在调用底层方法时用作参数。
当使用 Object[]
调用 proceed
时,其行为与使用 AspectJ 编译器编译的环绕通知的 proceed
行为略有不同。对于使用传统 AspectJ 语言编写的环绕通知,传递给 proceed
的参数数量必须与传递给环绕通知的参数数量匹配(而不是底层连接点所需的参数数量),并且在给定参数位置传递给 proceed
的值将取代连接点上实体的原始值(如果现在不理解也不用担心)。
Spring 采用的方法更简单,并且更符合其基于代理的、仅执行语义。只有在为 Spring 编写的 @AspectJ
切面并使用 AspectJ 编译器和织入器与参数一起使用 proceed
时,才需要注意这一差异。有一种方法可以编写在 Spring AOP 和 AspectJ 中 100% 兼容的切面,这将在关于通知参数的下一节中讨论。
around 通知返回的值是方法调用者看到的返回值。例如,一个简单的缓存切面可以从缓存中返回一个值(如果有的话),或者调用 proceed()
(并返回该值)如果没有的话。注意,proceed
可以在 around 通知的主体中被调用一次、多次或根本不调用。这些都是合法的。
如果你将环绕通知方法的返回类型声明为 void
,那么将始终返回 null
给调用者,从而有效地忽略任何 proceed()
调用的结果。因此,建议环绕通知方法声明返回类型为 Object
。即使底层方法的返回类型为 void
,通知方法通常也应返回 proceed()
调用返回的值。不过,根据具体使用场景,通知方法也可以选择返回缓存值、包装值或其他值。
以下示例展示了如何使用环绕通知:
- 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) {
// ...
}
pointcut 表达式中的 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
- Kotlin
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") 1
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
引用在组合切入点表达式中定义的名为
publicMethod
的切入点。
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") 1
fun audit(auditable: Auditable) {
val code = auditable.value()
// ...
}
引用在组合切入点表达式中定义的名为
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 来确定参数名称。要求代码使用 javac
的 -parameters
标志进行编译。推荐在 Java 8+ 上使用这种方法。
AspectJAdviceParameterNameDiscoverer
从切入点表达式、returning
和 throwing
子句中推导参数名称。有关所使用算法的详细信息,请参阅 javadoc。
显式参数名称
@AspectJ 通知和切入点注解有一个可选的 argNames
属性,你可以用它来指定被注解方法的参数名称。
如果一个 @AspectJ 切面即使没有调试信息也已经通过 AspectJ 编译器 (ajc
) 编译过,则不需要添加 argNames
属性,因为编译器保留了所需的信息。
同样地,如果一个 @AspectJ 切面是通过 javac
使用 -parameters
标志编译的,则不需要添加 argNames
属性,因为编译器保留了所需的信息。
下面的示例展示了如何使用 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
}
引用在组合切入点表达式中定义的名为
publicMethod
的切入点。声明
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
}
引用在组合切入点表达式中定义的名为
publicMethod
的切入点。声明
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
}
引用在组合切入点表达式中定义的名为
publicMethod
的切入点。声明
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
}
引用在组合切入点表达式中定义的名为
publicMethod
的切入点。声明
bean
和auditable
为参数名称。
对类型为 JoinPoint
、ProceedingJoinPoint
或 JoinPoint.StaticPart
的第一个参数给予特殊处理,对于不收集任何其他连接点上下文的通知方法来说特别方便。在这种情况下,您可以省略 argNames
属性。例如,以下通知不需要声明 argNames
属性:
使用参数继续执行
我们之前提到过,我们将描述如何编写一个带有参数的 proceed
调用,使其在 Spring AOP 和 AspectJ 中都能一致地工作。解决方案是确保通知签名按顺序绑定每个方法参数。以下示例展示了如何做到这一点:
- 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});
}
引用在共享命名切入点定义中定义的
inDataAccessLayer
命名切入点。
@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))
}
引用在共享命名切入点定义中定义的
inDataAccessLayer
命名切入点。
在许多情况下,你都会进行这种绑定(如前面的例子中所示)。
通知顺序
当多个通知都想在同一个连接点运行时,会发生什么?Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序。优先级最高的通知在“进入时”首先运行(因此,假设有两个前置通知,优先级最高的通知首先运行)。在从连接点“退出”时,优先级最高的通知最后运行(因此,假设有两个后置通知,优先级最高的通知将第二个运行)。
当定义在不同方面的两个建议都需要在同一个连接点运行时,除非另有说明,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这可以通过在方面类中实现 org.springframework.core.Ordered
接口或使用 @Order
注解来完成,这与 Spring 的常规方式相同。给定两个方面,返回较低 Ordered.getOrder()
值(或注解值)的方面具有更高的优先级。
每种特定方面的不同建议类型在概念上都是直接应用于连接点的。因此,一个 @AfterThrowing
建议方法不应该从一个伴随的 @After
/@AfterReturning
方法中接收异常。
在同一个 @Aspect
类中定义的需要在同一个连接点运行的建议方法,会根据它们的建议类型被赋予优先级,优先级从高到低的顺序如下:@Around
、@Before
、@After
、@AfterReturning
、@AfterThrowing
。然而需要注意的是,一个 @After
建议方法实际上会在同一方面的任何 @AfterReturning
或 @AfterThrowing
建议方法之后被调用,这遵循了 AspectJ 的 @After
的 "after finally advice" 语义。
当在同一个 @Aspect
类中定义的两个相同类型的建议(例如,两个 @After
建议方法)都需要在同一个连接点运行时,顺序是未定义的(因为无法通过反射检索 javac 编译类的源代码声明顺序)。考虑将此类建议方法合并为每个 @Aspect
类中每个连接点的一个建议方法,或者将建议部分重构为可以通过 Ordered
或 @Order
在方面级别排序的单独 @Aspect
类。