在Spring AOP中获取通知的方法信息

2023/05/13

1. 概述

在本教程中,我们演示如何使用Spring AOP切面获取有关方法签名、参数和注解的所有信息。

2. Maven依赖


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

3. 创建切入点注解

我们创建一个@AccountOperation注解,使用它作为切面的切入点:


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

请注意,创建注解对于定义切入点不是必需的。 也就是说,我们可以使用Spring AOP提供的切入点定义语言来定义其他切入点类型,比如类中的某些方法、以某个前缀开头的方法等。

4. 创建相关类

4.1 Account

让我们创建一个包含accountNumber和balance属性的Account POJO,我们会在Service方法中使用它作为方法参数:

public class Account {

    private String accountNumber;
    private double balance;

    // getters、setters、toString ...
}

4.2 Service

现在我们创建包含两个方法的BankAccountService类,并使用@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();
    }
}

public class WithdrawLimitException extends RuntimeException {

    public WithdrawLimitException(String message) {
        super(message);
    }
}

5. 定义切面

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


@Aspect
@Component
public class BankAccountAspect {

    @Before(value = "@annotation(cn.tuyucheng.taketoday.method.info.AccountOperation)")
    public void getAccountOperationInfo(JoinPoint joinPoint) {

        // Method Information
        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());

        // Method args
        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()));

        // Additional Information
        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 annotation
        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());
    }
}

我们将切入点定义为注解,因此,由于BankAccountService中的getBalance方法没有使用@AccountOperation进行标注,切面不会拦截它。

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()方法:


@SpringBootTest
class BankAccountServiceIntegrationTest {

    @Autowired
    BankAccountService bankAccountService;
    private Account account;

    @BeforeEach
    public void setup() {
        account = new Account();
        account.setAccountNumber("12345");
        account.setBalance(2000.0);
    }

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

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

full method description: public void cn.tuyucheng.taketoday.method.info.BankAccountService.withdraw
(cn.tuyucheng.taketoday.method.info.Account,java.lang.Double) throws cn.tuyucheng.taketoday.method.info.WithdrawLimitException
method name: withdraw
declaring type: class cn.tuyucheng.taketoday.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()));
class BankAccountServiceIntegrationTest {

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

这是我们在控制台上能够看到的:

Method args names:
arg name: account
arg name: amount
Method args types:
arg type: class cn.tuyucheng.taketoday.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: @cn.tuyucheng.taketoday.method.info.AccountOperation(operation="withdraw")
Account operation value: withdraw

5.4 获取其它信息

我们也可以获取一些关于方法的额外信息,比如它们的返回类型、它们的修饰符以及它们抛出的异常(如果有的话):

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()方法超过其定义的取款限制:

class BankAccountServiceIntegrationTest {

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

然后观察控制台输出:

returning type: void
method modifier: public
exception type: class cn.tuyucheng.taketoday.method.info.WithdrawLimitException

最后一个测试演示getBalance()方法。 正如我们之前所说,它不会被切面拦截,因为方法声明中没有指定@AccountOperation注解:

class BankAccountServiceIntegrationTest {

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

运行此测试时,控制台中没有任何输出。

6. 总结

在本文中,我们演示了如何使用Spring AOP切面获取有关方法的所有可用信息。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章