跳到主要内容
版本:7.0.3

声明建议

Hunyuan 7b 中英对照 Declaring Advice

建议(Advice)与一个切点表达式(Pointcut Expression)相关联,会在与该切点匹配的方法执行之前、之后或期间运行。该切点表达式可以是一个内联切点(inline pointcut),或者是对命名切点的引用。

事先建议

你可以通过使用@Before注释来在某个方面的建议之前进行声明。

以下示例使用了一个内联切点表达式。

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
public class BeforeExample {

@Before("com.xyz.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}

返回后的建议

当匹配的方法执行正常返回后,@AfterReturning注解所定义的回调方法会被执行。你可以使用@AfterReturning注解来声明它。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

@AfterReturning("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
备注

你可以在同一个方面中有多个建议声明(以及其他成员)。在这些示例中,我们只展示一个建议声明,以便集中展示每个声明的效果。

有时,你需要在建议体中访问实际返回的值。你可以使用@AfterReturning的形式来绑定返回值以获取该访问权限,如下例所示:

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) {
// ...
}
}

returning属性中使用的名称必须与建议方法(advice method)中参数的名称相对应。当方法执行返回时,返回值会作为相应的参数值传递给建议方法。returning子句还限制了匹配范围,仅限于那些返回指定类型值的方法执行(在此情况下为Object,该类型可以匹配任何返回值)。

请注意,在使用“after returning advice”之后,无法返回一个完全不同的参考文献。

抛出异常后的建议

当匹配方法的执行因抛出异常而退出时,@AfterThrowing注解会被触发。如以下示例所示,你可以使用该注解来声明这一行为:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
public void doRecoveryActions() {
// ...
}
}

通常,你希望建议(advice)仅在特定类型的异常被抛出时才执行,并且你往往还需要在建议体中访问被抛出的异常。你可以使用 throwing 属性来限制匹配(如果需要的话——否则可以使用 Throwable 作为异常类型),并将被抛出的异常绑定到建议的参数上。以下示例展示了如何实现这一点:

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) {
// ...
}
}

throwing属性中使用的名称必须与建议方法(advice method)中的参数名称相对应。当一个方法的执行因抛出异常而终止时,该异常会作为相应的参数值传递给建议方法。throwing子句还限制了匹配条件,仅允许那些抛出指定类型异常(本例中为DataAccessException)的方法执行被选中。

备注

请注意,@AfterThrowing并不表示一个通用的异常处理回调。具体来说,@AfterThrowing建议方法只应该接收来自连接点(用户声明的目标方法)本身的异常,而不应接收来自伴随的@After/@AfterReturning方法的异常。

最后建议

在匹配的方法执行结束时(终于),after(finally)建议才会被执行。它是通过使用@After注解来声明的。after建议必须能够处理正常返回条件和异常返回条件。它通常用于释放资源等类似的目的。以下示例展示了如何使用after finally建议:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

@After("execution(* com.xyz.dao.*.*(..))")
public void doReleaseLock() {
// ...
}
}
备注

请注意,AspectJ中的@After建议被定义为“在finally建议之后执行”,类似于try-catch语句中的finally块。无论从连接点(用户声明的目标方法)返回的结果如何,无论是正常返回还是抛出异常,@After都会被调用;而@AfterReturning则仅适用于成功的正常返回情况。

围绕建议

最后一类建议是“围绕(method)的建议”(around advice)。这种建议会在匹配方法的执行过程中“环绕”该方法进行操作。它有机会在方法执行前后执行某些操作,并能够决定该方法何时、如何执行,甚至是否真的需要执行。当需要在线程安全的方式下在方法执行前后共享状态时,常常会使用这种建议——例如,启动和停止定时器。

提示

始终使用满足你需求的最简单的建议形式。
例如,如果“在...之前”这个建议就足以满足你的需要,那就不要使用“在...周围”这个建议。

通过使用@Around注解来声明环绕(around)建议。该方法应声明其返回类型为Object,并且该方法的第一个参数必须是ProceedingJoinPoint类型。在建议方法的主体内部,必须对ProceedingJoinPoint调用proceed(),以便底层方法能够运行。如果不带参数调用proceed(),那么当底层方法被调用时,将会使用调用者的原始参数。对于高级用例,proceed()方法还有一个重载版本,它可以接受一个参数数组(Object[])。该数组中的值将在底层方法被调用时作为其参数使用。

备注

proceed被调用并传入一个Object[]数组时,其行为与AspectJ编译器生成的环绕(around)建议(advice)中的proceed行为略有不同。对于使用传统AspectJ语言编写的环绕建议来说,传递给proceed的参数数量必须与传递给该环绕建议的参数数量相匹配(而不是底层连接点(join point)所接收的参数数量),并且传递给proceed的参数在指定位置上的值会替换连接点处该参数所绑定的实体的原始值(如果现在这部分内容让你感到困惑,不必担心)。

