Spring ‐ Aspect‐Oriented Programming (AOP) - Yash-777/MyWorld GitHub Wiki

Enabling @AspectJ Support with Java configuration

To enable @AspectJ support with Java @Configuration add the @EnableAspectJAutoProxy annotation:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

Aspect Oriented Programming (AOP) is a programming paradigm that enables the separation of cross-cutting concerns, or aspects, from the primary business logic in your code. This approach allows for the modularization of behaviors that span multiple classes, methods, or functions.

Key components of Aspect-Oriented Programming (AOP):linkedin.com post

Concept Code Representation Explanation
Aspect @Aspect on LoggingAspect class A class that encapsulates advices and pointcuts (cross-cutting logic)
Join Point Parameter JoinPoint joinPoint in advice methods A point in execution like method call or exception throw
Pointcut @Pointcut("execution(...)") An expression that matches join points
Advice @Before, @After, @AfterReturning, @AfterThrowing, @Around The actual code executed at the matched join point
  • Aspect: A module that defines cross-cutting concerns (like logging, security, or transaction management) and applies them across different parts of the application.

  • Join Point: A point in the program's execution where an aspect can be applied (e.g., method call, constructor call, field access).

  • Pointcut: A predicate or expression that matches join points where the aspect should be executed.

  • Advice: The code that is executed at a particular join point. Types of advice include:

    • Before Advice: Runs before a method execution
    • After Advice: Runs after a method execution.
    • Around Advice: Runs before and after the method execution.
    • After Returning: Runs after a method returns successfully.
    • After Throwing: Runs if a method throws an exception.
  • Weaving: The process of linking aspects with the main program code. It can happen at:

    • Compile-time: Aspects are woven into the code during compilation.
    • Load-time: Aspects are woven during class loading.
    • Runtime: Aspects are woven while the program is running (via proxies, e.g., in frameworks like Spring AOP).

Why Use AOP?

  • Separation of Concerns: Keeps business logic clean by separating cross-cutting concerns like logging, security, and caching.
  • Code Reusability: Reuse aspects like logging or authentication across multiple parts of the application.
  • Improved Maintainability: Since cross-cutting logic is modularized, updates and maintenance become easier.
  • Better Readability: Business logic stays clean and focused, making it easier to read and understand.

Common Frameworks for AOP

  • Spring AOP (Java)
  • AspectJ (Java)
Spring AOP is proxy-based cross-cutting concernsjavachinna.com/, or aspectsmedium.com PontCut Types of advice RealTime Usagemedium.com
image
image
image image image image image

📘 Java Example: AopConceptsExample.java

package com.example.aop;

/**
 * Demonstrates core AOP concepts in Spring:
 *
 * - Aspect    : A modularization of a cross-cutting concern (like logging, security, etc.)
 * - Join Point: A specific point in the execution of the program (e.g., method execution)
 * - Pointcut  : A predicate that matches join points (e.g., all methods in a package)
 * - Advice    : Action taken at a pointcut (e.g., before, after, around)
 */
@Aspect
@Component
public class LoggingAspect {

    /**
     * Pointcut Definition
     *
     * This defines a set of join points — in this case, all public methods in the `service` package.
     */
    @Pointcut("execution(public * com.example.service..*(..))")
    public void serviceLayerExecution() {
        // Pointcut methods are empty. The annotation defines where the pointcut applies.
    }

    /**
     * Advice: Before
     *
     * This advice runs before any matched method (join point) defined by the `serviceLayerExecution` pointcut.
     * It uses the join point to access method signature information.
     *
     * @param joinPoint provides access to the target method, arguments, etc.
     */
    @Before("serviceLayerExecution()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("[Before Advice] Calling: " + joinPoint.getSignature().toShortString());
    }

    /**
     * Advice: After Returning
     *
     * Executes after successful completion of a service method.
     */
    @AfterReturning(pointcut = "serviceLayerExecution()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("[AfterReturning Advice] Method returned: " + result);
    }

    /**
     * Advice: Around
     *
     * Wraps method execution, allowing you to control before and after execution.
     */
    @Around("serviceLayerExecution()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[Around Advice] Before execution: " + pjp.getSignature());
        Object result = pjp.proceed(); // Proceed with original method
        System.out.println("[Around Advice] After execution: " + pjp.getSignature());
        return result;
    }
}

Spring AOP & AspectJ Pointcut Designators

Spring AOP (using AspectJ expressions) provides a variety of pointcut designators like execution(), @within(), @target(), this(), target() and more, each of which plays a specific role.

Here’s a comprehensive summary of when and how to use each:

