Spring AOP in conjunction with custom annotations

Published by Alexander Braun on 13 Nov 2017 - tagged with Spring, Spring Boot, Java, Thymeleaf, AOP

In one of my recent projects, I had the task to clean up existing Java classes regarding a typical cross-cutting concern problem. Access to certain methods within a web application should be logged, basically which user invoked which method using what kind of parameters. This is a situation when Spring AOP with custom annotations can provide a clean solution. In the following post, I will walk you through the entire process using a simplified solution.

Introduction

The solution I'm going to explain in this post covers the following requirements:

  • Log the following information for selected method invocations (username, class name, method name, parameter names and values)
  • Log the method invocation result - number of objects and the basic information about the object as printed out by the toString() method
  • The solution has to be flexible, the logic should only be applied to selected methods
  • Not all invocation parameters should be logged, only selected ones
  • The business logic of the service classes should not be polluted, the logging logic has to be separate
  • The logging specific code might be changed, therefore it should be maintained in one place only

Pre-Requisition

This code assumes you already have a basic, secured Spring Boot web application. Probably the easiest way is to go through the following post and download the sample application from GitHub repository.

The final code for this post is available in this GitHub repository.

Enhance the basic Spring Boot application

First we will enhance the existing Spring Boot web application and add the following business functionality. We will simulate a simple customer lookup service. The following two operations are offered:

  • findOne: Find an existing customer by its unique id
  • findByFirstNameAndLastName: Find a list of existing customers by first and last name

As we will focus on Spring AOP in this post, these lookup methods are mocked and we always return a default customer.

As mentioned in the requirements section: All of these methods can only be accessed by an authenticated user. Any time one of the methods above is being invoked the application should log the user that tried to access the data as specified above.

Create Customer class

First we add a new package com.progressive.code.starter.domain and add the Customer POJO.

Customer class

package com.progressive.code.starter.domain;

public class Customer {

	private Long id;
	private String firstName;
	private String lastName;

	public Customer(String firstName, String lastName) {
		super();
		this.firstName = firstName;
		this.lastName = lastName;
	}

	public Customer() {
		super();
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	@Override
	public String toString() {
		return id + " " + firstName + " " + lastName;
	}

}

Create Customer Service

In the next step, we create the com.progressive.code.starter.service and the Customer Service interface and implementation

CustomerService interface

package com.progressive.code.starter.service;

import java.util.List;

import com.progressive.code.starter.domain.Customer;

public interface CustomerService {

	Customer findOne(Long id);

	List<Customer> findByFirstNameAndLastName(String firstName, String lastName);

}

CustomerServiceImpl implementation

package com.progressive.code.starter.service;

import java.util.Arrays;
import java.util.List;

import org.springframework.stereotype.Service;

import com.progressive.code.starter.domain.Customer;

@Service
public class CustomerServiceImpl implements CustomerService {

	@Override
	public Customer findOne(Long id) {
		return getDefaultCustomer();
	}

	@Override
	public List<Customer> findByFirstNameAndLastName(String firstName, String lastName) {
		return Arrays.asList(getDefaultCustomer());
	}

	private Customer getDefaultCustomer() {
		Customer customer = new Customer("First", "Last");
		customer.setId(1L);
		return customer;
	}

}

Customer Controller

Then we can create the CustomerController class within the com.progressive.code.starter.admin.controller package.

CustomerController

package com.progressive.code.starter.admin.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.progressive.code.starter.service.CustomerService;

@Controller
public class CustomerController {

	@Autowired
	private CustomerService customerService;

	@RequestMapping(value="/admin/customer/{id}", method=RequestMethod.GET)
	public String findOne(Model model, @PathVariable("id") Long id) {
		model.addAttribute("customer",customerService.findOne(id));
		return "admin/customer";
	}

