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
, ortransaction 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
orexpression
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., inframeworks 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 |
---|---|---|---|---|---|
![]() ![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
📘 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 (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 forquick
,targeted
advice when you don't plan to reuse the expression. - Use
external (named)
pointcuts formaintainability
,reusability
, andclarity
—especially 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() { ... } |
|
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() { ... } |
|
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) { ... } |
|
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) { ... } |
|
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.
📄 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) |
📦🔧🛠🧱 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.
🎯 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:
- 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.
-
Package:
- 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
.
- 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);
}
- 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.
-
Package:
- 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.
-
Package:
- 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.
- 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 specifiedcron expressions
orfixed-rate/delay
values. It manages the lifecycle of scheduled tasks within the Spring context.
-
Package:
-
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.
- 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 annotationexecution(...) : Matches method executionwithin(...) : Restrict by class/package! operator: Used for negation&&, ||, ! for fine-grained control@Around(
"@within(org.springframework.stereotype.Service) " +
"&& execution(* com.example..*.*(..)) " +
"&& !within(com.example.web..*) " +
"&& !within(@org.springframework.context.annotation.Configuration *)"
)
|
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())));
}
}
🧩
|
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) |
🧩
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
==================================== ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); getRequestAttributes() returns a RequestAttributes object (often cast to ServletRequestAttributes in web apps).
To inspect a request URL in an AOP
✅ 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);
}
}
} |
🧩
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;
}
} |