Pointcut Designator Description / When to Use Examples & Summary
execution() execution() — Match Method Signatures (Compile-Time)
Matches method executions (method signature). Used for intercepting method calls. Works at compile-time.
execution(* com.example.service.MyService.*(..))
➡ Matches any method in MyService.
execution(public String com.example.MyService.get*(..))
➡ Matches public methods returning String whose names start with get.
within() within() — Limit Scope to Certain Packages or Classes
Matches all join points within certain classes or packages. Use to scope advice to packages.
within(com.example.service..*)
➡ Matches all methods inside com.example.service and its subpackages.
this() this() — Proxy Type Match (Interface or Class)
Matches when the proxy object (i.e., Spring AOP proxy) implements or extends the given type.
this(com.example.MyInterface)
➡ Applies to beans proxied as MyInterface.Useful with interface-based proxies.
this(com.example.repository.UserRepository)
➡ Matches when the current proxy implements UserRepository.
target() target() — Actual Target Class Match
Matches when the target object (the actual class behind the proxy) is of a given type.
target(com.example.MyClass)
➡ Advice runs if actual object is a MyClass instance.Use with CGLIB or class-based proxies.
target(com.example.repository.UserRepository)
➡ Matches if the underlying class (proxied or not) is of type UserRepository.
args() args() — Method Argument Types Match
Matches methods based on the runtime argument types.
args(java.lang.String, ..)
➡ Matches methods where the first parameter is a String.
@within() @within() — Class-Level Annotation Match
Matches if the declaring class is annotated with a specific annotation.
Checked at compile-time.
@within(org.springframework.stereotype.Service)
➡ Applies to methods inside classes annotated with @Service.
@within(org.springframework.stereotype.Repository)
➡ Applies to any method inside a class annotated with @Repository.
@target() @target() — Runtime Class Annotation Match
Matches when the target class is annotated with a specific annotation. Evaluated at runtime.
Similar to @within() but resolved at runtime.
@target(org.springframework.stereotype.Repository)
➡ Applies to all methods in beans annotated with @Repository.
➡ Useful when you're using AOP on proxies and want to check annotations at runtime.
@annotation() @annotation() — Method-Level Annotation Match
Matches methods that are directly annotated with a specific annotation.
@annotation(org.springframework.transaction.annotation.Transactional)
➡ Advice runs on any method annotated with @Transactional.
@args()
Matches when method arguments themselves are annotated with a specific annotation.
@args(com.example.MyAnnotation)
➡ Matches if any method argument is annotated with @MyAnnotation.
bean() (Spring AOP only)
Matches beans by their name, supports wildcards. Useful in XML-based configuration or component scanning.
bean(*Service)
➡ Matches any bean whose name ends with Service.
cflow() (AspectJ only)
Matches join points within the control flow of another join point. Not supported in Spring AOP.
cflow(call(* com.example.MyService.*(..)))
➡ Matches if current execution is inside a MyService method call.
if() (AspectJ only)
Matches based on custom boolean condition evaluated at runtime. Not supported in Spring AOP.
if(someBooleanCondition())
➡ Used in full AspectJ (not Spring).
call() (AspectJ only)
Matches when a method is called (not executed). Not usable in Spring AOP.
call(* com.example.MyService.*(..))
➡ Works in compile-time weaving with AspectJ only.

ProceedingJoinPoint — Access Method Info Inside Advice

In @Around advice, you use ProceedingJoinPoint to:

  • Get method arguments
  • Get method signature
  • Proceed with the method call
@Around("execution(* com.example..*Service.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Object[] args = pjp.getArgs();
    return pjp.proceed();
}

you can use multiple conditions in a single pointcut expression in Spring AOP by combining them using logical operators like && (AND), || (OR), and ! (NOT).


📘 Spring AOP: Inline vs External Pointcut Expression Comparison

  • Use inline pointcuts for quick, targeted advice when you don't plan to reuse the expression.
  • Use external (named) pointcuts for maintainability, reusability, and clarityespecially in larger projects.
Type Definition Syntax Example Use Case / Pros
Inline Pointcut Directly use the pointcut expression inside the advice annotation
@Before("execution(* com.example.service..*(..))")
public void logBefore() { ... }
  • Quick and simple for one-off advices
  • Good for demos or small projects
External (Named) Pointcut Declare the expression separately in a method with @Pointcut and reuse its name
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayerExecution() {}
@Before("serviceLayerExecution()")
public void logBefore() { ... }
  • Improves readability and maintainability
  • Can be reused across multiple advices (e.g. @Before, @After, etc.)
  • Recommended for large applications
Inline with @AfterReturning Example for advice with return value capture
@AfterReturning(value = "execution(* com.example.service..*(..))", returning = "result")
public void logAfter(JoinPoint jp, Object result) { ... }
  • Useful when the advice logic is tightly coupled with a specific return scenario
External with @AfterReturning Named pointcut reused for capturing return values
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayerExecution() {}
@AfterReturning(pointcut = "serviceLayerExecution()", returning = "result")
public void logAfter(JoinPoint jp, Object result) { ... }
  • Encourages reusability with return-aware advices

In Spring Boot, aspects typically refer to Aspect-Oriented Programming (AOP), which is used to separate cross-cutting concerns (like logging, transactions, security, etc.) from business logic.

Spring Boot comes with inbuilt support for AOP, built on top of Spring AOP and AspectJ. This allows you to define reusable logic (called advice) that can be applied to method execution (called join points) using aspects.

image Spring Framework - Common Inbuilt Aspects (AOP annotations)

📄 In Spring Framework, the @Transactional, @Async, and @Scheduled annotations are implemented using Aspect-Oriented Programming (AOP) to separate cross-cutting concerns from business logic.

Feature Annotation Uses AOP? Built-in?
Transaction Management @Transactional
Caching @Cacheable, etc.
Async Execution @Async
Scheduling @Scheduled
Validation @Valid, @Validated
Custom Logging, Security, etc. @Aspect ❌ (User-defined)

🧾 Aspect Ordering

📦🔧🛠🧱 In scenarios where multiple aspects are applied to the same method, the order of execution can be controlled using the order attribute. For example, when both @Transactional and @Async annotations are present, you can specify the order in which their aspects are applied:

@EnableAsync(order = 1)
@EnableTransactionManagement(order = 2)

This configuration ensures that the asynchronous aspect is applied before the transactional aspect.

🎯

Common Inbuilt Aspects in Spring Boot

Although Spring Boot doesn't ship with many aspects by default, it enables the use of important AOP features and also uses AOP internally for:

  1. Transactional Management (@Transactional)
  • Automatically applies transaction management around methods.
  • Uses AOP behind the scenes to open/close transactions.
@Transactional
public void saveData(Entity entity) {
    repository.save(entity);
}
  • Primary Aspect Class: AnnotationTransactionAspect
    • Package: org.springframework.transaction.aspectj
    • Description: This class is responsible for applying transaction management to methods annotated with @Transactional. It intercepts method calls and manages the transaction lifecycle (begin, commit, rollback) based on the method's execution outcome.
  • Configuration: To enable AspectJ-based transaction management, you can configure the AnnotationTransactionAspect with a transaction manager:
  DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());
  AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);

