Spring Boot AOP: Part 5 - @Around Advice and Custom Annotations

Yet Another AOP Project

In Part 4, we used @Before and @After advice to add transaction logging. But what if you need to wrap around a method (run code before AND after), and even control whether the method executes at all?

That’s where @Around advice comes in. It’s the most powerful advice type in AOP.

Today, we’re building a performance monitoring system that tracks how long methods take to execute. And we’ll do it using a custom annotation called @TrackTime.

Let’s dive in!


What We’re Building

We’ll create a simple order processing system where we can track execution time by just adding @TrackTime to any method:

@TrackTime
public void processOrder() {
    // business logic
}

Output:

void OrderService.processOrder() executed in 1002 ms

No manual timing code, no stopwatch logic in your business methods. Just one annotation, and AOP handles the rest!


Project Setup

Quick Start

  1. Go to start.spring.io
  2. Create a new project:
    • Group: com.adigitallab
    • Artifact: aop-performance-tracker
    • Dependencies: None (we’ll add AOP manually)
  3. Download, unzip, and open in your IDE

Add AOP Dependency

Add this to your pom.xml:

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

That’s it for setup! Now let’s build.


Step 1: Create a Custom Annotation

First, let’s create our @TrackTime annotation. Create a new package com.adigitallab.aop and add this class:

package com.adigitallab.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackTime {
}

Understanding the Annotation

@Target(ElementType.METHOD): This annotation can only be used on methods (not classes or fields)

@Retention(RetentionPolicy.RUNTIME): The annotation will be available at runtime (so AOP can detect it)

public @interface TrackTime: Defines a custom annotation named TrackTime

Why create a custom annotation?

Instead of writing complex pointcut expressions like:

execution(* com.adigitallab.service.OrderService.*(..))

We can simply use:

@annotation(com.adigitallab.aop.TrackTime)

It’s cleaner, more flexible, and easier to apply to any method you want to track!


Step 2: Create the OrderService

Create a new package com.adigitallab.service and add this service:

package com.adigitallab.service;

import com.adigitallab.aop.TrackTime;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    
    @TrackTime
    public void processOrder() throws InterruptedException {
        Thread.sleep(1000); // Simulate a slow task
    }
}

Notice how clean this is! The @TrackTime annotation is all we need. Nothing extra, just pure business logic (well, a simulated slow task).


Step 3: Create the PerformanceAspect

Now for the magic! Create PerformanceAspect.java in the com.adigitallab.aop package:

package com.adigitallab.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class PerformanceAspect {
    
    @Around("@annotation(com.adigitallab.aop.TrackTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        // Call the actual method
        Object result = joinPoint.proceed();
        
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        
        System.out.println(
            joinPoint.getSignature() + " executed in " + executionTime + " ms"
        );
        
        return result;
    }
}

Understanding @Around Advice

This is different from @Before and @After! Let’s break it down:

@Around: Wraps around the method execution—you control when (or if) the method runs

ProceedingJoinPoint: Special type of JoinPoint that lets you call proceed() to execute the actual business method

joinPoint.proceed(): This is where the actual business method runs. Everything before this is “before” logic, everything after is “after” logic

return result: You must return the result from the actual method (if it has a return value)

The Flow

1. @Around advice starts

2. Record start time

3. joinPoint.proceed() → Actual method runs

4. Record end time

5. Calculate execution time

6. Print the result

7. Return the result

Important: Always call joinPoint.proceed() in your @Around advice! If you forget, the actual method will never execute.

Also, always return the result from proceed(). If you don’t, methods that return values will return null!


Step 4: Update the Main Application

Update your Application.java:

package com.adigitallab;

import com.adigitallab.service.OrderService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class Application implements CommandLineRunner {
    
    private final OrderService orderService;
    
    public Application(OrderService orderService) {
        this.orderService = orderService;
    }
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    @Override
    public void run(String... args) throws Exception {
        orderService.processOrder();
    }
}

We’re using CommandLineRunner here, which runs automatically after Spring Boot starts. It’s perfect for testing!


Step 5: Run and See the Magic

Run your application. You should see output like this:

void com.adigitallab.service.OrderService.processOrder() executed in 1002 ms

That’s it!

The method took about 1000ms (because of Thread.sleep(1000)), and our aspect automatically tracked and logged it.


Why @Around is Powerful

@Around advice is the most flexible because you can:

1. Run Code Before and After

@Around("@annotation(TrackTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Before method");
    Object result = joinPoint.proceed();
    System.out.println("After method");
    return result;
}

2. Modify Arguments

@Around("@annotation(TrackTime)")
public Object modifyArgs(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    // Modify arguments
    args[0] = "Modified value";
    return joinPoint.proceed(args); // Pass modified args
}

3. Modify Return Values

@Around("@annotation(TrackTime)")
public Object modifyResult(ProceedingJoinPoint joinPoint) throws Throwable {
    Object result = joinPoint.proceed();
    // Modify the result
    return result + " (modified)";
}

4. Handle Exceptions

@Around("@annotation(TrackTime)")
public Object handleException(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        return joinPoint.proceed();
    } catch (Exception e) {
        System.out.println("Exception caught: " + e.getMessage());
        return null; // Return default value
    }
}

5. Skip Method Execution

@Around("@annotation(TrackTime)")
public Object skipMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    if (someCondition) {
        return null; // Don't call proceed() - method never runs!
    }
    return joinPoint.proceed();
}

Let’s Enhance It!

Add Multiple Methods

Update OrderService to track multiple methods:

@Service
public class OrderService {
    
    @TrackTime
    public void processOrder() throws InterruptedException {
        Thread.sleep(1000);
        System.out.println("Order processed");
    }
    
    @TrackTime
    public void validateOrder() throws InterruptedException {
        Thread.sleep(500);
        System.out.println("Order validated");
    }
    
    @TrackTime
    public void shipOrder() throws InterruptedException {
        Thread.sleep(1500);
        System.out.println("Order shipped");
    }
}

Update Application.java to call all methods:

@Override
public void run(String... args) throws Exception {
    orderService.validateOrder();
    orderService.processOrder();
    orderService.shipOrder();
}

Output:

Order validated
void OrderService.validateOrder() executed in 502 ms
Order processed
void OrderService.processOrder() executed in 1001 ms
Order shipped
void OrderService.shipOrder() executed in 1501 ms

Every method with @TrackTime is automatically tracked!


Comments

Join the discussion and share your thoughts