How do I use AOP in Spring for cross-cutting concerns?

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:

  • LoggingAspect
  • AuditAspect
  • PerformanceAspect
  • SecurityAspect

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.