This setup ensures that the AnnotationTransactionAspect uses the specified transaction manager to manage transactions for methods annotated with @Transactional.

  1. Caching (@Cacheable, @CachePut, @CacheEvict)
  • Methods are wrapped in advice to check the cache before execution.
@Cacheable("books")
public Book getBook(String isbn) {
    return bookRepository.findByIsbn(isbn);
}
  1. Asynchronous Execution (@Async)
  • Uses proxies to execute methods in a different thread.
@Async
public void sendEmail(String to) {
    // send email logic
}

Requires @EnableAsync on a configuration class.

  • Primary Aspect Class: AnnotationAsyncExecutionInterceptor
    • Package: org.springframework.scheduling.annotation
    • Description: This class intercepts method calls annotated with @Async and executes them asynchronously using a specified Executor. It supports both void and Future return types, allowing asynchronous execution with or without a result.
  • Base Class: AsyncExecutionAspectSupport
    • Package: org.springframework.aop.interceptor
    • Description: Provides foundational support for asynchronous method execution aspects, including executor qualification on a method-by-method basis.
  • Configuration: To enable asynchronous method execution, you can configure the AnnotationAsyncExecutionInterceptor with an Executor:
  AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor();
  interceptor.setExecutor(taskExecutor);

This setup ensures that methods annotated with @Async are executed asynchronously using the specified Executor.

  1. Scheduling (@Scheduled)
  • Executes methods on a schedule using background threads.
@Scheduled(cron = "0 0 * * * ?")
public void runHourlyTask() {
    // task logic
}

Requires @EnableScheduling.

  • Primary Aspect Class: ScheduledAnnotationBeanPostProcessor
    • Package: org.springframework.scheduling.annotation
    • Description: This class processes beans annotated with @Scheduled and schedules their execution based on the specified cron expressions or fixed-rate/delay values. It manages the lifecycle of scheduled tasks within the Spring context.
  • Configuration: To enable scheduling, you can configure the ScheduledAnnotationBeanPostProcessor:
  ScheduledAnnotationBeanPostProcessor processor = new ScheduledAnnotationBeanPostProcessor();
  processor.setScheduler(taskScheduler);

This setup ensures that methods annotated with @Scheduled are executed according to their specified schedules.

  1. Validation (@Valid, @Validated)
  • Automatically triggers validation on controller inputs.
  • Uses proxies or method interception to apply validation logic.
@PostMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody UserDto user) {
    // validation happens before this method is invoked
}

🧰 Custom Aspect Example (Logging) : To define your own aspect, use @Aspect and @Component:

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Calling method: " + joinPoint.getSignature().getName());
    }

    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void logAfter(JoinPoint joinPoint, Object result) {
        System.out.println("Method " + joinPoint.getSignature().getName() + " returned: " + result);
    }
}

Make sure AOP is enabled (Spring Boot does this automatically if spring-boot-starter-aop is on the classpath):

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

🧩 Most commonly used Spring AOP @Around pointcut expressions, including inclusion (expression) and exclusion (!expression) patterns



🧠 Notes:

  • @within: Based on class-level annotation
  • @annotation: Based on method-level annotation
  • execution(...): Matches method execution
  • within(...): Restrict by class/package
  • ! operator: Used for negation
  • Combine expressions with &&, ||, ! for fine-grained control

✅ Example Use

@Around(
    "@within(org.springframework.stereotype.Service) " +
    "&& execution(* com.example..*.*(..)) " +
    "&& !within(com.example.web..*) " +
    "&& !within(@org.springframework.context.annotation.Configuration *)"
)

✔️ Logs all methods in @Service classes under com.example, but ❌ Skips any in the web package or @Configuration classes.

📘 Spring AOP @Around Pointcut Expression Cheat Sheet

Type Pointcut Expression Description Typical Use Case
Method Execution execution(* com.example..*.*(..)) Matches execution of any method in package or subpackages Log all method calls in a base package
execution(public void save(..)) Matches public save method Target method by name and visibility
execution(* *..*.get*(..)) Matches any method starting with get Logging getters
Method Exclusion !execution(* com.example.config..*.*(..)) Excludes all methods in config package Skip config classes
Within Class/Package within(com.example.service.*) Match methods within classes in service package Target service layer
!within(com.example.web..*) Exclude methods in web package Avoid logging in web layer
Class Annotation (@within) @within(org.springframework.stereotype.Service) Match any class annotated with @Service Apply logging to service classes only
!@within(org.springframework.web.bind.annotation.RestController) Exclude @RestController annotated classes Avoid REST endpoint logging
Method Annotation (@annotation) @annotation(org.springframework.transaction.annotation.Transactional) Match methods annotated with @Transactional Transaction-specific logging
Target Object Annotation @target(org.springframework.stereotype.Repository) Match if target object is @Repository DAO layer logging
Proxy Object Type (this) this(com.example.MyService) Match where the proxy is of specific type Interface-based proxying
Method Arguments args(java.lang.String, ..) Match methods where first argument is a String Input-based cross-cutting
Specific Bean Name bean(myBean) Match any method inside a bean named myBean Debug specific Spring bean
Combined Conditions @within(org.springframework.stereotype.Service) && execution(* com.example..*.*(..)) Match service methods in com.example.. Combined scoped logging
!within(com.example.web..*) && !within(@org.springframework.context.annotation.Configuration *) Exclude web and @Configuration classes Filter unwanted targets