	@RequestMapping(value="/admin/customer/{firstName}/{lastName}", method=RequestMethod.GET)
	public String findByFirstNameAndLastName(Model model, @PathVariable("firstName") String firstName,
			@PathVariable("lastName") String lastName) {
		model.addAttribute("customerList", customerService.findByFirstNameAndLastName(firstName, lastName));
		return "admin/customerList";
	}

}

customer and cutomerList HTML pages

The last step is to add the HTML pages referenced by the CustomerController. As these pages are only available form authenticated users the HTML files have to be stored in the templates/admin folder.

admin/customer.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org"
  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Customer</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport"
  content="width=device-width, initial-scale=1, shrink-to-fit=no" />

<link rel="stylesheet" type="text/css" media="all"
  href="../../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />

</head>
<body>
  <div class="container">
    <div class="row">

      <h2>Customer details</h2>

      <ul>
        <li>First Name: <strong th:text="${customer.firstName}"></strong></li>
        <li>Last Name: <strong th:text="${customer.lastName}"></strong></li>
      </ul>

    </div>
  </div>

  <script type="text/javascript" th:src="@{/js/jquery.min.js}"
    src="js/jquery.min.js"></script>
  <script type="text/javascript"
    th:src="@{/js/jquery.serialize-object.min.js}"
    src="js/jquery.serialize-object.min.js"></script>
  <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"
    src="js/bootstrap.min.js"></script>


</body>
</html>

admin/customerList.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org"
  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Customer list</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport"
  content="width=device-width, initial-scale=1, shrink-to-fit=no" />

<link rel="stylesheet" type="text/css" media="all"
  href="../../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />

</head>
<body>
  <div class="container">
    <div class="row">

      <h2>Customer list</h2>

      <table class="table">
        <thead>
          <tr>
            <th scope="col">Id</th>
            <th scope="col">First Name</th>
            <th scope="col">Last Name</th>
          </tr>
        </thead>
        <tbody>
          <tr th:each="customer : ${customerList}">
            <th th:text="${customer.id}">1</th>
            <td th:text="${customer.firstName}"></td>
            <td th:text="${customer.lastName}"></td>
          </tr>
        </tbody>
      </table>

    </div>
  </div>

  <script type="text/javascript" th:src="@{/js/jquery.min.js}"
    src="js/jquery.min.js"></script>
  <script type="text/javascript"
    th:src="@{/js/jquery.serialize-object.min.js}"
    src="js/jquery.serialize-object.min.js"></script>
  <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"
    src="js/bootstrap.min.js"></script>


</body>
</html>

Test the current state of the application

After implementing the changes we can test the application using the following URLs:

  • customer: http://localhost:8090/admin/customer/1
  • customerList: http://localhost:8090/admin/customer/First/Last

Result: we can invoke the endpoints to see the individual customer or the list of customers. A user has to login to invoke the functionality. In the next section, we will add a simple solution to log data access.

Log data access - non-AOP solution

the next step is to create a service to log our requests, in reality, this service might store the requests in a database (MySQL, Oracle, Cassandra or MongoDB). To keep the example simple we log the data into the file system.

Create the AccessLog domain object, the AccessLoggerService interface, and the corresponding service

AccessLog domain object

package com.progressive.code.starter.domain;

public class AccessLog {

	private String username;
	private String clazz;
	private String method;
	private String paramters;

	public AccessLog(String username, String clazz, String method, String paramters) {
		super();
		this.clazz = clazz;
		this.username = username;
		this.method = method;
		this.paramters = paramters;
	}

	public AccessLog() {
		super();
	}

	public String getUsername() {
		return username;
	}

	public String getClazz() {
		return clazz;
	}

	public void setClazz(String clazz) {
		this.clazz = clazz;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getMethod() {
		return method;
	}

	public void setMethod(String method) {
		this.method = method;
	}

	public String getParamters() {
		return paramters;
	}

	public void setParamters(String paramters) {
		this.paramters = paramters;
	}

}

Interface AccessLoggerService

package com.progressive.code.starter.service;

import com.progressive.code.starter.domain.AccessLog;

public interface AccessLoggerService {

