Understanding Spring AOP: Creating Annotation for Logging Requests and Responses

Pratiyush Prakash
Dev Genius
Published in
5 min readJan 24, 2024

--

One powerful tool in the arsenal of Java developer is Aspect-Oriented Programming (AOP), and when seamlessly integrated with Spring Boot, it becomes a game-changer. In this article we will try to understand Spring Boot AOP by building an Annotation which can be re-used across any request mapping to log it’s request, response and status.

Photo by Ashkan Forouzani on Unsplash

What is AOP?

Aspect-Oriented Programming (AOP) is a programming paradigm that allows developer to modularize cross-cutting concerns, such as logging, error handling, and security, by separating them from the main business logic. In AOP, these concerns, known as aspects, are defined independently and then woven into the code at specific points, reducing code duplication- and improving maintainability. This approach enhances code organization and promotes a cleaner separation of concerns in software development.

AOP Concepts

There are few concepts which we should be familiarized with, before jumping down to the implementation.

Aspect

A module encapsulating a cross-cutting concern, such as logging or security. Aspect define what to do and where to do it in the code. In Spring AOP, aspects are implemented using regular classes annotated with @Aspect annotation.

Join Point

A specific point in the execution of the program, like method calls, object instantiations, or exception handling. Aspect specify where to apply their behavior by targeting a specific join point. In Spring AOP, a join point always represents a method execution.

Advice

Action taken by an aspect at a particular join point. There are different types of Advices like “before”, “after” etc. In Spring AOP, advice works as an interceptor, maintaining a chain of interceptors around the join point.

There are different types of Advices like:

  • Before Advice: Executes before a join point, providing a chance to perform actions or validations prior to the main logic.
  • After returning Advice: Executes after the completion of a join point’s normal execution. It’s useful for actions to be taken when a method successfully completes.
  • After throwing Advice: Executes if a join point throws an exception. It allows handling or logging exceptions.
  • After Advice: Executes regardless of the outcome (normal or exception) of the join point. It’s used for cleanup or finalization task.
  • Around Advice: It’s the most powerful out of all. It encompasses the join point, allowing complete control over the join point’s execution. It can modify the method’s behavior or entirely replace it.

Pointcut

A set of join points that an advice should be applied to. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut. Spring uses the AspectJ pointcut expression language by default.

Introduction

Introducing new methods or fields to existing classes, allowing aspects to add functionality to classes without modifying their source code. Spring AOP allow you to introduce new interfaces to any advised object.

Weaving

The process of integrating aspects with the main application code. It can happen at compile-time, load-time or runtime. Spring AOP performs weaving at runtime.

Target object

Object being advised by aspects. As Spring AOP only supports runtime weaving, this object will always be a proxied object.

AOP proxy

An object created by AOP framework in order to implement the aspect contracts. In Spring, AOP proxy will be a JDK dynamic proxy or a CGLIB proxy.

Custom Annotation

For this article, we will also need to understand about custom annotations. Lets cover some basics of that.

In Spring Boot, custom annotations allow you to define your own marker or metadata for your code. Let’s break down key concepts

  • Annotation Declaration: Use @interface to declare an annotation.
  • Target: Specify where the annotation can be used. Example: @Target(ElementType.METHOD) resticts usage to methods. We can use TYPE for classes and FIELD for fields.
  • Retention: Defines when the annotation information is retained. Example: @Retention(RetentionPolicy.RUNTIME) makes it available at runtime.
  • Attributes: Defines attributes within the annotation. Example: String value() default “Default value”;

Implementation

Now let’s go through the implementation.

Dependency

First, we will add spring AOP as a dependency.

<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Custom Annotation

Then we will create our custom Annotation.

package com.example.demo.annotations;

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

/**
* @author Pratiyush Prakash
*
* This is a custom annotation for logging request and response
*
* This only applies to METHODs and
* will be available in runtime
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRequestResponse {
}

Aspect

Now, we can create an Aspect where we will create Pointcut for our Annotation. And create advices to log requests and response for methods.

package com.example.demo.aspects;

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.annotation.Pointcut;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

/**
* @author Pratiyush Prakash
*
* This Aspect is a module which is responsible
* for logging all requests and response where we apply the
* LogRequestResponse annotation
*/
@Aspect
@Component
public class RequestResponseLoggingAspect {

// Define the pointcut for LogRequestResponse annotation
@Pointcut("@annotation(com.example.demo.annotations.LogRequestResponse)")
public void logAnnotationPointcut() {
}

// Before advice
@Before("logAnnotationPointcut()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before " + joinPoint.getSignature().getName());
}


// Normal after advice
@AfterReturning(pointcut = "logAnnotationPointcut()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
// Logging the response
System.out.println("After " + joinPoint.getSignature().getName() + "Method returned value " + result);

// Logging the response status
if (result instanceof ResponseEntity) {
ResponseEntity<Object> response = (ResponseEntity<Object>) result;
System.out.println("Response status: " + response.getStatusCode());
}
}

}

Application

Now we can add the annotation to the endpoints we want to test.

package com.example.demo.web;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.annotations.LogRequestResponse;

/**
* @author Pratiyush Prakash
*
* All endpoints resides here
*/
@RestController
@RequestMapping("/api/v1")
public class DemoController {

/**
* This is a sample secured API call
* @return String
*/
@LogRequestResponse
@GetMapping(value = "/secured/hello-world")
public String securedCall() {
return "hello world secured!!";
}

/**
* This is a sample un-secured API call
* @return String
*/
@LogRequestResponse
@GetMapping(value = "/hello-world")
public String unsecuredCall() {
return "hello world!!";
}

/**
* This is a sample GET call to return object
* @return Map of name and age
*/
@LogRequestResponse
@GetMapping(value = "/demo-object")
public Map<String, Integer> demoObjectMap() {
Map<String, Integer> map = new HashMap<>();
map.put("Pratiyush", 29);
return map;
}

/**
* This is a sample GET call to test different response status
* @return ResponseEntity
*/
@LogRequestResponse
@GetMapping(value = "/demo-response-entity")
public ResponseEntity<String> demoResponseEntity(@RequestParam Integer code) {
switch (code) {
case 200:
return new ResponseEntity<String>("Hello world!!", HttpStatus.ACCEPTED);
case 400:
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
default:
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}

}

That’s it. We are done with the code.

Demo

Now we can verify, if our implementation works or not. We can build and run our application. And open Swagger UI to call the endpoints. And then in your log, you can check if you can see the requests and responses or not.

Output log

To sum up, Aspect proves invaluable when we have to modularize our project and separate common concerns like logging, security, error handling and so on. In this article we have covered logging use case. Give it a try, in your projects if there is such a scenario.

References

--

--

Full stack Dev and Lead @ Texas Instruments. Follow me for short articles on software development.