RequestLoggingAspect
package com.github.myworld.aspects;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Aspect
@Component
@org.springframework.context.annotation.EnableAspectJAutoProxy
public class RequestLoggingAspect {
// @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) " + //@RequestMapping(method = RequestMethod.GET)
// "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
// "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +  // @PostMapping(value = "")
// "|| @annotation(org.springframework.web.bind.annotation.PathVariable)" +
// "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
// "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
// )
  @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
      "|| @annotation(org.springframework.web.bind.annotation.GetMapping) " +
      "|| @annotation(org.springframework.web.bind.annotation.PostMapping) " +
      "|| @annotation(org.springframework.web.bind.annotation.PathVariable) " +
      "|| @annotation(org.springframework.web.bind.annotation.PutMapping) " +
      "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)")
  public void requestMapping() {
  }
  
  @Around("requestMapping()")
  public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
    org.aspectj.lang.reflect.MethodSignature signature = (org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature();
    System.out.println("MethodSignature:"+ signature );
    
    long startTime = System.currentTimeMillis();

    // Log request details before method execution
    logRequestDetails(joinPoint);

    logDetails(joinPoint);
    
    // Proceed with the original method execution
    Object result = joinPoint.proceed();

    // Log response details after method execution
    long endTime = System.currentTimeMillis();
    logResponseDetails(result, endTime - startTime);

    return result;
  }
  
  public void printRequestMappingParam(org.springframework.web.bind.annotation.RequestMapping requestParam) {
    //org.springframework.web.bind.annotation.RequestMapping requestParam = AnnotationUtils.findAnnotation(method, org.springframework.web.bind.annotation.RequestMapping.class);
    if (requestParam != null) {// @RequestParam
      
      String nameOfMapping = requestParam.name();
      if (nameOfMapping != null && nameOfMapping.length() > 0) {
        System.out.println("nameOfMapping:"+nameOfMapping);
      }
      
      String[] headers = requestParam.headers();
      for (String header : headers) {
        System.out.println("Header:"+header);
      }
      String[] produces = requestParam.produces();
      for (String produce : produces) {
        System.out.println("produces:"+produce);
      }
      String[] consumes = requestParam.consumes();
      for (String consume : consumes) {
        System.out.println("Consumes:"+consume);
      }
      
      RequestMethod[] requestedMethod = requestParam.method();
      for (RequestMethod requestedMeth : requestedMethod) {
        System.out.println("RequestMethod:"+requestedMeth);
      }
      String[] params = requestParam.params();
      for (String param : params) {
        System.out.println("Param:"+param);
      }
      String[] values = requestParam.value();
      for (String value : values) {
        System.out.println("values:"+value);
      }
    }
  }
  
  public Object forwordReq(ProceedingJoinPoint joinPoint) throws Throwable {
    //long startTime = System.currentTimeMillis();
    org.springframework.util.StopWatch sw = new org.springframework.util.StopWatch();
    sw.start("logExecutionTime:");
    
    Object result = joinPoint.proceed();
    
    sw.stop();
    long timeTaken = sw.getTaskInfo()[0].getTimeMillis(); //endTime = System.currentTimeMillis() - startTime;
    System.out.println("Timetaken in MilliSec:"+timeTaken);
    return result;
  }

  public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    // Access the HttpServletRequest
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
//    log.info("Request received: Method={}, Headers={}, QueryParams={}, PathVariables={}, RequestBody={}",
//         request.getMethod(), request.getHeaderNames(), request.getParameterMap(),
//         pathVariables, requestObject);
    // Extract request details (headers, parameters, path variables, request body)
    // Convert request body to JSON (if applicable)
    // Log the details
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
      System.out.println(headerNames.nextElement());
    }

    org.aspectj.lang.reflect.MethodSignature signature = (org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature();
    System.out.println("MethodSignature:"+ signature );
    java.lang.reflect.Method method = signature.getMethod();
    
    // HttpComponentsClientHttpRequestFactory - method.getClass()
    // AnnotationFilter JAVA_LANG_ANNOTATION_FILTER = AnnotationFilter.packages("java.lang.annotation")
    
    //@org.springframework.web.bind.annotation.RequestMapping(path=[], headers=[], method=[GET], name=, produces=[], params=[], value=[compensation/ratecard/{planId}], consumes=[])
    Annotation[] declaredAnnotations = method.getDeclaredAnnotations();
    //Annotation[] annotations = method.getAnnotations();
    for (Annotation annotation : declaredAnnotations) {
      System.out.println("annotation:"+annotation.toString());
      //@org.springframework.security.access.prepost.PreAuthorize(value=)
      if (annotation instanceof org.springframework.web.bind.annotation.PathVariable) {// @PathVariable
        org.springframework.web.bind.annotation.PathVariable pathVar = (PathVariable) annotation;
        //org.springframework.web.bind.annotation.PathVariable pathVariable = AnnotationUtils.findAnnotation(method, org.springframework.web.bind.annotation.PathVariable.class);
        System.out.println("Annotation - PathVariable - name:val = " + pathVar.name() + " : " + pathVar.value());
      }
      //Key: interface org.springframework.web.bind.annotation.RequestMapping
      //Value: @org.springframework.web.bind.annotation.RequestMapping(path=[], headers=[], method=[POST], name=, produces=[], params=[], value=[organizationalUnit], consumes=[])
      if (annotation instanceof org.springframework.web.bind.annotation.RequestParam) { // @javax.ws.rs.QueryParam
        org.springframework.web.bind.annotation.RequestParam requestPar = (RequestParam) annotation;
        System.out.println("Annotation - RequestParam - name:val = " + requestPar.name() + " : " + requestPar.value());
      }
      if (annotation instanceof org.springframework.web.bind.annotation.RequestMapping) { // @javax.ws.rs.QueryParam
        org.springframework.web.bind.annotation.RequestMapping requestParam = (RequestMapping) annotation;
        //org.springframework.web.bind.annotation.RequestMapping requestParam = AnnotationUtils.findAnnotation(method, org.springframework.web.bind.annotation.RequestMapping.class);
        System.out.println("Annotation - RequestMapping - name:val = " + requestParam.name() + " : " + requestParam.value());
        printRequestMappingParam(requestParam);
      }
    }
    
    Object result = forwordReq(joinPoint);
    return result;
  }
  public void logDetails(ProceedingJoinPoint joinPoint) throws Throwable {
    // Access the HttpServletRequest
System.out.println("----- logDetails -----");
    org.aspectj.lang.reflect.MethodSignature signature = (org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature();
    //System.out.println("MethodSignature:"+ signature );
    java.lang.reflect.Method method = signature.getMethod();
    
    // HttpComponentsClientHttpRequestFactory - method.getClass()
    // AnnotationFilter JAVA_LANG_ANNOTATION_FILTER = AnnotationFilter.packages("java.lang.annotation")
    
    //@org.springframework.web.bind.annotation.RequestMapping(path=[], headers=[], method=[GET], name=, produces=[], params=[], value=[compensation/ratecard/{planId}], consumes=[])
    Annotation[] declaredAnnotations = method.getDeclaredAnnotations();
    for (Annotation annotation : declaredAnnotations) {
      System.out.println("Method DeclaredAnnotations:"+annotation.toString());
      //@org.springframework.security.access.prepost.PreAuthorize(value=hasPermission('/commissionrun/view', 'Feature Access'))
      //@org.springframework.web.bind.annotation.RequestMapping(path=[], headers=[], method=[GET], name=, produces=[], params=[], value=[commissionrundetails/{periodName}], consumes=[])
      
      if (annotation instanceof org.springframework.web.bind.annotation.RequestMapping) { 
        org.springframework.web.bind.annotation.RequestMapping requestParam = (RequestMapping) annotation;
        //org.springframework.web.bind.annotation.RequestMapping requestParam = AnnotationUtils.findAnnotation(method, org.springframework.web.bind.annotation.RequestMapping.class);
        System.out.println("Annotation - RequestMapping - name:val = " + requestParam.name() + " : " + requestParam.value());
        printRequestMappingParam(requestParam);
      }
    }
    
/*
 * it's important to note that @QueryParam is not a Spring annotation; it's typically used in JAX-RS (Java API for RESTful Web Services).
 * In Spring, query parameters are usually handled using @RequestParam. Below is an example of a Spring Rest API method that accepts 
 * @PathVariable, @RequestParam, and a query parameter (assuming it as @RequestParam): // @javax.ws.rs.QueryParam
 */
    Annotation[] annotationsOnMethod = method.getAnnotations();
    for (Annotation annotation : annotationsOnMethod) {
      System.out.println("Method annotationsOnMethod:"+annotation.toString());
    }
//    if (annotation instanceof org.springframework.web.bind.annotation.PathVariable) {// @PathVariable
//      org.springframework.web.bind.annotation.PathVariable pathVar = (PathVariable) annotation;
//      //org.springframework.web.bind.annotation.PathVariable pathVariable = AnnotationUtils.findAnnotation(method, org.springframework.web.bind.annotation.PathVariable.class);
//      System.out.println("Annotation - PathVariable - name:val = " + pathVar.name() + " : " + pathVar.value());
//    }
//    //Key: interface org.springframework.web.bind.annotation.RequestMapping
//    //Value: @org.springframework.web.bind.annotation.RequestMapping(path=[], headers=[], method=[POST], name=, produces=[], params=[], value=[organizationalUnit], consumes=[])
//    if (annotation instanceof org.springframework.web.bind.annotation.RequestParam) { // @javax.ws.rs.QueryParam
//      org.springframework.web.bind.annotation.RequestParam requestPar = (RequestParam) annotation;
//      System.out.println("Annotation - RequestParam - name:val = " + requestPar.name() + " : " + requestPar.value());
//    }
System.out.println("----- logDetails -----");
  }



  private void logRequestDetails(ProceedingJoinPoint joinPoint) throws IOException {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attributes != null) {
      HttpServletRequest request = attributes.getRequest();

      // Log request method, URI, headers, query parameters, and request body
      System.out.println("Request Method: " + request.getMethod());
      System.out.println("Request URI: " + request.getRequestURI());
      System.out.println("Request Headers: " + getRequestHeaders(request));
      
      System.out.println("Query String: " + request.getQueryString()); // regionCode=TMPL
      System.out.println("Query Parameters: " + getRequestParameters(request)); // {regionCode=[TMPL]} = /commissionrundetails/202310?regionCode=TMPL
       
      // Log request body in JSON format
      if (request.getInputStream() != null) {
        // Handle the HttpServletRequest
        
        ServletInputStream inputStream = request.getInputStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        Stream<String> lines = bufferedReader.lines();
        Optional<String> reduce = lines.reduce(String::concat);
        if (reduce.isPresent()) { // NoSuchElementException: No value present at java.util.Optional.get(Optional.java:135)
          String requestBody = reduce.get();
          System.out.println("HttpServletRequest Body String: " + requestBody );
          try {
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode requestBodyJson = objectMapper.valueToTree(requestBody);
            System.out.println("HttpServletRequest Body JSON Node: " + requestBodyJson);
          } catch (Exception e) {
            System.err.println("Error parsing request body: " + e.getMessage());
          }
        }
      }
    }
  }

  private void logResponseDetails(Object result, long executionTime) {
    // Log response details
    System.out.println("Response: " + result);
    System.out.println("Execution Time: " + executionTime + " ms");
  }

  private Map<String, String> getRequestHeaders(HttpServletRequest request) {
    return Collections.list(request.getHeaderNames())
        .stream()
        .collect(Collectors.toMap(name -> name, request::getHeader));
  }

  private Map<Object, Object> getRequestParameters(HttpServletRequest request) {
    return request.getParameterMap()
        .entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, entry -> Arrays.toString(entry.getValue())));
  }
}

