Using Spring Aspect in Spring Boot

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

  1. Create @RestLog annotation
  2. Create RestLogAspect class
  3. Move boilerplate code into RestLogAspect
  4. 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 return CrudResource.search(filter, pageable, clientService) we are now using 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";
    }
}
package com.ivanskodje.timehours.api.crud;

import com.ivanskodje.timehours.aop.rest.RestLog;
import com.ivanskodje.timehours.api.crud.helper.CrudResource;
import com.ivanskodje.timehours.api.service.ClientService;
import com.ivanskodje.timehours.dal.client.Client;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/client")
public class ClientResource {

    private final ClientService clientService;

    public ClientResource(ClientService clientService) {
        this.clientService = clientService;
    }

    @RestLog(uri = "/api/client")
    @GetMapping
    public ResponseEntity<List<Client>> search(Client filter, Pageable pageable) {
        return CrudResource.search(filter, pageable, clientService);
    }

    @RestLog(uri = "/api/client/{id}")
    @GetMapping(value = "/{id}")
    public ResponseEntity<Client> get(@PathVariable("id") Long id) {
        return CrudResource.get(id, clientService);
    }

    // omitted methods
    // ...
}
The REST Controller