1. Введение
В этом руководстве мы покажем вам, как получить всю информацию о сигнатуре, аргументах и аннотациях метода, используя аспект Spring AOP .
2. Зависимости Maven
Начнем с добавления зависимости библиотеки Spring Boot AOP Starter в pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3. Создание нашей аннотации Pointcut
Давайте создадим аннотацию AccountOperation
. Чтобы уточнить, мы будем использовать его как pointcut в нашем аспекте:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountOperation {
String operation();
}
Обратите внимание, что создание аннотации не является обязательным для определения точечного разреза. Другими словами, мы можем определить другие типы pointcut, такие как определенные методы в классе, методы, начинающиеся с некоторого префикса, и т. д., используя язык определения pointcut, предоставляемый Spring AOP.
4. Создание нашего примера сервиса
4.1. Класс счета
Давайте создадим Account
POJO со свойствами accountNumber
и balance
. Мы будем использовать его в качестве аргумента метода в наших сервисных методах:
public class Account {
private String accountNumber;
private double balance;
// getter / setters / toString
}
4.2. Класс обслуживания
Давайте теперь создадим класс BankAccountService
с двумя методами, которые мы аннотируем аннотацией @AccountOperation
, чтобы мы могли получить информацию о методах в нашем аспекте. Обратите внимание, что метод remove
создает проверенное исключение 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. Определение нашего аспекта
Давайте создадим BankAccountAspect
, чтобы получить всю необходимую информацию от связанных методов, вызываемых в нашем BankAccountService:
@Aspect
@Component
public class BankAccountAspect {
@Before(value = "@annotation(com.foreach.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());
}
}
Обратите внимание, что мы определили наш pointcut как аннотацию, поэтому, поскольку метод getBalance
в нашем BankAccountService
не аннотирован AccountOperation,
аспект не будет его перехватывать.
Давайте теперь подробно разберем каждую часть нашего аспекта и посмотрим, что мы получаем в консоли при вызове методов BankAccountService
.
5.1. Получение информации о сигнатуре метода
Чтобы иметь возможность получить информацию о подписи нашего метода, нам нужно получить MethodSignature
из объекта 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());
Давайте теперь вызовем метод remove()
нашего сервиса:
@Test
void withdraw() {
bankAccountService.withdraw(account, 500.0);
assertTrue(account.getBalance() == 1500.0);
}
После запуска теста remove()
мы видим в консоли следующие результаты:
full method description: public void com.foreach.method.info.BankAccountService.withdraw(com.foreach.method.info.Account,java.lang.Double) throws com.foreach.method.info.WithdrawLimitException
method name: withdraw
declaring type: class com.foreach.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
:
@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.foreach.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. Получение информации об аннотациях методов
Мы можем получить информацию об аннотации, используя метод getAnnotation()
класса Method
:
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());
Теперь давайте снова запустим наш тест remove()
и проверим, что мы получили:
Account operation annotation: @com.foreach.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));
Давайте теперь создадим новый тест removeWhenLimitReached
, который заставит метод remove()
превышать установленный лимит на снятие средств:
@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.foreach.method.info.WithdrawLimitException
Наш последний тест будет полезен для демонстрации метода getBalance()
. Как мы уже говорили ранее, аспект не перехватывает его, потому что в объявлении метода нет аннотации AccountOperation
:
@Test
void getBalance() {
bankAccountService.getBalance();
}
При запуске этого теста в консоли нет вывода, как мы и ожидали.
6. Заключение
В этой статье мы увидели, как получить всю доступную информацию о методе, используя аспект Spring AOP. Мы сделали это, определив pointcut, распечатав информацию в консоли и проверив результаты выполнения тестов.
Исходный код нашего приложения доступен на GitHub .