🧩
Coordinating Multiple Ordered Aspects in Spring with Shared Context

🔄 Execution Flow (Example) If a method is annotated with a pointcut matching both aspects:

HTTP Request Thread:
 └──> EndpointTimeAspect (Order 1)
       └──> Sets enabled = true
       └──> Proceeds
             └──> AroundMethodAspect (Order 2)
                   └──> Checks enabled
                   └──> Executes conditional logic
             └──> Controller Method
       └──> Finally block: enabled.remove()

In a Spring application, you may want to define multiple aspects where:

  • One aspect (e.g. Order1) runs first and sets some context.
  • Another aspect (e.g. Order2) executes after, and its logic depends on the context set by the first.

To achieve this safely and concurrently, especially in a multi-threaded environment like a web application, follow this approach.

🛠️ Core Concepts

Concept Description
@Aspect Marks a class as an Aspect (AOP component).
@Order Determines the execution order of aspects. Lower values execute earlier.
ThreadLocal Stores thread-confined values. Each thread has its own isolated variable.
finally { remove(); } Ensures proper cleanup to avoid memory leaks in thread pools.

Aspect 1 – Marks execution as "enabled" for downstream aspects

@Aspect @Order(1) // This aspect executes before others with a higher order
@Component
@EnableAspectJAutoProxy
public class EndpointTimeAspect {
  // Thread-local flag to be used by downstream aspects
  public static final ThreadLocal<Boolean> reqTriggered = ThreadLocal.withInitial(() -> false);
  