	void logAccess(AccessLog accessLog);

}

Implementation AccessLoggerServiceImpl

package com.progressive.code.starter.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.progressive.code.starter.domain.AccessLog;

@Service
public class AccessLoggerServiceImpl implements AccessLoggerService {

	private static final Logger LOGGER = LoggerFactory.getLogger(AccessLoggerServiceImpl.class);

	@Override
	public void logAccess(AccessLog accessLog) {
		LOGGER.info("User {}, invoked method {} of class {}, with parameters {}", accessLog.getUsername(),
				accessLog.getMethod(), accessLog.getClazz(), accessLog.getParamters());
	}

}

Additionally, we will modify our CustomerService to create an AccessLog object and invoke the AccessLoggerService. This example shows a naive approach without using any of Spring's AOP features. We basically add the logic to create the AccessLog object and the service invocation to each method that requires tracing data access. The service is being injected using Spring's @Autowired annotation.

We also add a method to retrieve the username from the SecurityContext.

CustomerServiceImpl

package com.progressive.code.starter.service;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import com.progressive.code.starter.domain.AccessLog;
import com.progressive.code.starter.domain.Customer;

@Service
public class CustomerServiceImpl implements CustomerService {

	@Autowired
	private AccessLoggerService accessLoggerService;

	@Override
	public Customer findOne(Long id) {

		accessLoggerService.logAccess(new AccessLog(getUserName(), this.getClass().getName(), "findOne", "id=" + id));

		return getDefaultCustomer();
	}

	@Override
	public List<Customer> findByFirstNameAndLastName(String firstName, String lastName) {

		accessLoggerService.logAccess(
				new AccessLog(getUserName(), this.getClass().getName(), "findByFirstNameAndLastName",
						"firstName=" + firstName + " lastName=" + lastName));

		return Arrays.asList(getDefaultCustomer());
	}

	private Customer getDefaultCustomer() {
		Customer customer = new Customer("First", "Last");
		customer.setId(1L);
		return customer;
	}

	private String getUserName() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null) {
		    return authentication.getName();
		}
		return null;
	}

}

Great, now we can see the following log entries when invoking the endpoints:

  • http://localhost:8090/admin/customer/1
  • http://localhost:8090/admin/customer/First/Last
User user, invoked method findOne of class com.progressive.code.starter.service.CustomerServiceImpl, with parameters id=1
User user, invoked method findByFirstNameAndLastName of class com.progressive.code.starter.service.CustomerServiceImpl, with parameters firstName=First lastName=Last

The problem with this approach is, that we duplicate the code for creating the AccessLog object and invoking the AccessLoggerService in each and every method that requires logging. This is a typical example of a cross-cutting concern and it is a good idea to separate these repeating, non-business logic related parts of the code from the methods that implement the actual business logic.

Log data access - Spring AOP solution

In this section, we will see how an Aspect Oriented Programming (AOP) approach can help to improve the handling of these cross-cutting concerns.

To add some flexibility to the new implementation I will use custom annotations. One annotation on the method level will be used to specify which method invocations should be logged. Another annotation on the parameter level will be used to decide which of the method parameters should be logged.

Let's start with creating a method level annotation.

First we create a new package named com.progressive.code.starter.annotation. We create a new annotation named LogAccess.

LogAccess annotation

package com.progressive.code.starter.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAccess {

}

Then we create a second annotation on parameter level named LogValue. Here we need a parameter value() as we want to store the parameter name within the annotated method - which is not available at runtime.

LogValue annotation

package com.progressive.code.starter.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogValue {

	String value();

}

We also have to modify the pom.xml file to include the dependency to spring-boot-starter-aop which adds the AOP functionality.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.progressive.code</groupId>
  <artifactId>SpringAOPExample</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>SpringAOPExample</name>
  <description>Example to demonstrate Spring AOP within a web application</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>


</project>

After that we create a new package com.progressive.code.starter.aspect and add a class named AccessLoggerAspect. This class implements our cross-cutting concern. We intercept method invocations for methods that are annotated with the LogAccess annotation.

