1. Introduction

In this tutorial, we’ll show you how to get all the information about a method’s signature, arguments, and annotations, using a Spring AOP aspect.

2. Maven Dependencies

Let’s start by adding Spring Boot AOP Starter  and the commons-rng-simple library dependencies in the pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-rng-simple</artifactId>
</dependency>

3. Creating Our Pointcut Annotation

Let’s create an AccountOperation annotation. To clarify, we’ll use it as the pointcut in our aspect:

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

Note that creating an annotation is not mandatory for defining a pointcut. In other words, we can define other pointcuts types like certain methods in a class, methods starting with some prefix, etc., by using the pointcut definition language provided by Spring AOP.

4. Creating Our Example Service

4.1. Account Class

Let’s create an Account POJO with accountNumber and balance properties. We’ll use it as the method argument in our service methods:

public class Account {

    private String accountNumber;
    private double balance;

    // getter / setters / toString
}

4.2. Service Class

Let’s now create the BankAccountService class with two methods we annotate with @AccountOperation annotation so we can get the information of the methods in our aspect. Note the withdraw method throws a checked exception WithdrawLimitException to demonstrate how we can get the information about the exceptions thrown by a method. We will use the UniformRangeProvider in commons-rng-simple library from apache-commons to simulate balance for the bank account.

Also, note the getBalance method doesn’t have the AccountOperation annotation, so it won’t be intercepted by the aspect:

@Component
public class BankAccountService {

    private final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();

    @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 rng.nextDouble();
    }
}

5. Defining Our Aspect

Let’s create a BankAccountAspect to get all the necessary information from the related methods called in our BankAccountService:

@Aspect
@Component
public class BankAccountAspect {

    @Before(value = "@annotation(com.baeldung.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());
    }
}

Note we defined our pointcut as an annotation, so as the getBalance method in our BankAccountService is not annotated with AccountOperation, the aspect won’t intercept it.

Let’s now analyze each part of our aspect in detail and look at what we get in the console when calling the BankAccountService methods.

5.1. Getting the Information About Method Signature

To be able to get our method signature information, we need to retrieve the MethodSignature from the JoinPoint object:

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());

Let’s now call the withdraw() method of our service:

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

After running the withdraw() test, we can now see on the console the following results:

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. Getting the Information About Arguments

To retrieve the information about  the method arguments, we can use the MethodSignature object:

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()));

Let´s try this by calling the deposit method in BankAccountService:

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

This is what we see on the console:

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. Getting the Information About Method Annotations

We can get the information about an annotation by using the getAnnotation() method of the Method class:

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());

Let’s now re-run our withdraw() test and check what we get:

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

5.4. Getting the Additional Information

We can get some additional information about our methods, like their returning type, their modifiers, and what exceptions they throw, if any:

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

Let’s now create a new test withdrawWhenLimitReached that makes the withdraw() method exceed its defined withdraw limit:

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

Let´s now check the console output:

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

Our last test will be useful to demonstrate the getBalance() method. As we previously said, it’s not intercepted by the aspect because there is no AccountOperation annotation in the method declaration:

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

When running this test, there is no output in the console, as we expected should be the case.

6. Conclusion

In this article, we saw how to get all the available information about a method using a Spring AOP aspect.  We did that by defining a pointcut, printing out the information into the console, and checking the results of running the tests.

The source code for our application is available over on GitHub.

Course – LS (cat=Spring)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!