In a Spring application, some logic does not belong to just one business feature. For example:
- Logging method calls
- Measuring execution time
- Checking security rules
- Managing transactions
- Auditing user actions
- Handling repeated exception logic
These are called cross-cutting concerns because they “cut across” many parts of your application.
Instead of copying the same logging, auditing, or timing code into many services, Spring allows you to separate that logic using AOP, or Aspect-Oriented Programming.
What Is AOP?
AOP, or Aspect-Oriented Programming, is a programming technique that lets you apply reusable behavior around your normal application logic.
In Spring, AOP is commonly used to run extra code:
- Before a method runs
- After a method finishes
- After a method throws an exception
- Around the entire method execution
For example, instead of writing logging code inside every service method:
public void createOrder() {
System.out.println("Creating order...");
// business logic
}
You can define the logging behavior once in an aspect, and Spring applies it automatically to matching methods.
Common AOP Terms
Before writing code, it helps to understand a few important AOP terms.
| Term | Meaning |
|---|---|
| Aspect | A class that contains cross-cutting logic |
| Advice | The action that runs, such as before or after a method |
| Join Point | A point during program execution, usually a method call |
| Pointcut | An expression that selects which methods the advice applies to |
| Target Object | The Spring bean being advised |
| Proxy | The object Spring creates to wrap the original bean and apply the aspect |
In Spring AOP, join points are usually method executions on Spring-managed beans.
Example Scenario
Suppose we have a service that handles orders.
package com.example.demo.order;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public void createOrder(String productName) {
System.out.println("Creating order for: " + productName);
}
public void cancelOrder(Long orderId) {
System.out.println("Cancelling order: " + orderId);
}
}
We want to log whenever service methods are called, but we do not want to put logging code inside every method.
This is a perfect use case for Spring AOP.
Adding the Spring AOP Dependency
If you are using Maven with Spring Boot, add spring-boot-starter-aop.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
This starter includes Spring AOP and AspectJ annotation support.
If you are not using Spring Boot, you typically need Spring AOP and AspectJ Weaver dependencies manually.
Creating a Simple Aspect
An aspect is a Spring bean annotated with @Aspect.
package com.example.demo.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.demo.order.*.*(..))")
public void logBeforeMethodCall(JoinPoint joinPoint) {
System.out.println("Calling method: " + joinPoint.getSignature().getName());
}
}
This aspect says:
Before executing any method in
com.example.demo.order, print the method name.
The important part is this expression:
execution(* com.example.demo.order.*.*(..))
This is called a pointcut expression.
Understanding the Pointcut Expression
The expression:
execution(* com.example.demo.order.*.*(..))
can be read as:
| Part | Meaning |
|---|---|
execution |
Match method execution |
* |
Any return type |
com.example.demo.order.* |
Any class in this package |
.* |
Any method name |
(..) |
Any number of parameters |
So it matches methods such as:
OrderService.createOrder(String productName)
OrderService.cancelOrder(Long orderId)
Running the Service
You can call the service from a controller, command-line runner, or another Spring bean.
package com.example.demo;
import com.example.demo.order.OrderService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DemoRunner implements CommandLineRunner {
private final OrderService orderService;
public DemoRunner(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void run(String... args) {
orderService.createOrder("Laptop");
orderService.cancelOrder(1001L);
}
}
Example output:
Calling method: createOrder
Creating order for: Laptop
Calling method: cancelOrder
Cancelling order: 1001
The OrderService class does not contain logging logic, but logging still happens.
That is the main benefit of AOP.
Types of Advice in Spring AOP
Spring AOP provides several advice annotations.
@Before
Runs before the matched method.
@Before("execution(* com.example.demo.order.*.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
System.out.println("Before: " + joinPoint.getSignature().getName());
}
Use this for:
- Logging before execution
- Security checks
- Validating method arguments
@After
Runs after the method finishes, whether it succeeds or throws an exception.
@After("execution(* com.example.demo.order.*.*(..))")
public void afterMethod(JoinPoint joinPoint) {
System.out.println("After: " + joinPoint.getSignature().getName());
}
Use this for cleanup logic.
@AfterReturning
Runs only when the method completes successfully.
@AfterReturning(
pointcut = "execution(* com.example.demo.order.*.*(..))",
returning = "result"
)
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Method returned successfully: " + joinPoint.getSignature().getName());
System.out.println("Result: " + result);
}
Example service method:
public String findOrderStatus(Long orderId) {
return "PROCESSING";
}
@AfterReturning can access the return value.
@AfterThrowing
Runs only when the method throws an exception.
@AfterThrowing(
pointcut = "execution(* com.example.demo.order.*.*(..))",
throwing = "exception"
)
public void afterThrowing(JoinPoint joinPoint, Exception exception) {
System.out.println("Method failed: " + joinPoint.getSignature().getName());
System.out.println("Exception: " + exception.getMessage());
}
Use this for:
- Error logging
- Auditing failed operations
- Sending failure metrics
@Around
@Around is the most powerful advice type. It wraps the method execution completely.
It can:
- Run code before the method
- Run code after the method
- Change arguments
- Change the return value
- Prevent the method from running
- Measure execution time
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.demo.order.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long end = System.nanoTime();
long durationInMillis = (end - start) / 1_000_000;
System.out.println(
joinPoint.getSignature().getName()
+ " executed in "
+ durationInMillis
+ " ms"
);
}
}
}
The key method here is:
joinPoint.proceed();
This tells Spring to continue and execute the original target method.
If you do not call proceed(), the original method will not run.
Reusing Pointcuts
If you use the same pointcut expression in multiple advice methods, it is better to define it once.
package com.example.demo.aop;
import org.aspectj.lang.annotation.Pointcut;
public class CommonPointcuts {
@Pointcut("execution(* com.example.demo.order.*.*(..))")
public void orderServiceMethods() {
}
}
Then use it in your aspects:
package com.example.demo.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("com.example.demo.aop.CommonPointcuts.orderServiceMethods()")
public void logBeforeMethodCall(JoinPoint joinPoint) {
System.out.println("Calling: " + joinPoint.getSignature().getName());
}
}
This makes your code easier to maintain.
Matching Methods by Annotation
A very common and clean approach is to create a custom annotation and apply AOP only to methods annotated with it.
For example, create an annotation named @Auditable.
package com.example.demo.audit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
String action();
}
Now annotate a service method:
package com.example.demo.order;
import com.example.demo.audit.Auditable;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Auditable(action = "CREATE_ORDER")
public void createOrder(String productName) {
System.out.println("Creating order for: " + productName);
}
}
Then create an aspect that reacts to this annotation:
package com.example.demo.audit;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuditAspect {
@Before("@annotation(auditable)")
public void audit(JoinPoint joinPoint, Auditable auditable) {
System.out.println("Audit action: " + auditable.action());
System.out.println("Method: " + joinPoint.getSignature().getName());
}
}
This is often better than matching methods by package name because it is more explicit.
You can immediately see which methods are audited:
@Auditable(action = "CREATE_ORDER")
public void createOrder(String productName) {
// business logic
}
Practical Example: Logging Method Arguments
You can access method arguments using JoinPoint.
package com.example.demo.aop;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MethodArgumentLoggingAspect {
@Before("execution(* com.example.demo.order.*.*(..))")
public void logArguments(JoinPoint joinPoint) {
System.out.println("Method: " + joinPoint.getSignature().getName());
System.out.println("Arguments: " + Arrays.toString(joinPoint.getArgs()));
}
}
Example output:
Method: createOrder
Arguments: [Laptop]
Be careful when logging arguments. Do not accidentally log sensitive information such as:
- Passwords
- Access tokens
- Credit card numbers
- Personal identity information
Practical Example: Measuring Service Performance
Here is a slightly cleaner performance aspect using Java’s Duration.
package com.example.demo.aop;
import java.time.Duration;
import java.time.Instant;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ServiceTimingAspect {
@Around("execution(* com.example.demo..service..*(..))")
public Object measureServiceTime(ProceedingJoinPoint joinPoint) throws Throwable {
Instant start = Instant.now();
try {
return joinPoint.proceed();
} finally {
Duration duration = Duration.between(start, Instant.now());
System.out.println(
joinPoint.getSignature().toShortString()
+ " took "
+ duration.toMillis()
+ " ms"
);
}
}
}
This pointcut:
execution(* com.example.demo..service..*(..))
matches methods inside packages containing service.
The .. means “this package and its subpackages.”
Using AOP with Spring MVC Controllers
You can also apply AOP to controllers.
For example:
@Around("within(@org.springframework.web.bind.annotation.RestController *)")
public Object logRestControllerCalls(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("REST call: " + joinPoint.getSignature().toShortString());
return joinPoint.proceed();
}
This matches beans annotated with @RestController.
However, for HTTP request logging, a Spring MVC HandlerInterceptor or servlet filter is sometimes a better fit.
Use AOP when you want to intercept method-level application behavior.
Use filters or interceptors when you want to work directly with HTTP requests and responses.
AOP and Transactions
If you have used @Transactional, you have already used a form of AOP.
For example:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PaymentService {
@Transactional
public void processPayment(Long orderId) {
// database operations
}
}
Spring applies transaction behavior around the method call.
Conceptually, it works like this:
begin transaction
try {
processPayment(orderId)
commit transaction
} catch (Exception ex) {
rollback transaction
throw ex
}
You do not usually write this logic yourself. Spring applies it as a cross-cutting concern.
Important Limitation: Self-Invocation
Spring AOP is proxy-based. This means Spring creates a proxy object around your bean.
Because of this, AOP usually works when one Spring bean calls another Spring bean.
For example, this works:
@Service
public class OrderControllerService {
private final OrderService orderService;
public OrderControllerService(OrderService orderService) {
this.orderService = orderService;
}
public void run() {
orderService.createOrder("Keyboard");
}
}
But this may not trigger AOP:
@Service
public class OrderService {
public void createOrder(String productName) {
validateOrder(productName);
System.out.println("Creating order for: " + productName);
}
@Auditable(action = "VALIDATE_ORDER")
public void validateOrder(String productName) {
System.out.println("Validating: " + productName);
}
}
Why?
Because createOrder() calls validateOrder() directly inside the same class. The call does not go through the Spring proxy.
This is called self-invocation.
A common solution is to move the advised method to another Spring bean.
@Service
public class OrderValidationService {
@Auditable(action = "VALIDATE_ORDER")
public void validateOrder(String productName) {
System.out.println("Validating: " + productName);
}
}
Then inject it into OrderService.
@Service
public class OrderService {
private final OrderValidationService validationService;
public OrderService(OrderValidationService validationService) {
this.validationService = validationService;
}
public void createOrder(String productName) {
validationService.validateOrder(productName);
System.out.println("Creating order for: " + productName);
}
}
Now the method call goes through a Spring-managed bean, so AOP can be applied.
Best Practices for Using Spring AOP
1. Use AOP for Infrastructure Concerns
Good use cases include:
- Logging
- Auditing
- Metrics
- Tracing
- Security checks
- Transaction boundaries
- Retry handling
Avoid using AOP to hide important business rules that developers need to see clearly.
2. Prefer Annotation-Based Pointcuts for Explicit Behavior
This is clear:
@Auditable(action = "CREATE_ORDER")
public void createOrder(String productName) {
// business logic
}
This is less obvious:
@Before("execution(* com.example.demo.order.*.*(..))")
Package-based pointcuts are useful, but annotation-based pointcuts are often easier to understand in large projects.
3. Avoid Logging Sensitive Data
Be careful with this:
Arrays.toString(joinPoint.getArgs())
It may expose passwords, tokens, or personal data.
For production systems, use structured logging and sanitize sensitive values.
4. Keep Aspects Small
An aspect should focus on one concern.
For example:
LoggingAspectAuditAspectPerformanceAspectSecurityAspect
Avoid creating one large aspect that does many unrelated things.
5. Understand Proxy Behavior
Spring AOP works through proxies, so keep these in mind:
- The target class should be a Spring bean.
- Calls should usually come from outside the bean.
- Self-invocation does not usually trigger advice.
- Final classes and final methods can be problematic depending on proxy type.
Complete Example
Here is a compact working example.
Service
package com.example.demo.order;
import com.example.demo.audit.Auditable;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Auditable(action = "CREATE_ORDER")
public String createOrder(String productName) {
System.out.println("Creating order for: " + productName);
return "Order created for " + productName;
}
}
Custom Annotation
package com.example.demo.audit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
String action();
}
Audit Aspect
package com.example.demo.audit;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuditAspect {
@Before("@annotation(auditable)")
public void audit(JoinPoint joinPoint, Auditable auditable) {
System.out.println("Audit action: " + auditable.action());
System.out.println("Method: " + joinPoint.getSignature().toShortString());
}
}
Timing Aspect
package com.example.demo.aop;
import java.time.Duration;
import java.time.Instant;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimingAspect {
@Around("execution(* com.example.demo..*(..))")
public Object timeMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Instant start = Instant.now();
try {
return joinPoint.proceed();
} finally {
Duration duration = Duration.between(start, Instant.now());
System.out.println(
joinPoint.getSignature().toShortString()
+ " took "
+ duration.toMillis()
+ " ms"
);
}
}
}
Runner
package com.example.demo;
import com.example.demo.order.OrderService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DemoRunner implements CommandLineRunner {
private final OrderService orderService;
public DemoRunner(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void run(String... args) {
String result = orderService.createOrder("Laptop");
System.out.println(result);
}
}
Example output:
Audit action: CREATE_ORDER
Method: OrderService.createOrder(..)
Creating order for: Laptop
OrderService.createOrder(..) took 3 ms
Order created for Laptop
When Should You Not Use AOP?
AOP is powerful, but it should not be used everywhere.
Avoid AOP when:
- The logic is core business logic
- The behavior is hard to discover
- A simple method call would be clearer
- You need direct control over HTTP request/response details
- The aspect makes debugging confusing
For example, calculating an order discount is business logic. It should probably stay in a normal service method, not hidden inside an aspect.
Summary
Spring AOP helps you separate cross-cutting concerns from business logic.
You can use it for:
- Logging
- Auditing
- Performance monitoring
- Security checks
- Exception tracking
- Transaction-like behavior
The basic structure is:
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.demo..*(..))")
public void doSomethingBefore() {
// cross-cutting logic
}
}
The most commonly used advice types are:
| Advice | Runs When |
|---|---|
@Before |
Before the method |
@After |
After the method finishes or fails |
@AfterReturning |
After successful return |
@AfterThrowing |
After an exception |
@Around |
Around the full method execution |
For many real-world applications, annotation-based AOP is the cleanest approach because it makes the behavior explicit:
@Auditable(action = "CREATE_ORDER")
public void createOrder(String productName) {
// business logic
}
Used carefully, Spring AOP keeps your application cleaner, reduces duplication, and makes infrastructure concerns easier to manage.