Spring采取的方法更为简单,也更符合其基于代理、仅执行(execution-only)的语义。只有当你为Spring编写@AspectJ方面的代码,并使用AspectJ编译器和织造器(weaver)来调用proceed时,才需要关注这种差异。实际上,有一种方式可以编写出在Spring AOP和AspectJ之间100%兼容的方面(aspect),这部分内容将在关于建议参数的后续章节中详细讨论。

around 通知返回的值是该方法调用者所看到的返回值。例如,一个简单的缓存组件可以在有缓存的情况下从缓存中获取值,如果没有缓存则调用 proceed()(并返回该值)。请注意,在 around 通知的实现体内,proceed 可能会被调用一次、多次,或者根本不被调用。所有这些情况都是合法的。

注意

如果您将 around advice 方法的返回类型声明为 void,则总会向调用者返回 null,这实际上会忽略对 proceed() 的任何调用结果。因此,建议 around advice 方法声明的返回类型为 Object。通常情况下,advise 方法应返回从 proceed() 调用中得到的值,即使底层方法的返回类型是 void。不过,根据使用场景的不同,advise 方法也可以选择返回缓存的值、包装后的值或其他类型的值。

以下示例展示了如何使用“around”这个建议:

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;
}
}

建议参数

Spring提供了完全类型的建议(advise),这意味着你需要在建议的签名中声明所需的参数(正如我们之前在返回值和抛出异常的例子中看到的那样),而不是一直使用Object[]数组。在本节的后面部分,我们将了解如何让参数和其他上下文值能够在建议体(advice body)中使用。首先,我们来看看如何编写通用的建议(generic advice),以便能够获取当前被该建议所关注的方法的信息。

访问当前的 JoinPoint

任何通知方法都可以声明一个类型为org.aspectj.lang.JoinPoint的参数作为其第一个参数。需要注意的是,对于围绕(around)通知,需要声明一个类型为ProceedingJoinPoint的第一个参数,而ProceedingJoinPointJoinPoint的子类。

JoinPoint接口提供了一些有用的方法:

  • getArgs(): 返回方法参数。
  • getThis(): 返回代理对象。
  • getTarget(): 返回目标对象。
  • getSignature(): 返回被建议(advised)的方法的描述。
  • toString(): 打印被建议方法的有用描述。

有关更多详细信息,请参阅javadoc

向 Advice 传递参数

我们已经了解了如何绑定返回值或异常值(通过使用“after returning”和“after throwing”建议)。为了让建议体能够访问参数值,你可以使用args的绑定形式。如果在args表达式中使用参数名而不是类型名,那么在调用该建议时,相应参数的值就会被作为参数值传递进来。一个例子应该能更清楚地说明这一点。假设你想对那些以Account对象作为第一个参数的DAO操作进行建议处理,并且你需要在建议体中访问这个账户信息,你可以这样写:

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
// ...
}

点切表达式中的args(account,..)部分有两个作用。首先,它限制匹配条件为那些方法至少接受一个参数,并且传递给该参数的参数是Account实例的情况。其次,它通过account参数将实际的Account对象提供给通知(advise)代码使用。

另一种写法是声明一个在匹配到连接点(join point)时“提供”Account对象值的切点(pointcut),然后在建议(advice)中引用这个命名切点。其代码实现如下:

@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}

有关更多详细信息,请参阅AspectJ编程指南。

代理对象(this)、目标对象(target)以及注释(@within@target@annotation@args)都可以以类似的方式进行绑定。接下来的示例展示了如何匹配带有 @Auditable 注解的方法的执行,并提取审计代码:

以下是@Auditable注释的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}

以下是与执行@Auditable方法相匹配的建议:

@Before("com.xyz.Pointcuts.publicMethod() && @annotation(auditable)") 1
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}

建议参数和泛型

Spring AOP可以处理在类声明和方法参数中使用的泛型。假设你有一个如下所示的泛型类型:

public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}

你可以通过将 advice 参数与你希望拦截方法的参数类型关联起来,来限制对特定参数类型的方法类型的拦截:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}

这种方法不适用于通用的集合(generic collections)。因此,你不能如下定义切点(pointcut):

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}

要实现这一点,我们必须检查集合中的每一个元素,这是不现实的,因为我们也无法确定如何处理一般的 null 值。要达到类似的效果,就必须将参数设置为 Collection<?>,然后手动检查元素的类型。

确定参数名称

在建议(advice)调用中的参数绑定依赖于将切点表达式(pointcut expression)中使用的名称与建议及切点方法签名(pointcut method signature)中声明的参数名称进行匹配。

备注