AccessLoggerAspect

package com.progressive.code.starter.aspect;

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 AccessLoggerAspect {

	private static final Logger LOGGER = LoggerFactory.getLogger(AccessLoggerAspect.class);

	@Around("@annotation(com.progressive.code.starter.annotation.LogAccess)")
    public Object logDataAccess(ProceedingJoinPoint joinPoint) throws Throwable {

        LOGGER.info("Before invoking the target");

        Object invocationResult = joinPoint.proceed();

        LOGGER.info("After invoking the target");

        return invocationResult;
	}
}

This is a simplified version, we only create a log entry before and after invocating the target method in the CustomerServiceImpl class.

Note, you have to use the full package name (com.progressive.code.starter.annotation.LogAccess) if the annotation is in a different package than the aspect.

To activate the aspect we have to add the annotation LogAccess to the CustomerServiceImpl methods as shown below

CustomerServiceImpl

package com.progressive.code.starter.service;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import com.progressive.code.starter.annotation.LogAccess;
import com.progressive.code.starter.domain.AccessLog;
import com.progressive.code.starter.domain.Customer;

@Service
public class CustomerServiceImpl implements CustomerService {

	@Autowired
	private AccessLoggerService accessLoggerService;

	@Override
	@LogAccess
	public Customer findOne(Long id) {

		accessLoggerService.logAccess(new AccessLog(getUserName(), this.getClass().getName(), "findOne", "id=" + id));

		return getDefaultCustomer();
	}

	@Override
	@LogAccess
	public List<Customer> findByFirstNameAndLastName(String firstName, String lastName) {

		accessLoggerService.logAccess(
				new AccessLog(getUserName(), this.getClass().getName(), "findByFirstNameAndLastName",
						"firstName=" + firstName + " lastName=" + lastName));

		return Arrays.asList(getDefaultCustomer());
	}

	private Customer getDefaultCustomer() {
		Customer customer = new Customer("First", "Last");
		customer.setId(1L);
		return customer;
	}

	private String getUserName() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null) {
		    return authentication.getName();
		}
		return null;
	}

}

Test the current state of the application

When testing the endpoints, e.g. http://localhost:8090/admin/customer/First/Last you will see that the following log will be created.

Before invoking the target
User user, invoked method findByFirstNameAndLastName of class com.progressive.code.starter.service.CustomerServiceImpl, with parameters firstName=First lastName=Last
After invoking the target

We can see that the annotated methods are now intercepted and the two additional log messages (Before... and After...) show up. This only proves that the first annotation we placed on the method works as expected.

Now we can add the missing pieces. We add the LogValue annotation to all parameters we want to log.

Here is the new version of our CustomerServiceImpl class. In this example, we want to log the id of the findOne method and the first and last name of the findByFirstNameAndLastName method. For each of those parameters, we have to add the LogValue annotation.

Additionally, we can clean up the class by removing the getUserName method and the accessLoggerService invocation. We now have a class that only focusses on actual business logic. The code related to our cross-cutting concern has been moved to the Aspect class. The only indicators left are the two annotations used to mark a method to be logged when accessed and which parameters of the method signature should be logged.

CustomerServiceImpl

package com.progressive.code.starter.service;

import java.util.Arrays;
import java.util.List;

import org.springframework.stereotype.Service;

import com.progressive.code.starter.annotation.LogAccess;
import com.progressive.code.starter.annotation.LogValue;
import com.progressive.code.starter.domain.Customer;

@Service
public class CustomerServiceImpl implements CustomerService {

	@Override
	@LogAccess
	public Customer findOne(@LogValue("id") Long id) {
		return getDefaultCustomer();
	}

	@Override
	@LogAccess
	public List<Customer> findByFirstNameAndLastName(@LogValue("firstName") String firstName, @LogValue("lastName") String lastName) {
		return Arrays.asList(getDefaultCustomer());
	}

	private Customer getDefaultCustomer() {
		Customer customer = new Customer("First", "Last");
		customer.setId(1L);
		return customer;
	}

}

