What is AOP?
AOP - Aspect Oriented Programming is a programming pattern that allows the modularization of common cross-cutting use cases. This can be done by adding behaviour to existing code without the need to modify the code itself. This allows behaviours (retrying a method) that are not part of the business logic to be added to a program without cluttering the code core to the functionality.
What is AspectJ?
AspectJ is an aspect-oriented programming extension created at PARC for the java programming language. AspectJ is an open-source project in the Eclipse Foundation and has become a widely used standard for AOP. More on AspectJ - here
What is a Join Point?
A join point is a point in the control flow of a program. Join points are well defined moments in the execution of a program like a method call, an object instantiation or variable access.
What is a Pointcut?
Pointcut allows the programmer to specify the point where the aspect code is to be executed. With point cuts we can specify join points. We give an expression to a point cut to determine whether a given join point matches.
What is an Advice?
Advices allow a programmer to specify code that can be run at any join point which matches a given pointcut. The code can be run before, after, or around a matched join point.
Now let's dive into a basic example on how to create a custom annotation to retry a method on a given exception using AspectJ. I'm here assuming that you know how to install and create a Gradle project. If not, Gradle has really good documentation on how to get started. click here
Here is my project structure:
Add the following two dependencies to our Gradle project in the build.gradle file:
implementation 'org.aspectj:aspectjrt:1.9.8'
&
implementation 'org.aspectj:aspectjweaver:1.9.8'
As I mentioned earlier we can define an advice to run a specific code at a join point. But how does it work? We use a plugin in Gradle that does post-compile-weaving. Weaving is a process in which the AspectJ weaver takes class files and input and produces class files as output. The produced output class files will have a modified code that includes the advice injected at join points which are matched by the pointcut. here
Add the AspectJ Gradle plugin id "io.freefair.aspectj.post-compile-weaving" version "6.4.1"
to your build.gradle file. For more details on the plugin check the following links:
Gradle plugin
Post compile weaving
Let's write our retry annotation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryMethod {
public int retries() default 5;
public int interval() default 2;
public int backoff() default 1;
}
@Target(ElementType.METHOD)
tells that the annotation is for methods.
@Retention(RetentionPolicy.RUNTIME)
tells that the annotation is to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
I'm taking the number of retries, time interval between each retry and the backoff rate with each failure as parameters with the annotation. This way the annotation can be configured for each method that we need to retry.
Now let's define our aspect with a pointcut and the advice:
@Aspect
public class RetryMethodAspect {
public static final Logger log = LoggerFactory.getLogger(RetryMethodAspect.class);
@Pointcut("@annotation(RetryMethod) && execution(* *(..))")
public void retriableMethod() {
}
@Around("retriableMethod()")
public Object retryMethodAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Advice called for annotation RetryMethod");
log.info(joinPoint.toString());
String methodName = ((MethodSignature) joinPoint.getSignature()).getName();
int retries = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RetryMethod.class).retries();
int interval = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RetryMethod.class).interval();
int backoff = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RetryMethod.class).backoff();
int delay = interval;
int attempts = 0;
Object proceed;
while(attempts < retries) {
log.info("{} attempt - {}",methodName,attempts);
attempts++;
try {
proceed = joinPoint.proceed();
return proceed;
}
catch (Exception exception) {
log.info("Exception at attempt - {}",attempts-1);
if(attempts >= retries) {
log.info("Max retry attempts reached. Throwing exception.");
throw exception;
}
if(exception.getClass() == BadRequestException.class) {
if(exception.getMessage() != null && exception.getMessage().contains("Bad Request Received")) {
log.info("Sleeping {} seconds before retry.",delay);
TimeUnit.SECONDS.sleep(delay);
delay = delay * backoff;
}
}
else {
throw exception;
}
}
}
throw new Exception("Failure executing method."); //ideally we never reach this line
}
}
As you can see from the above code, it has a pointcut and advice. The pointcut has an expression that defines the join point. Here we gave it as @annotation(RetryMethod) && execution(* *(..))
. This expression matches a join point where there is an annotation @annotation(RetryMethod)
and the execution(* *(..))
tells that the advice needs to be called at the execution time of the method which is annotated with @annotation(RetryMethod)
.
@Around("retriableMethod()")
tells that the advice is executed around the joint point that matched the pointcut expression.
The point in the code where we call proceed = joinPoint.proceed();
is the place where the actual method which is annotated is executed.
Let's write an example class that has a simple method that returns an integer with our retry annotation.
public class Example {
public static int count = 0;
@RetryMethod(
retries = 5,
interval = 2
)
public int retryMethod(String test) throws BadRequestException, Exception {
if(test == "retry") {
if(count < 3) {
count++;
throw new BadRequestException("Bad Request Received");
}
return 0;
}
else if(test == "fail"){
throw new Exception("Failed to execute method");
}
return 0;
}
}
Here I wrote the retryMethod
to take in a string value "retry"
and throw an exception 3 times for demo purposes. Our objective here is to retry the method when the method throws a BadRequestException
with the message "Bad Request Received"
. If you have already noticed, we can also pass the exception message as a parameter to the annotation and check for the message in exception before retrying our method.
Now let's write a simple App.java
with the main method to call our retryMethod
.
public class App {
public static final Logger log = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
try {
log.info("Calling retriable method");
Example example = new Example();
int result = example.retryMethod("retry");
// int result = example.retryMethod("fail");
// int result = example.retryMethod("pass");
log.info("Printing result - {}",result);
} catch (BadRequestException e) {
e.printStackTrace();
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
Now let's run our main method in App.java
and check whether our retry aspect is working.
[main] INFO App - Calling retriable method
[main] INFO aspect.RetryMethodAspect - Advice called for annotation RetryMethod
[main] INFO aspect.RetryMethodAspect - execution(int Example.retryMethod(String))
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 0
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 0
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 1
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 1
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 2
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 2
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 3
[main] INFO App - Printing result - 0
Process finished with exit code 0
As you can see from the above logs our annotation worked. We are retrying our method when the exception is BadRequestException
and the message is "Bad Request Received"
. Now let's try a failure scenario. Change the code having count < 3
to count < 7
and run the code. Because we mentioned that retries = 5
, the code should throw an exception after 5 attempts.
[main] INFO App - Calling retriable method
[main] INFO aspect.RetryMethodAspect - Advice called for annotation RetryMethod
[main] INFO aspect.RetryMethodAspect - execution(int Example.retryMethod(String))
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 0
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 0
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 1
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 1
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 2
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 2
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 3
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 3
[main] INFO aspect.RetryMethodAspect - Sleeping 2 seconds before retry.
[main] INFO aspect.RetryMethodAspect - retryMethod attempt - 4
[main] INFO aspect.RetryMethodAspect - Exception at attempt - 4
[main] INFO aspect.RetryMethodAspect - Max retry attempts reached. Throwing exception.
exception.BadRequestException: Bad Request Received
at Example.retryMethod_aroundBody0(Example.java:17)
at Example.retryMethod_aroundBody1$advice(Example.java:43)
at Example.retryMethod(Example.java:1)
at App.main(App.java:14)
As you can see that it threw an exception after retrying 5 times.
Link to Code: click here
If you made it this far, please show your support with reactions and don't hesitate to ask questions within comments, I'd love to answer each one of them and know your thoughts about this blog. Just FYI, This is my first blog ever :).
Top comments (1)
Thanks for the great Information. Helped me a lot :)