Spring AOP Recipies

Overview

Spring AOP is a power tool to help you separate the cross-cutting concern from your main logic. This post provides some recipes to help you get started quickly with AOP.

The annotations

Here is a quick introduction to the annotations you can use in AOP:

AnnotationDescription
@BeforeExecutes before the advised method is invoked. Useful for tasks like logging, security checks, or initialization. It cannot prevent the method call or alter the input arguments.
@AfterThe most comprehensive advice type, @Around wraps the advised method, allowing execution before and after the method call. It can modify input arguments, control whether the method executes, alter the return value, and handle exceptions. It is the most powerful but also the most complex.
@AfterReturningExecutes after the advised method completes successfully (i.e., without throwing an exception). It can access and modify the return value of the advised method, making it useful for post-processing return values or logging.
@AfterThrowingExecutes if the advised method throws an exception. It allows access to the exception, enabling logging or custom exception handling. It does not execute if the method completes successfully.
@AroundThe most comprehensive advice type, @Around wraps the advised method, allowing execution before and after the method call. It has the capability to modify input arguments, control whether the method executes, alter the return value, and handle exceptions. It is the most powerful but also the most complex.
The introduction to spring AOP annotations

@Before and @AfterReturning

These are the two annotations I’m going to show you today. You can play with the rest to explore further.

Business requirements

Let’s say I have an application that transforms Dogs into SuperDogs. The requirement is to log the input and/or the output on every invocation of a service class using annotations. In addition, there is some sensitive information that needs to be hidden before logging.

Data

Here is the data model of this tutorial:

public record Dog(
    String name,
    String phoneNumber,
    int age,
    int hp
) {
}

public record SuperDog (
        String name,
        String phoneNumber,
        int age,
        int hp
) {
}


package com.datmt.spring.springaoptutorial.service;

import com.datmt.spring.springaoptutorial.aspect.AfterLogging;
import com.datmt.spring.springaoptutorial.aspect.BeforeLogging;
import com.datmt.spring.springaoptutorial.model.Dog;
import com.datmt.spring.springaoptutorial.model.SuperDog;
import com.datmt.spring.springaoptutorial.transformer.HidePhoneTransformer;
import com.datmt.spring.springaoptutorial.transformer.HidePowerTransformer;
import org.springframework.stereotype.Service;

@Service
public class BuffDogService {

    public SuperDog buffDog(Dog dog) {
        return new SuperDog(dog.name(), dog.phoneNumber(), dog.age(), dog.hp() + 100);
    }

    public Dog retireDog(SuperDog dog) {
        return new Dog(dog.name(), dog.phoneNumber(), dog.age(), dog.hp() - 100);
    }

    public void listDog(Dog... dogs) {

    }
}

The listDog method in the BuffDogService doesn’t do anything. I want to avoid cluttering the log.

The hide-sensitive-info transformers

Let’s say I must hide the dog’s phone and its real HP. Let’s create the transformers for that:

package com.datmt.spring.springaoptutorial.transformer;

@FunctionalInterface
public interface Transformer<T> {
    T transform(T t);
}


public class HidePhoneTransformer implements Transformer<Dog> {
    @Override
    public Dog transform(Dog dog) {
        return new Dog(dog.name(), "**********", dog.age(), dog.hp());
    }
}

public class HidePowerTransformer implements Transformer<Dog> {
    @Override
    public Dog transform(Dog dog) {
        return new Dog(dog.name(), dog.phoneNumber(), dog.age(), 0);
    }
}

public class SuperDogHidePhoneTransformer implements Transformer<SuperDog> {
    @Override
    public SuperDog transform(SuperDog dog) {
        return new SuperDog(dog.name(), "**********", dog.age(), dog.hp());
    }
}

Here I create two transformers. In reality, one transformer is enough. However, for the sake of demonstration, let’s create two.

Now, how can I use these transformers?

Let’s create an aspect.

Create a logging aspect

First, let’s create the annotations:

import com.datmt.spring.springaoptutorial.transformer.Transformer;

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

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

public @interface BeforeLogging {
    Class<? extends Transformer>[] value() default {};
}
package com.datmt.spring.springaoptutorial.aspect;

import com.datmt.spring.springaoptutorial.transformer.Transformer;

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

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

public @interface AfterLogging {
    Class<? extends Transformer>[] value() default {};
}

The @BeforeLogging is responsible for logging the method’s input. The @AfterLogging is responsible for logging the method’s output.

Now let’s create the aspect:

The aspect is annotated with @Aspect and @Component (so spring will manage this). If you forget the @Component, you will not see your annotation working.

package com.datmt.spring.springaoptutorial.aspect;

import com.datmt.spring.springaoptutorial.transformer.Transformer;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@Aspect
public class LoggingAspect {

    private final Logger logger = org.apache.logging.log4j.LogManager.getLogger(LoggingAspect.class);

    @Before("@annotation(com.datmt.spring.springaoptutorial.aspect.BeforeLogging)")
    public void logBefore(JoinPoint joinPoint) {
        //get args
        Object[] args = joinPoint.getArgs();
        var signature = (MethodSignature) joinPoint.getSignature();
        //get annotation
        var beforeLogging = signature.getMethod().getAnnotation(BeforeLogging.class);

        //get transformer
        var transformers = beforeLogging.value();

        //create the list of transformers
        var transformerList = getTransformers(transformers);

        List<Object> transformedObjects = transformObject(transformerList, args);

        var invokeClass = joinPoint.getTarget().getClass();
        //create the logger for the class
        Logger logger = org.apache.logging.log4j.LogManager.getLogger(invokeClass);
        //log
        logger.info("Before logging {}", transformedObjects);
        transformedObjects.clear();
        transformerList.clear();
    }

