Skip to content
Spring Boot sb core 6 min read

Aspect-Oriented Programming (AOP)

Aspect-Oriented Programming (AOP) lets you factor out cross-cutting concerns — logging, timing, auditing, security, transactions — into reusable units called aspects instead of scattering that code across every method. Spring AOP applies aspects at runtime through proxies, so you add behavior around your beans without editing their bodies. This page covers aspects, join points, pointcuts, advice types, and a worked example.

Why AOP — the cross-cutting problem

Some logic does not belong to any single method but cuts across many of them. Consider audit logging: nearly every service method might need a “who called what, when” record. Copying that code into each method is repetitive, easy to forget, and clutters the real business logic.

// Without AOP — concern tangled into every method
public Order placeOrder(Order order) {
    log.info("placeOrder called by {}", currentUser());   // cross-cutting
    long start = System.nanoTime();                        // cross-cutting
    Order saved = repository.save(order);                  // actual work
    log.info("placeOrder took {}ms", elapsed(start));      // cross-cutting
    return saved;
}

AOP lets you pull the logging and timing into an aspect and declare where it applies with a pointcut, leaving placeOrder to do only its real job.

Setup — spring-boot-starter-aop

Add the starter, which pulls in Spring AOP and AspectJ annotations:

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

Spring Boot auto-enables AOP when this starter is present (@EnableAspectJAutoProxy is applied for you). No further configuration is needed to start writing aspects.

Core vocabulary

TermMeaning
AspectA class holding cross-cutting behavior (@Aspect + @Component)
Join pointA point in execution where an aspect can run — in Spring AOP, always a method execution
PointcutAn expression that selects which join points to advise
AdviceThe action taken at a join point (@Before, @Around, etc.)
WeavingLinking aspects to target objects — Spring does this at runtime via proxies

Note: Spring AOP is proxy-based and only intercepts public method executions on Spring-managed beans. It is not full AspectJ — it cannot advise field access or constructor calls. For most application concerns this is plenty.

Declaring an aspect

An aspect is a @Component annotated with @Aspect:

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    // pointcuts and advice go here
}

@Aspect marks it as carrying advice; @Component makes Spring detect and manage it. Both are required — @Aspect alone is not a stereotype.

Pointcut expressions

A pointcut tells Spring which methods to match. The two forms you will use most are execution(..) and @annotation(..).

// All public methods in any class under the service package
@Pointcut("execution(public * com.example.service..*(..))")
public void serviceLayer() {}

// Any method annotated with our custom @Loggable
@Pointcut("@annotation(com.example.aop.Loggable)")
public void loggable() {}

The execution designator reads as execution(modifiers? return-type declaring-type.method(params)):

PatternMatches
execution(* com.example..*(..))Any method in com.example or subpackages
execution(* *..*Service.*(..))Any method on a class ending in Service
@annotation(org.springframework.web.bind.annotation.GetMapping)Methods annotated @GetMapping
within(com.example.web..*)Any join point inside the web package
bean(*Repository)Methods on beans whose name ends in Repository

Define a named pointcut once with @Pointcut and reference it by method name, or inline the expression directly in an advice annotation.

Advice types

Each advice annotation runs at a different moment relative to the target method.

AdviceRunsCan change flow?
@BeforeBefore the methodNo (can throw)
@AfterAfter the method (finally — success or failure)No
@AfterReturningAfter a successful returnReads return value
@AfterThrowingAfter the method throwsReads the exception
@AroundWraps the methodYes — must call proceed()
@Aspect
@Component
public class AuditAspect {

    @Before("execution(* com.example.service..*(..))")
    public void logEntry(JoinPoint jp) {
        System.out.println("→ " + jp.getSignature().toShortString());
    }

    @AfterReturning(pointcut = "execution(* com.example.service..*(..))",
                    returning = "result")
    public void logReturn(JoinPoint jp, Object result) {
        System.out.println("← " + jp.getSignature().getName() + " = " + result);
    }

    @AfterThrowing(pointcut = "execution(* com.example.service..*(..))",
                   throwing = "ex")
    public void logError(JoinPoint jp, Throwable ex) {
        System.out.println("✗ " + jp.getSignature().getName() + " threw " + ex);
    }
}

@Around is the most powerful — it receives a ProceedingJoinPoint and must call proceed() to invoke the target. Forgetting to call proceed() silently skips the real method.

Worked example — a @Loggable timing aspect

Combine a custom annotation with an @Around advice to time and log any method you tag.

First, the marker annotation:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";   // optional label
}

Then the aspect that advises any method carrying it:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class LoggableAspect {

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

    // binds the matched annotation instance as the 'loggable' parameter
    @Around("@annotation(loggable)")
    public Object logAround(ProceedingJoinPoint pjp, Loggable loggable) throws Throwable {
        MethodSignature sig = (MethodSignature) pjp.getSignature();
        String label = loggable.value().isBlank() ? sig.getName() : loggable.value();

        long start = System.nanoTime();
        log.info("▶ {} args={}", label, java.util.Arrays.toString(pjp.getArgs()));
        try {
            Object result = pjp.proceed();             // invoke the real method
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("✔ {} returned in {}ms", label, ms);
            return result;
        } catch (Throwable ex) {
            log.error("✘ {} failed: {}", label, ex.getMessage());
            throw ex;                                  // re-throw to preserve semantics
        }
    }
}

Tag a method and the aspect does the rest:

@Service
public class OrderService {

    @Loggable("place-order")
    public Order placeOrder(Order order) {
        return repository.save(order);
    }
}

Output:

INFO  LoggableAspect : ▶ place-order args=[Order[id=null, total=49.90]]
INFO  LoggableAspect : ✔ place-order returned in 12ms

Tip: Bind the annotation directly as a parameter (@annotation(loggable) plus a Loggable loggable argument). You get type-safe access to attributes like loggable.value() without reflection.

Ordering multiple aspects

When several aspects match the same method, control their nesting with @Order (or by implementing Ordered). A lower number has higher precedence and wraps the others on the outside.

@Aspect @Component @Order(1)  // outermost — runs first on the way in
public class SecurityAspect { /* ... */ }

@Aspect @Component @Order(2)  // inner — runs after security
public class LoggableAspect { /* ... */ }

For @Around advice the higher-precedence aspect’s proceed() invokes the next aspect, producing a clean onion of wrappers around the target.

Proxy limitations — read this before debugging

Because Spring AOP works through proxies, a few things silently do not get advised:

  • Self-invocation: a method calling another advised method on this bypasses the proxy, so the advice does not run. Move the advised method to another bean, or inject a proxy reference of self.
  • private and final methods: cannot be overridden by the proxy, so they cannot be advised. CGLIB also cannot subclass final classes.
  • static methods: never advised — there is no instance proxy involved.
  • Only beans managed by Spring are eligible; new-ed objects get no aspects.
@Service
public class ReportService {

    public void runAll() {
        for (Long id : ids()) {
            build(id);     // BUG: internal call — @Loggable does NOT fire
        }
    }

    @Loggable
    public void build(Long id) { /* ... */ }
}

Warning: Self-invocation silently disabling advice (including @Transactional) is the single most common AOP surprise. If an aspect “isn’t running,” check whether the target is being called from inside the same class.

This is the same proxy machinery described in the Proxy Pattern & AOP page, and it is exactly how Transactions (@Transactional) are implemented.

Last updated June 13, 2026
Was this helpful?