在本节中,argument(参数)和parameter(参数)这两个术语被交替使用,因为AspectJ API将参数名称称为argument名称。

Spring AOP 使用以下 ParameterNameDiscoverer 实现来确定参数名称。每个发现器都将有机会来发现参数名称,第一个成功发现的发现器将获胜。如果注册的所有发现器都无法确定参数名称,则会抛出异常。

AspectJAnnotationParameterNameDiscoverer

使用用户通过相应的建议(advice)或切点(pointcut)注解中的argNames属性明确指定的参数名称。详情请参阅显式参数名称

KotlinReflectionParameterNameDiscoverer

使用Kotlin反射API来确定参数名称。只有在类路径上存在此类API时,才会使用此发现器。

StandardReflectionParameterNameDiscoverer

使用标准的 java.lang.reflect.Parameter API 来确定参数名称。要求代码在使用 javac 编译时需要加上 -parameters 标志。这是 Java 8 及以上版本推荐的方法。

AspectJAdviceParameterNameDiscoverer

从切点表达式中的returningthrowing子句中推断参数名称。有关所使用算法的详细信息,请参阅javadoc

明确的参数名称

@AspectJ的advise和pointcut注解有一个可选的argNames属性,你可以使用它来指定被注解方法的参数名称。

提示

如果一个@AspectJ切面是由AspectJ编译器(ajc)编译的,即使没有调试信息,也不需要添加argNames属性,因为编译器会保留所需的信息。

同样地,如果一个@AspectJ切面是使用javac-parameters标志编译的,也不需要添加argNames属性,因为编译器会保留所需的信息。

以下示例展示了如何使用 argNames 属性:

@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 的切点。

  • 声明 beanauditable 为参数名。

如果第一个参数的类型是 JoinPointProceedingJoinPointJoinPoint.StaticPart,则可以在 argNames 属性的值中省略该参数的名称。例如,如果您修改前述建议以接收连接点对象,则 argNames 属性不需要包含该名称:

@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 的切点。

  • 声明 beanauditable 为参数名。

对于类型为JoinPointProceedingJoinPointJoinPoint.StaticPart的第一个参数,所给予的特殊处理对于那些不收集任何其他连接点上下文的建议方法来说尤其方便。在这种情况下,您可以省略argNames属性。例如,以下建议方法就无需声明argNames属性:

@Before("com.xyz.Pointcuts.publicMethod()") 1
public void audit(JoinPoint jp) {
// ... use jp
}

继续进行论证

我们之前提到过,我们将描述如何编写一个能够在Spring AOP和AspectJ中一致使用的、带有参数的proceed调用。解决方法是要确保建议(Advice)的签名能够按照顺序绑定每个方法参数。以下示例展示了如何实现这一点:

@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});
}

在很多情况下,你无论如何都会进行这种绑定(就像前面的例子那样)。

建议排序

当多条建议(advice)都想在同一个连接点(join point)执行时,会发生什么?Spring AOP遵循与AspectJ相同的优先级规则来确定建议执行的顺序。优先级最高的建议会首先执行,“在进入连接点的过程中”(也就是说,如果有两条前置建议(before advice),那么优先级最高的那个会首先执行)。而在“从连接点退出的过程中”,优先级最高的建议会最后执行(也就是说,如果有两条后置建议(after advice),那么优先级最高的那个会其次执行)。

当两个从不同方面定义的建议(advices)都需要在同一个连接点(join point)执行时,除非另有指定,否则它们的执行顺序是不确定的。你可以通过指定优先级来控制执行顺序。在 Spring 中,通常可以通过在切面类中实现 org.springframework.core.Ordered 接口,或者使用 @Order 注解来实现这一点。如果有两个切面,那么返回来自 Ordered getOrder()(或注解值)较低数值的切面,将具有更高的优先级。

备注

每种不同类型的Advice在概念上都是直接应用于连接点(join point)的。因此,@AfterThrowing类型的Advice方法不应该接收来自配套的@After或@AfterReturning方法的异常。

在同一@Aspect类中定义的、需要在同一连接点执行的Advice方法,其执行优先级按照以下顺序确定:@Around、@Before、@After、@AfterReturning、@AfterThrowing。不过需要注意的是,根据AspectJ对于@After类型的“after finally”语义,@AfterAdvice方法实际上会在所有@AfterReturning或@AfterThrowing Advice方法之后被执行。

当在同一@Aspect类中定义了两个相同类型的Advice(例如,两个@AfterAdvice方法)并且它们都需要在同一连接点执行时,其执行顺序是不确定的(因为对于javac编译后的类,无法通过反射来获取源代码中的声明顺序)。可以考虑将这类Advice方法合并为每个连接点一个,或者将这些Advice方法重构为独立的@Aspect类,然后通过@Ordered或@Order在@Aspect层面来指定它们的执行顺序。