All you need to know about Spring Apect Oriented Programming (AOP)
Imagine you have a toy robot that can do many things: it can walk, talk, and even dance. But now, you want to teach your robot a new trick: every time it does something, it should also say, “I am awesome!” before and after. Instead of changing the walking, talking, and dancing instructions one by one, you add a special instruction that automatically makes the robot say, “I am awesome!” before and after any action it performs.
This is similar to what Aspect Oriented Programming (AOP) does in Spring.
What is Aspect Oriented Programming (AOP)?
AOP is a way to add extra behaviors to parts of your program without changing the actual code for those parts. For example, the extra behavior could be logging every request that comes into the methods in your controller layer. Traditionally, you would go into the the methods in the controller layer to put your logger, thereby changing the actual code:
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public Product createProduct(@RequestBody Product product) {
log.info("createProduct - Request received with parameters {}", product);
return productService.createProduct(product);
}
@GetMapping(value = "/{productId}", produces = MediaType.APPLICATION_JSON_VALUE)
public Product getProduct(@PathVariable int productId) {
log.info("getProduct - Request received with parameters {}", productId);
return productService.getProduct(productId);
}
With AOP, you would be able to add a log statement without changing the actual code. It does this by intercepting the original method invocation and does whatever you specify before and after the method. We will get into more details on how to use AOP to make the code above better.
Major Concepts of AOP
- Aspect: This is like a special instruction set/class you want to add. The instruction set could be for logging (as shown above) or for Authentication which helps to validate a user for every/some methods. In Spring, you use @Aspect annotation to identify an Aspect.
- Join Point: These are points in your program where you can apply the aspect. For the code above, this would be the createProduct and getProduct.
- Advice: This is the action of the Aspect at a particular Join Point. In the code snippet above, the Advice would be the actual log action expected. The different types of Advice can be found in the table below:
- Pointcut: This is a way to choose specific join points where the Aspect should be applied. It is a predicate that matches join points.
How AOP is Used?
Using the Logging use case stated earlier in this article, let’s look at how to use Spring AOP to implement logging for every request that goes through the controller.
@Slf4j
@Aspect
@Component
public class AspectLogger {
@Pointcut("within(dev.crackers.snippets.aop.ProductController)")
private void anyControllerRequest(){}
@Before(value = "anyControllerRequest()")
private void logRequest(JoinPoint joinpoint) {
log.info("{} - Request received with parameters {}",
joinpoint.getSignature().getName(), Arrays.toString(joinpoint.getArgs()));
}
}
- First we have to declare that our class is an Aspect by using @Aspect. this enables Spring to detect that this class has a set of instructions that require AOP. Note that the Aspect class has to be marked with @Component
- We define the Point Cut which is basically the point or the location where you want this action to take place. In this case, this would be every method within the
ProductController
class. within is a pointcut designator. - We declare the Advice and what it’s meant to do. In our context, we use the @Before Advice to instruct to log before any method within the
ProductController
class.
Other Pointcut Designators
Pointcut designators are expressions that help match join points to apply the advice. As seen above, within is a pointcut designator. In this section, we would look at other pointcut designators and how to apply them:
execution(): Matches method execution join points. This is the most commonly used pointcut designator. In the snippet below, it’s going to match every method in ProductService
@Before("execution(* dev.crackers.snippets.aop.ProductService.*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("Before method: {}", joinPoint.getSignature().getName());
}
this(): Matches join points where the bean reference is an instance of the given type (i.e. every method in ProductService
).
@Before("this(dev.crackers.snippets.aop.ProductService)")
public void logBeforeThis(JoinPoint joinPoint) {
log.info("Before method in a bean of type Service: {}", joinPoint.getSignature().getName());
}
target(): Matches join points where the target object is an instance of the given type (i.e every method in ProductService
).
@Before("target(dev.crackers.snippets.aop.ProductService)")
public void logBefore(JoinPoint joinPoint) {
log.info("Before method in target of type Service: {}", joinPoint.getSignature().getName());
}
args(): Matches join points where the arguments are instances of the given types.
@Before("execution(* dev.crackers.snippets.aop.ProductService.updateProduct(..)) && args(product,..)")
public void logBeforeArgs(Product product) {
log.info("Product Intercepted: {}", product);
}
args() restricts matching to only those method executions where the method takes at least one parameter, and the argument passed to that parameter is an instance of Product. Additionally, it allows the advice to access the actual Product
object through the method parameter.
@annotation(): Matches join points where the subject of the join point has the given annotation.
@Before("@annotation(org.springframework.transaction.annotation.Transactional)")
public void logBeforeAnnotation(JoinPoint joinPoint) {
log.info("Before method with @GetMapping annotation: {}", joinPoint.getSignature().getName());
}
Spring AOP Use Cases
Logging Execution Time
@Around("execution(* dev.crackers.snippets.aop.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed(); // Proceed with the method execution
long executionTime = System.currentTimeMillis() - start;
log.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}
Security Check
@Before("execution(* dev.crackers.snippets.aop.ProductService.*(..))")
public void checkSecurity(JoinPoint joinPoint) {
// Simple security check logic
if (!SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
throw new SecurityException("User not authenticated");
}
log.info("Security check passed for: " + joinPoint.getSignature());
}
Transaction Management
@Around("execution(* dev.crackers.snippets.aop.ProductService.*(..))")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
Object result;
try {
result = joinPoint.proceed(); // Proceed with the method execution
status.commit();
} catch (Throwable throwable) {
status.rollback();
throw throwable;
}
return result;
}
In Summary
AOP in Spring Boot allows you to inject behaviors like logging, security checks, transaction management, and caching into your application without modifying the actual business logic. By using pointcut designators and advice types, you can neatly separate these cross-cutting concerns from the main functionality of your application.