  @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) " + //@RequestMapping(method = RequestMethod.GET)
      "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +  // @PostMapping(value = "")
      "|| @annotation(org.springframework.web.bind.annotation.PathVariable)" +
      "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
      )
  public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    reqTriggered.set(true); // Enable the downstream aspect
    
    Object result = null;
    try {
      org.springframework.util.StopWatch sw = new org.springframework.util.StopWatch();
      sw.start("logExecutionTime:");
      
      result = joinPoint.proceed();
      
      sw.stop();
      long timeTaken = sw.getTaskInfo()[0].getTimeMillis();
      
      org.aspectj.lang.reflect.MethodSignature signature = (org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature();
      java.lang.reflect.Method method = signature.getMethod();
      
      RequestMapping ann = AnnotationUtils.findAnnotation(method, RequestMapping.class);
      String[] value = ann.value();
      String path = "";
      for (String string : value) {
        path += string;
      }
      
      System.out.println("["+ signature +"], Time taken for endpoint [ " +path +" ] : " + timeTaken + " ms");
      
    } finally {
      reqTriggered.remove(); // IMPORTANT: Avoid memory leaks
    }
    return result;
  }
}

Aspect 2 – Executes conditionally based on Aspect 1

@Aspect @Order(2) // Executes after EndpointTimeAspect
@Component
public class AroundMethodAspect {
  private static final Logger logger = LoggerFactory.getLogger(PerformanceLoggingAspect.class);
  
  @Value("${performance.logging.enabled:true}")
  private boolean enabled;
  
  @Around("execution(* com.github.yash777.*..impl..*.*(..))")
  public Object logConditionalLogic(final ProceedingJoinPoint joinPoint) throws Throwable {
    if (!EndpointTimeAspect.reqTriggered.get()) {
      // Skip this logic if not enabled by the previous aspect
      return joinPoint.proceed();
    }
    
    if (!enabled) {
      return joinPoint.proceed();
    }
    
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String className = methodSignature.getDeclaringType().getSimpleName();
    String methodName = methodSignature.getName();
    
    logger.info("Start Execution of {}.{}", className, methodName);
    
    long start = System.currentTimeMillis();
    Object result = joinPoint.proceed();
    long elapsed = System.currentTimeMillis() - start;
    
    logger.info("End Execution of {}.{} :: {} ms", className, methodName, elapsed);
    return result;
  }
}

✅ Key Notes

  • ThreadLocal makes it thread-safe and request-isolated — each incoming request thread gets its own copy of the enabled flag.
  • Static ThreadLocal is safe if used correctly (with .remove()).
  • @Order is essential to enforce execution precedence of aspects.

🧪 Test Scenarios

Scenario Expected Behavior
Multiple concurrent requests Each has its own isolated ThreadLocal state
Only Aspect 1 matches pointcut Aspect 1 runs; Aspect 2 skips due to !enabled
Aspect 2 runs without Aspect 1 Conditional logic is skipped (safety guard)

✅ Why ThreadLocal Works Safely with Concurrent Requests

  • ThreadLocal is specifically designed to store thread-confined data.
  • Even though enabled is static, each thread (i.e., each request in a web app) gets its own isolated value.

So, for example:

Request Thread enabled.get() value
Req 1 T1 true or false (isolated)
Req 2 T2 true or false (independent)

🧩
Aspect for Capturing HTTP Requests, Responses, Controller Metadata, and Execution Time

Captures full details of incoming HTTP requests and outgoing responses, including controller class, method, query parameters, request body, and execution time. Useful for debugging, auditing, and performance analysis.

===== Endpoint Execution Trace =====
  → Controller: com.example.controller.MyController
  → Method: createSomething()
  → HTTP Method: POST
  → URL: /api/v1/create
  → Request Body: {...}
  → Response: {...}
  → Duration: 42 ms
====================================

⚠️ Spring-managed web application context. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

  • RequestContextHolder holds request-related context using ThreadLocal.
  • getRequestAttributes() returns a RequestAttributes object (often cast to ServletRequestAttributes in web apps).
  • This allows you to access HttpServletRequest from anywhere Spring manages (like AOP, services, etc.).
  • This only works inside a web request context — i.e., during an actual HTTP request.
  • If used outside a controller (like in async threads, schedulers), it may return null.

