在 Spring AOP 中获取 Advise 方法信息

1、简介

本文将带你了解如何使用 Spring AOP Aspect 获取 Advise 方法的签名、参数、注解以及其他的额外信息。

2、Maven 依赖

首先,在 pom.xml 中添加 spring-boot-starter-aop Starter 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3、创建 Pointcut 注解

创建一个 AccountOperation 注解,作为切面中的切点:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountOperation {
    String operation();
}

注意,切点(Pointcut)不一定非要使用注解定义。也可以使用 Spring AOP 提供的 Pointcut 定义语言定义其他 Pointcut 类型,如类中的某些方法、以某些前缀开头的方法等。

4、创建示例 Service

4.1、Account 类

创建一个 Account POJO,带有 accountNumberbalance 属性。

在 service 方法中使用它作为方法参数:

public class Account {

    private String accountNumber;
    private double balance;

    // get/set toString方法省略
}

4.2、Service 类

现在,创建 BankAccountService 类,它有 2 个方法,并且注解了 @AccountOperation,以便在切面中获取方法的信息。注意,withdraw 方法会抛出一个受检异常 WithdrawLimitException,以演示如何获取方法抛出的异常信息。

此外,注意 getBalance 方法没有 AccountOperation 注解,因此不会被切面拦截:

@Component
public class BankAccountService {

    @AccountOperation(operation = "deposit")
    public void deposit(Account account, Double amount) {
        account.setBalance(account.getBalance() + amount);
    }

    @AccountOperation(operation = "withdraw")
    public void withdraw(Account account, Double amount) throws WithdrawLimitException {

        if(amount > 500.0) {
            throw new WithdrawLimitException("Withdraw limit exceeded.");
        }

        account.setBalance(account.getBalance() - amount);
    }

    public double getBalance() {
        return RandomUtils.nextDouble();
    }
}

5、定义切面(Aspect)

创建 BankAccountAspect,从 BankAccountService 调用的相关方法中获取所有必要信息:

@Aspect
@Component
public class BankAccountAspect {

    @Before(value = "@annotation(com.baeldung.method.info.AccountOperation)")
    public void getAccountOperationInfo(JoinPoint joinPoint) {

        // 方法信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        System.out.println("full method description: " + signature.getMethod());
        System.out.println("method name: " + signature.getMethod().getName());
        System.out.println("declaring type: " + signature.getDeclaringType());

        // 方法参数
        System.out.println("Method args names:");
        Arrays.stream(signature.getParameterNames())
          .forEach(s -> System.out.println("arg name: " + s));

        System.out.println("Method args types:");
        Arrays.stream(signature.getParameterTypes())
          .forEach(s -> System.out.println("arg type: " + s));

        System.out.println("Method args values:");
        Arrays.stream(joinPoint.getArgs())
          .forEach(o -> System.out.println("arg value: " + o.toString()));

        // 额外信息
        System.out.println("returning type: " + signature.getReturnType());
        System.out.println("method modifier: " + Modifier.toString(signature.getModifiers()));
        Arrays.stream(signature.getExceptionTypes())
          .forEach(aClass -> System.out.println("exception type: " + aClass));

        // 方法注解
        Method method = signature.getMethod();
        AccountOperation accountOperation = method.getAnnotation(AccountOperation.class);
        System.out.println("Account operation annotation: " + accountOperation);
        System.out.println("Account operation value: " + accountOperation.operation());
    }
}

Pointcut (切点)定义为注解,因此,由于 BankAccountService 中的 getBalance 方法没有注解为 AccountOperation,因此切面不会拦截它。

接下来详细分析一每个部分,并查看在调用 BankAccountService 方法时控制台中的输出内容。

5.1、获取方法签名信息

为了获取方法签名信息,需要从 JoinPoint 对象中获取 MethodSignature

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

System.out.println("full method description: " + signature.getMethod());
System.out.println("method name: " + signature.getMethod().getName());
System.out.println("declaring type: " + signature.getDeclaringType());

现在调用 Service 的 withdraw() 方法:

@Test
void withdraw() {
    bankAccountService.withdraw(account, 500.0);
    assertTrue(account.getBalance() == 1500.0);
}

运行 withdraw() 测试后,可以在控制台看到以下输出:

full method description: public void com.baeldung.method.info.BankAccountService.withdraw(com.baeldung.method.info.Account,java.lang.Double) throws com.baeldung.method.info.WithdrawLimitException
method name: withdraw
declaring type: class com.baeldung.method.info.BankAccountService

5.2、获取参数信息

要检索有关方法参数的信息,可以使用 MethodSignature 对象:

System.out.println("Method args names:");
Arrays.stream(signature.getParameterNames()).forEach(s -> System.out.println("arg name: " + s));

System.out.println("Method args types:");
Arrays.stream(signature.getParameterTypes()).forEach(s -> System.out.println("arg type: " + s));

System.out.println("Method args values:");
Arrays.stream(joinPoint.getArgs()).forEach(o -> System.out.println("arg value: " + o.toString()));

调用 BankAccountService 中的 deposit 方法:

@Test
void deposit() {
    bankAccountService.deposit(account, 500.0);
    assertTrue(account.getBalance() == 2500.0);
}

控制台输出如下:

Method args names:
arg name: account
arg name: amount
Method args types:
arg type: class com.baeldung.method.info.Account
arg type: class java.lang.Double
Method args values:
arg value: Account{accountNumber='12345', balance=2000.0}
arg value: 500.0

5.3、获取方法注解的相关信息

可以使用 Method 类的 getAnnotation() 方法获取注解的相关信息:

Method method = signature.getMethod();
AccountOperation accountOperation = method.getAnnotation(AccountOperation.class);
System.out.println("Account operation annotation: " + accountOperation);
System.out.println("Account operation value: " + accountOperation.operation());

现在重新运行 withdraw() 测试,结果如下:

Account operation annotation: @com.baeldung.method.info.AccountOperation(operation=withdraw)
Account operation value: withdraw

5.4、获取其他信息

还可以获得有关方法的一些附加信息,如返回类型、Modifier(权限修饰符)以及抛出的异常(如果有的话):

System.out.println("returning type: " + signature.getReturnType());
System.out.println("method modifier: " + Modifier.toString(signature.getModifiers()));
Arrays.stream(signature.getExceptionTypes())
  .forEach(aClass -> System.out.println("exception type: " + aClass));

现在,创建一个新测试方法 withdrawWhenLimitReached,使 withdraw() 方法超过其定义的 withdraw 限额:

@Test 
void withdrawWhenLimitReached() 
{ 
    Assertions.assertThatExceptionOfType(WithdrawLimitException.class)
      .isThrownBy(() -> bankAccountService.withdraw(account, 600.0)); 
    assertTrue(account.getBalance() == 2000.0); 
}

现在,查看控制台输出:

returning type: void
method modifier: public
exception type: class com.baeldung.method.info.WithdrawLimitException

最后一个测试用来演示 getBalance() 方法。如前所述,由于方法声明中没有 AccountOperation 注解,因此该切面不会拦截该方法:

@Test
void getBalance() {
    bankAccountService.getBalance();
}

运行该测试时,控制台中没有任何输出,符合预期。

6、总结

本文介绍了如何使用 Spring AOP 切面获取方法的所有可用信息。


Ref:https://www.baeldung.com/spring-aop-get-advised-method-info