Spring Aspect (AOP) aims to help with separation of concerns by providing clean reusable annotation classes.
When we work with REST controllers, especially when we create APIs for internal (or external) use, we usually end up with a lot of duplicate code that is difficult to refactor out. To solve this we will utilize Spring Aspect and Aspect-Oriented Programming (AOP) in a real world example of how you may completely remove duplicate code in your REST Controllers.
You will learn to setup your own annotation for your REST controllers, with the intention of removing a lot of duplicate code such as logging, method timing, and exception handling.
Using this, you can rewrite your REST API methods from looking like this:
@GetMapping
public ResponseEntity<List<Client>> search(Client filter, Pageable pageable) {
log.info("Starting GET in /api/client");
long startTime = System.nanoTime();
try {
return CrudResource.search(filter, pageable, clientService);
} catch (Problem problem) {
throw problem;
} catch (Exception ex) {
log.error("Unhandled Exception: {}", ex.getMessage(), ex);
throw new InternalServerErrorProblem(ex);
} finally {
double timeDifferenceInMs = (System.nanoTime() - startTime) / 1000000d;
log.info("Time used in ms: {}", timeDifferenceInMs);
}
}
Into looking like this:
@RestLog(uri = "/api/client")
@GetMapping
public ResponseEntity<List<Client>> search(Client filter, Pageable pageable) {
return CrudResource.search(filter, pageable, clientService);
}
The logging in both cases would look something similar to this:
15:39:12.472 INFO [] ClientResource : Starting GET in /api/client
15:39:12.567 INFO [] ClientResource : Time used in ms: 94.5073
Perquisites
- An existing spring boot project
- A Rest Controller
- Spring Aspect dependencies
You will need to have an existing Spring Boot project up and running, along with a Rest Controller. If you are using spring-boot-starter-data-jpa
you should already have the aspect dependencies you need.
If you need to manually add it; you can add the spring-boot-starter-aop
dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>compile</scope>
</dependency>
Using Spring Aspect in Spring Boot
- Create @RestLog annotation
- Create RestLogAspect class
- Move boilerplate code into RestLogAspect
- Resulting Code
Create @RestLog annotation
Create a new java interface with the name of the annotation you intend to create, which in our case is RestLog.java
package com.ivanskodje.timehours.aop.rest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RestLog {
String uri() default "";
}
The @Target
annotation is what allows you to specify where this annotation can be used. We will only allow this to be used on methods.
String uri() default "";
is an optional parameter value we will store the URI path that is logged in the original controller methods. There are better ways of handling this, but for the sake of this tutorial I will include different ways of getting data from the annotated method.
Create RestLogAspect class
package com.ivanskodje.timehours.aop.rest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RestLogAspect {
@Around(value = "@annotation(restLogAnnotation)")
public Object restLog(ProceedingJoinPoint joinPoint, RestLog restLogAnnotation) throws Throwable {
return joinPoint.proceed();
}
}
When we add the @RestLog
annotation on a method, any calls to the method will have to go through the @Around
method in our RestLogAspect class. This is what will allow us to log before and after, as well as handling any potential exceptions.
Move boilerplate code into RestLogAspect
Looking at the original controller method, we see that the main purpose of this is to search for clients using a filter.
@GetMapping
public ResponseEntity<List<Client>> search(Client filter, Pageable pageable) {
log.info("Starting GET in /api/client");
long startTime = System.nanoTime();
try {
return CrudResource.search(filter, pageable, clientService);
} catch (Problem problem) {
throw problem;
} catch (Exception ex) {
log.error("Unhandled Exception: {}", ex.getMessage(), ex);
throw new InternalServerErrorProblem(ex);
} finally {
double timeDifferenceInMs = (System.nanoTime() - startTime) / 1000000d;
log.info("Time used in ms: {}", timeDifferenceInMs);
}
}
The only code we really care about here is return CrudResource.search(filter, pageable, clientService)
. Let us cut everything in the method, and paste it in our RestLogAspect class, with some minor modification.
@RestLog(uri = "/api/client")
@GetMapping
public ResponseEntity<List<Client>> search(Client filter, Pageable pageable) {
return CrudResource.search(filter, pageable, clientService);
}
package com.ivanskodje.timehours.aop.rest;
import com.ivanskodje.timehours.aop.problem.InternalServerErrorProblem;
import com.ivanskodje.timehours.aop.problem.Problem;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RestLogAspect {
@Around(value = "@annotation(restLogAnnotation)")
public Object restLog(ProceedingJoinPoint joinPoint, RestLog restLogAnnotation) throws Throwable {
Logger log = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
log.info("Starting GET in /api/client");
long startTime = System.nanoTime();
try {
return joinPoint.proceed();
} catch (Problem problem) {
throw problem;
} catch (Exception ex) {
log.error("Unhandled Exception: {}", ex.getMessage(), ex);
throw new InternalServerErrorProblem(ex);
} finally {
double timeDifferenceInMs = (System.nanoTime() - startTime) / 1000000d;
log.info("Time used in ms: {}", timeDifferenceInMs);
}
}
}
You can see that in place of the
we are now using return CrudResource.search(filter, pageable, clientService)
return joinPoint.proceed()
.
In order to log
as before, we are using the joinPoint
to get the callers class Logger log = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
. Without this we would be logging as the RestLogAspect
class itself, which would not be desirable.
If we now run our application, you will notice that it behaves identically as before. However, we are not yet using the uri
parameter from the RestLog annotation, and we do not know if we are using this annotation on a GetMapping, PostMapping, DeleteMapping, (...), rest method.
package com.ivanskodje.timehours.aop.rest;
import com.ivanskodje.timehours.aop.problem.InternalServerErrorProblem;
import com.ivanskodje.timehours.aop.problem.Problem;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
@Aspect
@Component
public class RestLogAspect {
@Around(value = "@annotation(restLogAnnotation)")
public Object restLog(ProceedingJoinPoint joinPoint, RestLog restLogAnnotation) throws Throwable {
long startTime = System.nanoTime();
Logger log = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
String uri = restLogAnnotation.uri();
String requestType = getRequestType(joinPoint);
log.info("Starting {} in {}", requestType, uri);
try {
return joinPoint.proceed();
} catch (Problem problem) {
throw problem;
} catch (Exception ex) {
log.error("Unhandled Exception: {}", ex.getMessage(), ex);
throw new InternalServerErrorProblem(ex);
} finally {
double timeDifferenceInMs = (System.nanoTime() - startTime) / 1000000d;
log.info("Time used in ms: {}", timeDifferenceInMs);
}
}
private String getRequestType(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
GetMapping getMapping = method.getAnnotation(GetMapping.class);
if (getMapping != null) {
return "GET";
}
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
if (deleteMapping != null) {
return "DELETE";
}
PostMapping postMapping = method.getAnnotation(PostMapping.class);
if (postMapping != null) {
return "POST";
}
PutMapping putMapping = method.getAnnotation(PutMapping.class);
if (putMapping != null) {
return "PUT";
}
PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);
if (patchMapping != null) {
return "PATCH";
}
return "N/A";
}
}
String uri = restLogAnnotation.uri()
is the straight and forward way of getting values that has been set on the annotation.
String requestType = getRequestType(joinPoint)
is using a new method that we created. From joinPoint we can check other annotations that is also set on the method we added @RestLog
on. This allows is to easily verify whether or not we have a XMapping
by attempting to get it. If it returns null, it means we don't have it on the method. Because the mapping annotations are mutually exclusive, this is a safe way of checking if we are on a for example a GET or POST method.
Resulting Code
RestLog.java
package com.ivanskodje.timehours.aop.rest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RestLog {
String uri() default "";
}
RestLogAspect.java
package com.ivanskodje.timehours.aop.rest;
import com.ivanskodje.timehours.aop.problem.InternalServerErrorProblem;
import com.ivanskodje.timehours.aop.problem.Problem;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
@Aspect
@Component
public class RestLogAspect {
@Around(value = "@annotation(restLogAnnotation)")
public Object restLog(ProceedingJoinPoint joinPoint, RestLog restLogAnnotation) throws Throwable {
long startTime = System.nanoTime();
Logger log = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
String uri = restLogAnnotation.uri();
String requestType = getRequestType(joinPoint);
log.info("Starting {} in {}", requestType, uri);
try {
return joinPoint.proceed();
} catch (Problem problem) {
throw problem;
} catch (Exception ex) {
log.error("Unhandled Exception: {}", ex.getMessage(), ex);
throw new InternalServerErrorProblem(ex);
} finally {
double timeDifferenceInMs = (System.nanoTime() - startTime) / 1000000d;
log.info("Time used in ms: {}", timeDifferenceInMs);
}
}
private String getRequestType(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
GetMapping getMapping = method.getAnnotation(GetMapping.class);
if (getMapping != null) {
return "GET";
}
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
if (deleteMapping != null) {
return "DELETE";
}
PostMapping postMapping = method.getAnnotation(PostMapping.class);
if (postMapping != null) {
return "POST";
}
PutMapping putMapping = method.getAnnotation(PutMapping.class);
if (putMapping != null) {
return "PUT";
}
PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);
if (patchMapping != null) {
return "PATCH";
}
return "N/A";
}
}