00:00

Spring Boot AOP (Aspect-Oriented Programming)

Last updated:

Some code is needed in many places but has nothing to do with the business logic around it: logging, timing, security checks, auditing. Copying it into every method makes the code long, repetitive and easy to get wrong. Aspect-Oriented Programming (AOP) lets you write that code once and apply it automatically wherever it is needed.

This tutorial explains the core AOP concepts, how Spring applies aspects through proxies, every type of advice, pointcut expressions, custom annotations, real-world examples, testing and the limitations to watch out for.

1. What Is AOP?

Imagine you want to log the start, end and duration of every service method. Without AOP, each method carries the same extra lines:

OrderService.java (without AOP)
public Order placeOrder(OrderRequest request) {
    log.info("placeOrder started");
    long start = System.currentTimeMillis();
    try {
        // the actual business logic: 3 lines
        Order order = Order.from(request);
        inventory.reserve(order);
        return orderRepository.save(order);
    } finally {
        log.info("placeOrder took {} ms", System.currentTimeMillis() - start);
    }
}

public void cancelOrder(Long id) {
    log.info("cancelOrder started");
    long start = System.currentTimeMillis();
    // ... the same lines again, in every method
}

The logging and timing are cross-cutting concerns: they cut across many classes, but do not belong to any of them. AOP moves them into one separate class, called an aspect, and Spring applies it automatically. The service then contains only business logic:

OrderService.java (with AOP)
public Order placeOrder(OrderRequest request) {
    Order order = Order.from(request);
    inventory.reserve(order);
    return orderRepository.save(order);
}

2. Why Use AOP? Common Use Cases

  • No duplicated code: the concern is written once.
  • Cleaner business logic: methods show only what they really do.
  • Consistency: every matching method gets exactly the same behavior.
  • Easy to change: update or remove the concern in one place.
Use caseWhat the aspect does
Logging and tracingLog method calls, arguments and results
Performance monitoringMeasure and record how long methods take
AuditingRecord who did what, and when
SecurityCheck permissions before a method runs
RetryRepeat a call that failed with a temporary error
Error translationConvert technical exceptions into business ones

You already use AOP every day: Spring itself implements @Transactional, @Cacheable, @Async and method security with exactly this technique.

3. Core AOP Concepts

TermMeaningIn the logging example
AspectA class that holds cross-cutting logic, marked @AspectLoggingAspect
Join pointA point in the program where an aspect can run; in Spring AOP, always a method executionThe call to placeOrder()
PointcutAn expression that selects which join points to apply the aspect to“Every public method in the service package”
AdviceThe code that runs, and when: before, after or around the methodLog the start time, then the duration
TargetThe original object whose methods are advisedThe real OrderService
ProxyThe wrapper Spring creates around the targetWhat other beans actually call
WeavingCombining aspects with the target codeSpring does it at runtime, with proxies

A simple way to remember it: the pointcut says where, the advice says what and when, and the aspect is the class that holds both.

4. How Spring AOP Works

Spring AOP works with proxies. When a bean matches an aspect’s pointcut, Spring does not hand out the bean itself. It creates a proxy object around it and injects that instead:

Calling an advised method
OrderController ──▶ Proxy of OrderService ──▶ real OrderService.placeOrder()
                     │ before advice runs        │
                     │                           │ business logic runs
                     │ after advice runs  ◀──────┘
Proxy typeUsed whenNotes
CGLIB (subclass)Default in Spring BootCreates a subclass of your class at runtime; cannot advise final classes or methods
JDK dynamic proxyWhen configured with spring.aop.proxy-target-class=false and the bean implements an interfaceOnly the interface methods are advised

Because everything goes through the proxy, an aspect only runs when a method is called from another bean. This is the key to understanding most AOP surprises, covered in section 14.

5. Enabling AOP in Spring Boot

Add the AOP starter. It brings AspectJ’s annotations and switches Spring’s AOP support on automatically:

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

In Spring Boot 4, this starter is named spring-boot-starter-aspectj. You do not need @EnableAspectJAutoProxy; Spring Boot’s auto-configuration enables it for you.

6. Your First Aspect

An aspect is a normal Spring bean with @Aspect. This one logs every public method call in the service package, with its arguments:

LoggingAspect.java
package com.javabytechie.shop.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Before("execution(public * com.javabytechie.shop.service..*(..))")
    public void logMethodCall(JoinPoint joinPoint) {
        log.info("Calling {} with arguments {}",
                joinPoint.getSignature().toShortString(),
                Arrays.toString(joinPoint.getArgs()));
    }
}
Console
Calling OrderService.placeOrder(..) with arguments [OrderRequest[customerId=7, items=[BOOK-1]]]

Both annotations are needed: @Aspect marks the class as an aspect, and @Component makes it a Spring bean. Without @Component, Spring never finds the aspect. The JoinPoint parameter gives access to details of the current call, such as the method signature and the arguments.

7. The Five Types of Advice

AdviceRunsCan stop or change the call?
@BeforeBefore the methodOnly by throwing an exception
@AfterReturningAfter the method returns normallyCan read the return value
@AfterThrowingAfter the method throws an exceptionCan read the exception
@AfterAfter the method, whether it returned or threw (like finally)No
@AroundBefore and after; it decides whether the method runs at allYes: arguments, result and exceptions

This aspect uses all five on the same methods, so you can see exactly when each one runs:

OrderEventsAspect.java
@Aspect
@Component
public class OrderEventsAspect {

    private static final Logger log = LoggerFactory.getLogger(OrderEventsAspect.class);

    private static final String ORDER_METHODS = "execution(* com.javabytechie.shop.service.OrderService.*(..))";

    @Before(ORDER_METHODS)
    public void before(JoinPoint jp) {
        log.info("1. Before {}", jp.getSignature().getName());
    }

    @AfterReturning(pointcut = ORDER_METHODS, returning = "result")
    public void afterReturning(JoinPoint jp, Object result) {
        log.info("2a. {} returned {}", jp.getSignature().getName(), result);
    }

    @AfterThrowing(pointcut = ORDER_METHODS, throwing = "ex")
    public void afterThrowing(JoinPoint jp, Exception ex) {
        log.warn("2b. {} failed: {}", jp.getSignature().getName(), ex.getMessage());
    }

    @After(ORDER_METHODS)
    public void after(JoinPoint jp) {
        log.info("3. After {} (always runs)", jp.getSignature().getName());
    }
}

The names in returning = "result" and throwing = "ex" must match the advice method’s parameter names. Their types also act as filters: Exception ex matches only methods that throw an Exception, and a more specific type would narrow it further.

8. @Around Advice in Depth

@Around is the most powerful advice: it wraps the method call completely. The advice receives a ProceedingJoinPoint and decides when, or whether, to call proceed():

PerformanceAspect.java
@Aspect
@Component
public class PerformanceAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);

    @Around("execution(public * com.javabytechie.shop.service..*(..))")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            return joinPoint.proceed();                   // run the real method
        } finally {
            long millis = (System.nanoTime() - start) / 1_000_000;
            if (millis > 500) {
                log.warn("SLOW: {} took {} ms", joinPoint.getSignature().toShortString(), millis);
            } else {
                log.debug("{} took {} ms", joinPoint.getSignature().toShortString(), millis);
            }
        }
    }
}

Three rules for @Around:

  1. Call proceed(), or the real method never runs.
  2. Return its result, or the caller receives null.
  3. Let exceptions pass through with throws Throwable, unless you deliberately handle them.

Use the simplest advice that does the job. If you only need to act before a method, @Before is clearer and cannot accidentally skip the call.

9. Pointcut Expressions

Pointcut expressions decide which methods an advice applies to. The most common designator is execution:

Example
execution(public * com.javabytechie.shop.service..*.*(..))
PartMeaning
publicVisibility (optional; leave it out to match any)
*Any return type
com.javabytechie.shop.service..*Any class in the service package or its sub-packages (..)
.*Any method name
(..)Any number and type of parameters
ExpressionMatches
execution(* com.javabytechie.shop.service.*.*(..))Every method of classes directly in the service package
execution(* com.javabytechie.shop..*(..))Every method in shop and all its sub-packages
execution(* *..OrderService.place*(..))Methods starting with place in any OrderService
execution(* *(String, ..))Methods whose first parameter is a String
within(com.javabytechie.shop.web..*)Every method of classes in the web packages
@annotation(com.javabytechie.shop.aop.Audited)Methods annotated with @Audited
@within(org.springframework.stereotype.Service)Every method of classes annotated with @Service
bean(*Service)Beans whose names end with Service (Spring-only)

