声明通知
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
)。
请注意, |
最终后置通知
最终后置通知在匹配的方法执行退出时运行。它通过使用@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[]
)。数组中的值将在调用底层方法时用作底层方法的参数。
使用 Spring采用的方法更简单,也更符合其基于代理的、仅执行语义。只有在您编译为Spring编写的 |
around通知返回的值是方法调用者看到返回值。例如,一个简单的缓存方面可以在缓存中找到值时返回缓存中的值,或者如果找不到值则调用proceed()
(并返回该值)。请注意,proceed
可以在around通知的主体中调用一次、多次或根本不调用。所有这些都是合法的。
如果将around通知方法的返回类型声明为void ,则始终会向调用者返回null ,从而有效地忽略对proceed() 的任何调用的结果。因此,建议around通知方法声明Object 的返回类型。通知方法通常应返回从proceed() 调用返回的值,即使底层方法具有void 返回类型也是如此。但是,根据用例,通知可以选择返回缓存值、包装值或其他一些值。 |
以下示例显示了如何使用around通知
-
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提供完全类型化的通知,这意味着您在通知签名中声明所需的参数(如前面returning和throwing示例中所示),而不是一直使用Object[]
数组。我们将在本节后面看到如何使参数和其他上下文值可用于通知主体。首先,我们来看看如何编写可以了解当前正在通知的方法的通用通知。
访问当前JoinPoint
任何通知方法都可以将其第一个参数声明为org.aspectj.lang.JoinPoint
类型的参数。请注意,around通知需要声明一个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
对象可用于通知。
另一种方法是声明一个切点,当它匹配连接点时“提供”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();
// ...
}
1 | 引用组合切点表达式中定义的名为publicMethod 的切点。 |
@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") (1)
fun audit(auditable: Auditable) {
val code = auditable.value()
// ...
}
1 | 引用组合切点表达式中定义的名为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
-
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 | 引用组合切点表达式中定义的名为publicMethod 的切点。 |
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 | 引用组合切点表达式中定义的名为publicMethod 的切点。 |
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 | 引用组合切点表达式中定义的名为publicMethod 的切点。 |
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 | 引用组合切点表达式中定义的名为publicMethod 的切点。 |
2 | 声明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});
}
1 | 引用共享命名切点定义中定义的名为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))
}
1 | 引用共享命名切点定义中定义的名为inDataAccessLayer 的切点。 |
在许多情况下,您无论如何都会进行此绑定(如前面的示例所示)。
通知顺序
当多个通知都想要在同一个连接点运行时会发生什么?Spring AOP遵循与AspectJ相同的优先级规则来确定通知执行的顺序。“进入”时,最高优先级通知首先运行(因此,给定两段before通知,优先级最高的通知首先运行)。从连接点“出来”时,最高优先级通知最后运行(因此,给定两段after通知,优先级最高的通知将第二个运行)。
当在不同方面定义的两条建议都需要在同一个连接点运行时,除非您另行指定,否则执行顺序是不确定的。您可以通过指定优先级来控制执行顺序。这可以通过在方面类中实现org.springframework.core.Ordered
接口或使用@Order
注解来完成,这与Spring的常规方式一致。对于两个方面,从Ordered.getOrder()
(或注解值)返回较小值的方面具有较高的优先级。
特定方面中的每种不同的建议类型在概念上都旨在直接应用于连接点。因此, 在同一个 当在同一个 |