📝 Notes:

  • For actual raw body content (e.g., JSON string), you’d need a filter or interceptor with HttpServletRequestWrapper because body content is consumed only once by default in servlets.
  • To display request and response body used Jackson's ObjectMapper:
    • ✅ Minify (compact) JSON
    • ✅ Beautify (pretty print) JSON
  • If your controller returns raw strings (e.g., "Hello World"), it will log that as plain text.
  • If response is complex (like ResponseEntity), you might need to handle that explicitly.

To inspect a request URL in an AOP @Around advice using Spring AOP and check if it contains /epretroactive/ or /commissionrun/, you can follow this approach. The key is to get the current HttpServletRequest inside the advice method and then inspect the URL.

⚠️ Raw body capture requires wrapping HttpServletRequest because the stream can only be read once. This version assumes you're using a Filter or HttpServletRequestWrapper to cache the body (shown below).

✅ 1. 📌 ApiLoggingAspect : For API-focused projects, logs all inbound/outbound traffic

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.fasterxml.jackson.databind.ObjectMapper;

@Aspect @Order(1) // This aspect executes before others with a higher order
@Component
@EnableAspectJAutoProxy
public class ApiLoggingAspect {
  // Thread-local flag to be used by downstream aspects
  public static final ThreadLocal<Boolean> reqTriggered = ThreadLocal.withInitial(() -> false);
  private final ObjectMapper objectMapper = new ObjectMapper();
  
  @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
      "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.PathVariable)" +
      "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
      )
  public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    String threadName = Thread.currentThread().getName();
    
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attributes != null) {
      HttpServletRequest request = attributes.getRequest();
      
      String method = request.getMethod();
      String url = request.getRequestURL().toString();
      String queryString = request.getQueryString();
      String fullUrl = url + (queryString != null ? "?" + queryString : "");
      
      String className = joinPoint.getSignature().getDeclaringTypeName();
      String methodName = joinPoint.getSignature().getName();
      
      System.out.println("--------------------------------------------------");
      System.out.println("Thread: " + threadName);
      System.out.println("Start Time: " + startTime);
      System.out.println("Handler: " + className + "#" + methodName);
      System.out.println("HTTP Method: " + method);
      System.out.println("Request URL: " + fullUrl);
      
      if (url.contains("/epretroactive/") || url.contains("/commissionrun/")) {
        System.out.println("Matched path: " + url);
        reqTriggered.set(true); // Enable the downstream aspect
      }
      
      // Print Query Parameters
      Map<String, String> queryParams = new HashMap<>();
      Enumeration<String> paramNames = request.getParameterNames();
      while (paramNames.hasMoreElements()) {
        String param = paramNames.nextElement();
        queryParams.put(param, request.getParameter(param));
      }
      if (!queryParams.isEmpty()) {
        System.out.println("Query Parameters: " + objectMapper.writeValueAsString(queryParams));
      }
      
      // Raw JSON body for POST/PUT/DELETE
      if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
        String rawBody = (String) request.getAttribute("cachedRequestBody");
        if (rawBody != null && !rawBody.isBlank()) {
          printJSON(rawBody.toString());
        } else {
          System.out.println("Request Body: [Empty or missing]");
        }
      }
    }
    
    Object result;
    try {
      // ---- Proceed and capture response ----
      result = joinPoint.proceed();
      
      // ---- Log Response ----
      if (result != null) {
        printJSON(result.toString());
      } else {
        System.out.println("Response Body: [null]");
      }
    } finally {
      long endTime = System.currentTimeMillis();
      System.out.println("End Time: " + endTime);
      System.out.println("Thread: " + threadName + " | Duration: " + (endTime - startTime) + " ms");
      System.out.println("--------------------------------------------------");
      
      reqTriggered.remove(); // IMPORTANT: Avoid memory leaks
    }
    
    return result;
  }
  
  
  private void printJSON(String rawBody) {
    if (isJsonString(rawBody)) {
      try {
        Object json = objectMapper.readValue(rawBody, Object.class);
        
        // Minified JSON
        String minifiedJson = objectMapper.writeValueAsString(json);
        System.out.println("Minified JSON Body: " + minifiedJson);
        
        // Beautified JSON
        String prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
        System.out.println("Beautified JSON Body:\n" + prettyJson);
      } catch (Exception e) {
        System.out.println("Failed to parse JSON body: " + rawBody);
      }
    } else {
      System.out.println("Raw Body (Non-JSON): " + rawBody);
    }
  }
  private boolean isJsonString(String input) {
    input = input.trim();
    if ((input.startsWith("{") && input.endsWith("}")) || (input.startsWith("[") && input.endsWith("]"))) {
      try {
        objectMapper.readTree(input);
        return true;
      } catch (Exception e) {
        return false;
      }
    }
    return false;
  }
}

✅ 2. CachedBodyHttpServletRequest – Wrapper to Cache Body

To read the request body multiple times (e.g., in AOP), you must wrap the request with a caching mechanism.

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
  
  private final byte[] cachedBody;
  
  public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
    super(request);
    InputStream requestInputStream = request.getInputStream();
    this.cachedBody = requestInputStream.readAllBytes();
  }
  
  @Override
  public ServletInputStream getInputStream() {
    return new CachedServletInputStream(this.cachedBody);
  }
  
  @Override
  public BufferedReader getReader() {
    return new BufferedReader(new InputStreamReader(getInputStream()));
  }
  
  public String getCachedBodyAsString() {
    return new String(this.cachedBody);
  }
  
  private static class CachedServletInputStream extends ServletInputStream {
    private final ByteArrayInputStream buffer;
    
    public CachedServletInputStream(byte[] body) {
      this.buffer = new ByteArrayInputStream(body);
    }
    
    @Override
    public boolean isFinished() {
      return buffer.available() == 0;
    }
    
    @Override
    public boolean isReady() {
      return true;
    }
    
    @Override
    public void setReadListener(ReadListener readListener) {
    }
    
    @Override
    public int read() {
      return buffer.read();
    }
  }
}