9.1 Reusable and combined pointcuts

Name pointcuts with @Pointcut, then combine them with &&, || and !:

ShopPointcuts.java
@Aspect
@Component
public class ShopPointcuts {

    @Pointcut("within(com.javabytechie.shop.service..*)")
    public void serviceLayer() {}

    @Pointcut("execution(* *..*Repository.*(..))")
    public void repositoryMethods() {}

    @Pointcut("execution(* get*(..)) || execution(* find*(..))")
    public void readMethods() {}

    @Before("serviceLayer() && !readMethods()")
    public void beforeWrite(JoinPoint jp) {
        // runs for service methods that are NOT reads
    }
}

Named pointcuts make expressions readable and keep them in one place. To use them from another aspect, refer to them by their full name, such as com.javabytechie.shop.aop.ShopPointcuts.serviceLayer().

10. AOP with Custom Annotations

Matching methods by package is broad. A custom annotation lets developers choose exactly which methods get the behavior, and even pass settings to the aspect:

LogExecutionTime.java
package com.javabytechie.shop.aop;

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 LogExecutionTime {

    long warnAboveMillis() default 500;
}
ExecutionTimeAspect.java
@Aspect
@Component
public class ExecutionTimeAspect {

    private static final Logger log = LoggerFactory.getLogger(ExecutionTimeAspect.class);

    @Around("@annotation(logExecutionTime)")
    public Object measure(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long millis = System.currentTimeMillis() - start;
            if (millis > logExecutionTime.warnAboveMillis()) {
                log.warn("{} took {} ms", joinPoint.getSignature().toShortString(), millis);
            }
        }
    }
}
ReportService.java
@Service
public class ReportService {

    @LogExecutionTime(warnAboveMillis = 2000)
    public Report buildMonthlyReport(YearMonth month) {
        // ...
    }
}

Writing @annotation(logExecutionTime) with a lowercase name binds the annotation to the advice parameter of the same name, so the aspect can read its settings, here warnAboveMillis. The parameter’s type tells Spring which annotation to look for. If you do not need the settings, use the fully qualified name instead, such as @annotation(com.javabytechie.shop.aop.LogExecutionTime).

11. Real-World Examples

11.1 Audit logging

Record who performed important actions, using the logged-in user from Spring Security:

Audited.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
    String action();
}
AuditAspect.java
@Aspect
@Component
public class AuditAspect {

    private final AuditRepository auditRepository;

    public AuditAspect(AuditRepository auditRepository) {
        this.auditRepository = auditRepository;
    }

    @AfterReturning("@annotation(audited)")
    public void record(JoinPoint joinPoint, Audited audited) {
        String user = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
                .map(Authentication::getName)
                .orElse("anonymous");
        auditRepository.save(new AuditEntry(user, audited.action(), Instant.now()));
    }
}
OrderService.java
@Audited(action = "CANCEL_ORDER")
public void cancelOrder(Long orderId) { ... }

Using @AfterReturning means only successful actions are recorded.

11.2 Retrying temporary failures

Repeat a call a few times when a dependency briefly fails:

RetryOnFailure.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryOnFailure {
    int attempts() default 3;
    long delayMillis() default 200;
}
RetryAspect.java
@Aspect
@Component
public class RetryAspect {

    private static final Logger log = LoggerFactory.getLogger(RetryAspect.class);

    @Around("@annotation(retry)")
    public Object retry(ProceedingJoinPoint joinPoint, RetryOnFailure retry) throws Throwable {
        for (int attempt = 1; ; attempt++) {
            try {
                return joinPoint.proceed();
            } catch (ResourceAccessException ex) {           // only temporary network errors
                if (attempt >= retry.attempts()) {
                    throw ex;
                }
                log.warn("Attempt {} of {} failed, retrying", attempt, retry.attempts());
                Thread.sleep(retry.delayMillis());
            }
        }
    }
}

