DEV Community

loading...
Cover image for Learn Aspect Oriented Programming by Example

Learn Aspect Oriented Programming by Example

pmgysel profile image Philipp Gysel ・7 min read

This post offers a great way for you to learn Aspect Oriented Programming by studying concrete examples. In particular, I will showcase SpringBoot AOP by implementing 4 Aspects.

Table of Contents:

If you’re the person who wants to skip lengthy descriptions and just look at concrete code, I’ve got you covered:

GitHub logo pmgysel / aop-examples

Some examples of Aspect Oriented Programming (AOP) with SpringBoot

What is an Aspect?

So there are some great resources out there for an overview of Spring AOP, including this Baeldung article and the official Spring AOP documentation. But since we don’t wanna focus on boring theory and rather keep things practical, here’s a really short summary how AOP works:

Aspect oriented programming explained

We’ll need the following terms in this tutorial:

  • Advice: the method which implements some common task like logging or caching
  • Pointcut: a pattern expression which matches the places where your Advice should be invoked
  • Aspect: The Advice plus the Pointcut expression
  • Bonus - Join point: All places in your code that represent candidates for a Pointcut

@Cacheable: a standard Spring Advice

Let’s start simple and consider an already implemented Advice by Spring, namely the @Cacheable annotation. Say your web service must compute numbers of the Fibonacci series.

If you don’t know what the Fibonacci series is: it’s the series starting with 0 and 1 and each consecutive number is the sum of the previous two numbers.

We implement the Fibonacci computation in a @Service class:

@Service
public class FibonacciService {
  public Long nthFibonacciTerm(Long n) {
    if (n == 1 || n == 0) {
      return n;
    }
    return nthFibonacciTerm(n-1) + nthFibonacciTerm(n-2);
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, we use this service class in our REST controller:

@RestController
public class WebController {
  @Autowired private final FibonacciService fibonacciService;

  @GetMapping(path = "/api/fibonacci/{number}")
  public Long fibonacci(@PathVariable(value = "number") Long number) {
    return fibonacciService.nthFibonacciTerm(number);
  }
}
Enter fullscreen mode Exit fullscreen mode

Our implementation is recursive and thus rather slow. So how do you make your web service faster? One way would be to use a faster algorithm, but let’s solve the problem with Spring’s @Cacheable feature. This annotation creates a cache in the background where all previous results get stored. All we must do, is add the @Cacheable annotation to our method:

@Cacheable("Fibonacci")
@GetMapping(path = "/api/fibonacci/{number}")
public Long fibonacci(@PathVariable(value = "number") Long number) { ... }
Enter fullscreen mode Exit fullscreen mode

Now we’re ready to test our caching mechanism by firing a REST request to http://localhost:8080/api/fibonacci/40. I tried to compute the 40th Fibonacci on my own laptop and here are the results:

  • First REST call: 1902ms
  • Second REST call: 1ms

Pretty good result eyyy🤙😎

One last thing I’d like to mention: in order to activate Spring’s cacheable feature, you have to add @EnableCaching to a @Configuration class.

Log REST calls with a custom Aspect

That was pretty easy right? So let’s move on to a more advanced use case: now we create a custom Aspect!

Our goal is to create a log message every time some REST method gets called. Since we might wanna add this functionality to future REST methods too, we want to generalize this task in an Aspect:

@Before("@annotation(com.example.aop.LogMethodName)")
public void logMethodName(JoinPoint joinPoint) {
  String method = joinPoint.getSignature().getName();
  String params = Arrays.toString(joinPoint.getArgs());
  System.out.println("Method [" + method + "] gets called with parameters " + params);
}
Enter fullscreen mode Exit fullscreen mode

The first line defines the Pointcut expression, and the subsequent method represents the Advice. Let’s break the two down one by one:

Pointcut:
The Pointcut expression defines the places where our Advice is inserted to. In our case, the Aspect is applied before every method with a @LogMehtodName annotation. Note that @LogMethodName is our custom annotation which we use as Pointcut marker.

Advice:
The advice method is the piece of logic that generalizes a task common to many different objects. In our case, the Advice finds the originating method’s name as well as its calling parameters and logs them to the console.

With our Aspect in place, there are three additional code lines required to get everything working:

  • First, add the marker @LogMethodName to our fibonacci() method
  • Second, we have to add @Aspect to the class containing our Aspect
  • Third, enable Spring’s Aspect scanning with @EnableAspectJAutoProxy in any @Configuration class

That’s it, we’ve implemented our own Advice!🙌 Let’s run a test! We fire a REST request to the web service to compute the 40th Fibonacci number and have a look at the console output:

Method [fibonacci] gets called with parameters [40]
Enter fullscreen mode Exit fullscreen mode

It goes without saying that such log messages will be of great help if you ever must track down bugs in your application.

Performance monitoring with AOP

In the previous example, we used a Pointcut expression of type @Before - here, the Advice runs before the actual method. Let’s switch gears and implement an @Around Pointcut. Such an Advice runs partly before the target method and partly after it.

Our goal now is to monitor the execution time of any REST call. Let’s go ahead and implement the monitoring requirement in a generalized fashion, namely an Aspect:

@Around("@annotation(com.example.aop.MonitorTime)")
public Object monitorTime(ProceedingJoinPoint joinPoint) throws Throwable {
  long startTime = System.currentTimeMillis();
  Object proceed = joinPoint.proceed();
  long duration = System.currentTimeMillis() - startTime;
  System.out.println("Execution took [" + duration + "ms]");
  return proceed;
}
Enter fullscreen mode Exit fullscreen mode

Pointcut:
Like before, we create a new custom annotation @MonitorTime for marking our Pointcuts.

Advice:
An @Around Aspect should have an argument of type ProceedingJoinPoint. This type has a proceed() method which triggers the execution of the actual target method. So in our Advice, we first query the current time in milliseconds. After the target method is executed, we measure the current time again, and from there we can compute time difference.

Let’s go ahead and mark our target method with the @MonitorTime annotation:

@MonitorTime
@LogMethodName
@Cacheable("Fibonacci")
@GetMapping(path = "/api/fibonacci/{number}")
public Long fibonacci(@PathVariable(value = "number") Long number) { ... }
Enter fullscreen mode Exit fullscreen mode

By now, our REST method has quite some Pointcut markers attached to it😉 Anyways, let’s go ahead and test our performance monitoring feature. As before, we compute the 40th Fibonacci number:

Method [fibonacci] gets called with parameters [40]
Execution took [1902ms]
Enter fullscreen mode Exit fullscreen mode

As you can see, this particular REST call took 1902ms. With this @Around Aspect in place, you’re definitely an advanced AOP programmer!💪

Retry mechanism with AOP

Distributed systems can experience concurrency issues. One such example would be when two web service instances are simultaneously trying to access the same record in a database. Oftentimes, such a lock problem can be resolved by retrying the operation. The only requirement here is that the operation is idempotent.

Let’s go ahead and create an Aspect which transparently retries an operation until it succeeds:

@Around("@annotation(com.example.aop.RetryOperation)")
public Object doIdempotentOperation(ProceedingJoinPoint joinPoint) throws Throwable {
  int numAttempts = 0;
  RuntimeException exception;
  do {
    try {
      return joinPoint.proceed();
    } catch(RuntimeException e) {
      numAttempts++;
      exception = e;
    }
  } while(numAttempts < 100);
  throw exception;
}
Enter fullscreen mode Exit fullscreen mode

Pointcut:
Our Advice runs around any method with the custom annotation @RetryOperation.

Advice:
In the try statement, we run the target method. This method might throw a RuntimeException. If this happens, we increment the numAttempts counter and simply rerun the target method. As soon as the target method succeeds, we exit the Advice.

For demonstration purposes, let’s create a REST method for storing a String. This method will fail 50% of the time:

@RetryOperation
@LogMethodName
@PostMapping(path = "/api/storeData")
public void storeData(@RequestParam(value = "data") String data) {
  if (new Random().nextBoolean()) {
    throw new RuntimeException();
  } else {
    System.out.println("Pretend everything went fine");
  }
}
Enter fullscreen mode Exit fullscreen mode

Thanks to our @RetryOperation annotation, the above method will be retried until it succeeds. Moreover, we use our @LogMethodName annotation so we can see every method invocation. Let’s go ahead and test our new REST endpoint; for this purpose we fire a REST request to localhost:8080/api/storeData?data=hello-world.

Method [storeData] gets called with parameters [hello-world]
Method [storeData] gets called with parameters [hello-world]
Method [storeData] gets called with parameters [hello-world]
Pretend everything went fine
Enter fullscreen mode Exit fullscreen mode

In the above case, the operation failed 2 times and only succeeded on the third try.

Conclusion

Congrats, you’re a professional AOP programmer now🥳🚀 You can find a fully working web service with all Aspects on my Github repo:

GitHub logo pmgysel / aop-examples

Some examples of Aspect Oriented Programming (AOP) with SpringBoot

Thanks so much for reading, please leave a comment if you have any questions or feedback!

Discussion (0)

pic
Editor guide