✅ 3. Filter to Wrap Incoming Requests

Register a filter that replaces the request with the wrapper:

@Component
public class CachedBodyFilter implements Filter {
  
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    if (request instanceof HttpServletRequest httpRequest) {
      CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(httpRequest);
      wrappedRequest.setAttribute("cachedRequestBody", wrappedRequest.getCachedBodyAsString());
      chain.doFilter(wrappedRequest, response);
    } else {
      chain.doFilter(request, response);
    }
  }
}

🧩
Aspect for Capturing HTTP Requests, Responses, Controller Metadata, and Execution Time

Captures full details of incoming HTTP requests and outgoing responses, including controller class, method, query parameters, request body, and execution time. Useful for debugging, auditing, and performance analysis.

===== Endpoint Execution Trace ===== Thread: http-nio-8080-exec-1
  → Controller: com.example.controller.MyController
  → Method: createSomething(param1=123, param2=abc)
  → HTTP Method: POST
  → URL: http://localhost:8080/myworld/api/v1/create?region=TMPL
  → Query Parameters:
     - region: TMPL
  → Headers:
     - Content-Type: application/json
     - User-Agent: PostmanRuntime/7.32.2
  → Cookies:
     - sessionId: abc123xyz
  → Request Body Minified: {"id":123,"name":"John"}
  → Response: {"status":"success","message":"Created"}
  → Duration: 42 ms
====================================
@Aspect
@Component
@EnableAspectJAutoProxy
public class EndpointMonitorAspect {
  
  private final ObjectMapper objectMapper = new ObjectMapper();
  
  @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
      "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.PathVariable)" +
      "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
      "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
      )
  //@Around("execution(* com.yourpackage.controller..*(..))")
  public Object logEndpointExecution(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    String threadName = Thread.currentThread().getName();
    
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes != null ? attributes.getRequest() : null;
    
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String className = methodSignature.getDeclaringType().getName();
    String methodName = methodSignature.getName();
    String[] paramNames = methodSignature.getParameterNames();
    Object[] paramValues = joinPoint.getArgs();
    
    StringBuilder methodParams = new StringBuilder();
    IntStream.range(0, paramNames.length).forEach(i -> {
      methodParams.append(paramNames[i]).append("=");
      methodParams.append(paramValues[i]);
      if (i < paramNames.length - 1) methodParams.append(", ");
    });
    
    Object response = null;
    Throwable error = null;
    
    try {
      response = joinPoint.proceed();
      return response;
    } catch (Throwable ex) {
      error = ex;
      throw ex;
    } finally {
      long duration = System.currentTimeMillis() - startTime;
      
      System.out.println("===== Endpoint Execution Trace ===== Thread: " + threadName);
      System.out.println("  → Controller: " + className);
      System.out.println("  → Method: " + methodName + "(" + methodParams + ")");
      
      if (request != null) {
        System.out.println("  → HTTP Method: " + request.getMethod());
        System.out.println("  → URL: " + request.getRequestURL() +
            (request.getQueryString() != null ? "?" + request.getQueryString() : ""));
        
        // Query parameters
        Enumeration<String> paramNamesEnum = request.getParameterNames();
        if (paramNamesEnum.hasMoreElements()) {
          System.out.println("  → Query Parameters:");
          while (paramNamesEnum.hasMoreElements()) {
            String name = paramNamesEnum.nextElement();
            System.out.println("     - " + name + ": " + request.getParameter(name));
          }
        }
        
        // Headers
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames.hasMoreElements()) {
          System.out.println("  → Headers:");
          while (headerNames.hasMoreElements()) {
            String header = headerNames.nextElement();
            System.out.println("     - " + header + ": " + request.getHeader(header));
          }
        }
        
        // Cookies
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
          System.out.println("  → Cookies:");
          for (Cookie cookie : cookies) {
            System.out.println("     - " + cookie.getName() + ": " + cookie.getValue());
          }
        }
        
        // Request body (cached from filter)
        String rawBody = (String) request.getAttribute("cachedRequestBody");
        if (rawBody != null && !rawBody.isBlank()) {
          if (isJsonString(rawBody)) {
            String minified = objectMapper.writeValueAsString(
                objectMapper.readValue(rawBody, Object.class));
            System.out.println("  → Request Body Minified: " + minified);
          } else {
            System.out.println("  → Request Body: " + rawBody);
          }
        }
      }
      
      // Response
      if (response != null) {
        if (isJsonString(response.toString())) {
          String minified = objectMapper.writeValueAsString(
              objectMapper.readValue(response.toString(), Object.class));
          System.out.println("  → Response: " + minified);
        } else {
          System.out.println("  → Response: " + response);
        }
      } else if (error != null) {
        System.out.println("  → Response: Exception - " + error.getClass().getSimpleName() + ": " + error.getMessage());
      } else {
        System.out.println("  → Response: null");
      }
      
      System.out.println("  → Duration: " + duration + " ms");
      System.out.println("====================================");
    }
  }
  
  private boolean isJsonString(String input) {
    input = input.trim();
    if ((input.startsWith("{") && input.endsWith("}")) || (input.startsWith("[") && input.endsWith("]"))) {
      try {
        objectMapper.readTree(input);
        return true;
      } catch (Exception e) {
        return false;
      }
    }
    return false;
  }
}

⚠️ **GitHub.com Fallback** ⚠️