    @AfterReturning(value = "@annotation(com.datmt.spring.springaoptutorial.aspect.AfterLogging)", returning = "returnValue")
    public void logAfter(JoinPoint joinPoint, Object returnValue) {
        var signature = (MethodSignature) joinPoint.getSignature();
        //get annotation
        var afterLogging = signature.getMethod().getAnnotation(AfterLogging.class);

        //get transformer
        var transformers = afterLogging.value();

        //create the list of transformers
        var transformerList = getTransformers(transformers);

        //transform the return value
        try {
            for (Transformer<Object> transformer : transformerList) {
                returnValue = transformer.transform(returnValue);
            }
            logger.info("After logging: {}", returnValue);
        } catch (Exception e) {
            logger.debug("Error while transforming object due to class cast");
        }
    }

    private List<Object> transformObject(ArrayList<Transformer<Object>> transformerList, Object[] args) {
        List<Object> transformedObjects = new ArrayList<>();
        //transform args
        for (Object arg : args) {
            //get the type of the object and the type of the transformer, if they match, transform
            try {
                for (Transformer<Object> transformer : transformerList) {
                    arg = transformer.transform(arg);
                }
            } catch (Exception e) {
                logger.debug("Error while transforming object due to class cast");
            }
            transformedObjects.add(arg);
        }

        return transformedObjects;
    }

    private ArrayList<Transformer<Object>> getTransformers(Class<? extends Transformer>[] transformers) {
        var transformerList = new ArrayList<Transformer<Object>>();
        for (var transformer : transformers) {
            try {
                transformerList.add(transformer.getDeclaredConstructor().newInstance());
            } catch (Exception e) {
                logger.debug("Error while creating transformer");
            }
        }
        return transformerList;
    }
}

It’s quite a big class so let me explain.

Starting from line 21, I define the pointcut. Here, my expression says invoke this advice on methods that have this annotation (BeforeLogging).

On line 27, I get the annotation. This is needed to get the list of transformers.

Then, I create the list of transformer instances based on the list of transformers (line 33).

After that, on line 35, I apply the transformers to the args.

The rest is the logging logic.

You can reason the same with the @AfterReturning annotation.

Spring AOP in action

Now let’s go back to the BuffDogService to apply the annotation:

package com.datmt.spring.springaoptutorial.service;

import com.datmt.spring.springaoptutorial.aspect.AfterLogging;
import com.datmt.spring.springaoptutorial.aspect.BeforeLogging;
import com.datmt.spring.springaoptutorial.model.Dog;
import com.datmt.spring.springaoptutorial.model.SuperDog;
import com.datmt.spring.springaoptutorial.transformer.HidePhoneTransformer;
import com.datmt.spring.springaoptutorial.transformer.HidePowerTransformer;
import com.datmt.spring.springaoptutorial.transformer.SuperDogHidePhoneTransformer;
import org.springframework.stereotype.Service;

@Service
public class BuffDogService {

    @BeforeLogging({HidePhoneTransformer.class, HidePowerTransformer.class})
    @AfterLogging({SuperDogHidePhoneTransformer.class})
    public SuperDog buffDog(Dog dog) {
        return new SuperDog(dog.name(), dog.phoneNumber(), dog.age(), dog.hp() + 100);
    }

    @BeforeLogging({HidePhoneTransformer.class, HidePowerTransformer.class})
    public Dog retireDog(SuperDog dog) {
        return new Dog(dog.name(), dog.phoneNumber(), dog.age(), dog.hp() - 100);
    }

    @BeforeLogging
    public void listDog(Dog... dogs) {

    }
}

Then, let’s run the application:

package com.datmt.spring.springaoptutorial;

import com.datmt.spring.springaoptutorial.model.Dog;
import com.datmt.spring.springaoptutorial.model.SuperDog;
import com.datmt.spring.springaoptutorial.service.BuffDogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringAopTutorialApplication implements CommandLineRunner {

    @Autowired
    private BuffDogService buffDogService;

    private final Logger logger = LoggerFactory.getLogger(SpringAopTutorialApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(SpringAopTutorialApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        Dog dog = new Dog("Super Dog", "0192392323", 1, 100);
        var superDog = buffDogService.buffDog(dog);

        logger.info("Super dog: {}", superDog);
        var normalDog = buffDogService.retireDog(superDog);

        buffDogService.listDog(dog, dog, dog);
    }
}
Spring AOP logging in action

Caution

With the @AfterReturning annotation, you have access to the return value. Be very careful when you modify the return value here. You can unintentionally alter the details of the return value (for logging purposes, for example) that in turn, alter the logic of your application.

Let’s say the SuperDog is now a class and the transformer, instead of creating a new instance, modifies the value directly:

public class SuperDogHidePhoneTransformer implements Transformer<SuperDog> {
    @Override
    public SuperDog transform(SuperDog dog) {
        dog.setPhoneNumber("**********");
        return dog;
    }
}

Then, the return value of the logic method is altered:

Conclusion

In this post, I’ve shown you how to use spring AOP to separate the logging (with transformers) from your applications’ logic.

The source code is available on GitHub

Leave a Comment