Of course the functionality we removed from the CustomerServiceImpl class has now to be moved into the AccessLoggerAspect class. Below is the updated implementation.

AccessLoggerAspect

package com.progressive.code.starter.aspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

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.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import com.progressive.code.starter.annotation.LogValue;
import com.progressive.code.starter.domain.AccessLog;
import com.progressive.code.starter.service.AccessLoggerService;

@Aspect
@Component
public class AccessLoggerAspect {

	@Autowired
	private AccessLoggerService accessLoggerService;

	@SuppressWarnings("unchecked")
	@Around("@annotation(com.progressive.code.starter.annotation.LogAccess)")
    public Object logDataAccess(ProceedingJoinPoint joinPoint) throws Throwable {

        //First we need the method signature of the target class (name and paramter types)
        MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();

        //We need to know what the final target class (here CustomerServiceImpl) is
        Object target = joinPoint.getTarget();
        @SuppressWarnings("rawtypes")
		Class targetClass = AopProxyUtils.ultimateTargetClass(target);

        //Then we need the method to be invoked on the target class
        Method method = targetClass.getDeclaredMethod(methodSig.getName(), methodSig.getParameterTypes());

        StringBuilder parameterAndValue = new StringBuilder();

        //Additionally, we want to get all arguments that have been passed to the joinPoint
        Object[] args = joinPoint.getArgs();

        //And we need all annotations for all parameters of the target method, this is
        //a two dimensional array as multiple parameters can have multiple annotations
        Annotation[][] annotations = method.getParameterAnnotations();

        //Finally we can iterate through all annotations and check if we find
        //a LogValue annotation. If we find such an annotation we will get
        //the parameter name from the LogValue annotation and the corresponding
        //value from the args array
        for(int i = 0; i < annotations.length; i++) {
            for (Annotation annotation : annotations[i]) {
                if (annotation instanceof LogValue) {
                    LogValue logValue = (LogValue) annotation;
                    if (parameterAndValue.length() > 0) {
                    	parameterAndValue.append(",");
                    }
                    parameterAndValue.append(logValue.value()).append("=").append(args[i].toString());
                }
            }
        }

        //Now we can invoke the AccessLoggerService to log the data access
        accessLoggerService.logAccess(new AccessLog(getUserName(), targetClass.getName(), method.getName(), parameterAndValue.toString()));

        Object invocationResult = joinPoint.proceed();

        return invocationResult;
	}

	private String getUserName() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null) {
		    return authentication.getName();
		}
		return null;
	}

}

Let's quickly walk through the main aspects of the new implementation (see also comments in the source code).

  • We first need to understand which method should be invoked. This information is provided by the ProceedingJoinPoint getSignature method
  • Then we have to figure out which ultimate target class is being invoked (here CustomerServiceImpl). This is required as the AOP proxy, doesn't inherit the parameter based annotations (@LogValue). We need to get a method reference of the target class to get this information
  • We need to get all parameter values in case we have to log the corresponding value (args[] array)
  • Based on the target method we will retrieve a 2-dimensional array of all annotations. The array is 2-dimensional as each parameter can have multiple annotations (Annotation[][] annotations)
  • We iterate over the 2-dimensional annotation array and check for each parameter if an annotation of type LogValue exists. If we find such an annotation we add the parameter name (as specified in the LogValue annotation) and the value from the args[] array.
  • Finally, we can create the AccessLog object and invoke the AccessLoggerService

Test the current state of the application

Again, we can restart the application and check what happens if we invoke the endpoints, e.g. http://localhost:8090/admin/customer/First/Last you will see that the following log will be created.

Before invoking the target
User user, invoked method findByFirstNameAndLastName of class com.progressive.code.starter.service.CustomerServiceImpl, with parameters firstName=First lastName=Last
After invoking the target

As you see the same log message is being created. But this time the code is not being executed in the service layer, but within our new Aspect!

Log the result

