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:
- What is an Aspect?
-
@Cacheable
: a standard Spring Advice - Log REST calls (with a custom Aspect)
- Performance monitoring (with AOP)
- Retry mechanism (with AOP)
If you’re the person who wants to skip lengthy descriptions and just look at concrete code, I’ve got you covered:
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:
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);
}
}
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);
}
}
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) { ... }
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);
}
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 ourfibonacci()
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]
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;
}
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) { ... }
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]
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;
}
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");
}
}
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
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:
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!
Top comments (0)