Table of Contents
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:
Annotation | Description |
---|---|
@Before | Executes 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. |
@After | The 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. |
@AfterReturning | Executes 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. |
@AfterThrowing | Executes 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. |
@Around | The 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. |
@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); } }
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
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.