The final step we want to implement is to log the result of the invocation. Here we only support two different return types:

  • For a collection, we log a number of elements, iterate through the result list and call the toString method for each object
  • for other objects, we simply perform a null check and call the toString operation if the object is not null

A simple solution is shown below. We can create a new method named logInvocationResult and pass in the invocation result, we received after the target method has completed.

AccessLoggerAspect

package com.progressive.code.starter.aspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;

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.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import com.progressive.code.starter.annotation.LogValue;
import com.progressive.code.starter.domain.AccessLog;
import com.progressive.code.starter.service.AccessLoggerService;

@Aspect
@Component
public class AccessLoggerAspect {

	private static final Logger LOGGER = LoggerFactory.getLogger(AccessLoggerAspect.class);


	@Autowired
	private AccessLoggerService accessLoggerService;

	@SuppressWarnings("unchecked")
	@Around("@annotation(com.progressive.code.starter.annotation.LogAccess)")
    public Object logDataAccess(ProceedingJoinPoint joinPoint) throws Throwable {

        //First we need the method signature of the target class (name and paramter types)
        MethodSignature methodSig = (MethodSignature) joinPoint.getSignature();

        //We need to know what the final target class (here CustomerServiceImpl) is
        Object target = joinPoint.getTarget();
        @SuppressWarnings("rawtypes")
		Class targetClass = AopProxyUtils.ultimateTargetClass(target);

        //Then we need the method to be invoked on the target class
        Method method = targetClass.getDeclaredMethod(methodSig.getName(), methodSig.getParameterTypes());

        StringBuilder parameterAndValue = new StringBuilder();

        //Additionally, we want to get all arguments that have been passed to the joinPoint
        Object[] args = joinPoint.getArgs();

        //And we need all annotations for all parameters of the target method, this is
        //a two dimensional array as multiple parameters can have multiple annotations
        Annotation[][] annotations = method.getParameterAnnotations();

        //Finally we can iterate through all annotations and check if we find
        //a LogValue annotation. If we find such an annotation we will get
        //the parameter name from the LogValue annotation and the corresponding
        //value from the args array
        for(int i = 0; i < annotations.length; i++) {
            for (Annotation annotation : annotations[i]) {
                if (annotation instanceof LogValue) {
                    LogValue logValue = (LogValue) annotation;
                    if (parameterAndValue.length() > 0) {
                    	parameterAndValue.append(",");
                    }
                    parameterAndValue.append(logValue.value()).append("=").append(args[i].toString());
                }
            }
        }

        //Now we can invoke the AccessLoggerService to log the data access
        accessLoggerService.logAccess(new AccessLog(getUserName(), targetClass.getName(), method.getName(), parameterAndValue.toString()));

        Object invocationResult = joinPoint.proceed();

        logInvocationResult(invocationResult);

        return invocationResult;
	}


	@SuppressWarnings("unchecked")
	private void logInvocationResult(Object invocationResult) {
		if (invocationResult != null) {
        	if (invocationResult instanceof Collection) {
        		@SuppressWarnings("rawtypes")
				Collection col = (Collection) invocationResult;
        		LOGGER.info("Collection with {} elements returned", col.size());
        		col.forEach(obj -> {
        			LOGGER.info(obj.toString());
        		});
        	} else {
        		LOGGER.info("Result: {}", invocationResult.toString());
        	}
        } else {
        	LOGGER.info("No result!");
        }
	}


	private String getUserName() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null) {
		    return authentication.getName();
		}
		return null;
	}

}

Test the final state of the application

Let's restart the application one more time and test the endpoints, using http://localhost:8090/admin/customer/First/Last you will see that the following log will be created.

User user, invoked method findByFirstNameAndLastName of class com.progressive.code.starter.service.CustomerServiceImpl, with parameters firstName=First,lastName=Last
Collection with 1 elements returned
1 First Last

This concludes today's posting about Spring AOP. All requirements specified in the first section are fulfilled.

Get the Code on GitHhub

The entire code is available in this GitHub repository.