This shows how aspects work, and is fine for simple needs. For production retries with backoff, metrics and circuit breakers, prefer a dedicated library such as Resilience4j, described in the Retry Pattern tutorial.

12. Ordering Multiple Aspects

When several aspects apply to the same method, control their order with @Order. A lower number means higher priority, so that aspect runs first on the way in and last on the way out:

Aspect order
@Aspect
@Component
@Order(1)        // outermost: sees the total time, including the other aspects
public class PerformanceAspect { ... }

@Aspect
@Component
@Order(2)
public class AuditAspect { ... }
Execution order
PerformanceAspect (before) ─▶ AuditAspect (before) ─▶ method
PerformanceAspect (after)  ◀─ AuditAspect (after)  ◀─┘

Without @Order, the order between aspects is undefined and may change between runs. Within one aspect, advice types run in a fixed order: @Around, @Before, then after the method @After, @AfterReturning or @AfterThrowing.

13. Testing an Aspect

Test an aspect without starting Spring, using AspectJProxyFactory, which builds the same kind of proxy Spring would:

RetryAspectTest.java
class RetryAspectTest {

    static class FlakyClient {
        int calls = 0;

        @RetryOnFailure(attempts = 3, delayMillis = 1)
        public String fetch() {
            calls++;
            if (calls < 3) {
                throw new ResourceAccessException("temporary network error");
            }
            return "ok";
        }
    }

    @Test
    void retriesUntilTheCallSucceeds() {
        FlakyClient target = new FlakyClient();
        AspectJProxyFactory factory = new AspectJProxyFactory(target);
        factory.setProxyTargetClass(true);
        factory.addAspect(new RetryAspect());
        FlakyClient proxy = factory.getProxy();

        assertThat(proxy.fetch()).isEqualTo("ok");
        assertThat(target.calls).isEqualTo(3);
    }
}

For aspects that depend on other beans, such as AuditAspect with its repository, a @SpringBootTest that calls an annotated method and checks the side effect works well.

14. Limitations of Spring AOP

LimitationWhyWhat to do
Calls within the same class are not advisedThe call does not go through the proxyMove the method to another bean
private methods are not advisedProxies cannot intercept themUse public methods for advised behavior
final classes and methods are not advised (CGLIB)A subclass cannot override themRemove final, or use interfaces
Only Spring beans are advisedSpring only creates proxies for beansLet Spring manage the object, or use AspectJ weaving
Only method executions are join pointsProxies wrap method calls onlyUse AspectJ for field access or constructors
static methods are not advisedThey belong to the class, not an objectUse instance methods

15. Spring AOP vs AspectJ

Spring AOPAspectJ
How it worksRuntime proxiesWeaves bytecode at compile time or class-load time
Join pointsMethod executions on Spring beansMethods, constructors, field access, static methods and more
Self-invocationNot advisedAdvised
SetupOne starter, nothing elseCompiler plugin or a Java agent
PerformanceSmall per-call overheadClose to hand-written code
Best forMost Spring applicationsAdvanced needs beyond method calls

Spring AOP uses AspectJ’s annotations and pointcut language, but not its weaving. Start with Spring AOP; switch to full AspectJ only when you hit a limitation you cannot design around.

16. Best Practices

  • Use AOP for true cross-cutting concerns, not for business logic, which should stay visible in the code.
  • Prefer custom annotations over broad package pointcuts, so it is obvious which methods are affected.
  • Choose the simplest advice: @Before or @AfterReturning before reaching for @Around.
  • Keep advice fast; it runs on every matching call.
  • Never log sensitive arguments such as passwords or card numbers.
  • Set @Order when several aspects can apply to the same method.
  • Test your aspects, including that they do not apply where they should not.
  • Use a logger, never System.out.println.

17. Summary

AOP moves cross-cutting concerns, such as logging, timing, auditing and retries, out of your business code and into aspects. In Spring Boot, add the AOP starter, write a class with @Aspect and @Component, choose where with a pointcut and when with one of the five advice types. Spring applies it through proxies, which is why only public calls between beans are advised. Custom annotations, named pointcuts, @Order and tests keep aspects clear and predictable, and full AspectJ is there for the rare cases that need more.