Spring Study Guide – AOP

August 6, 20266 min readUpdated 8/18/2026

Some behaviour is wanted in forty methods and belongs to none of them: timing, audit logging, security checks, transaction boundaries. AOP moves it into one place.

This is also the topic that explains the most about Spring generally, because @Transactional, @Cacheable, @Async, @Retryable and @PreAuthorize are all AOP underneath and all fail in the same way.

The concept

What is AOP?

Aspect Oriented Programming: separating cross-cutting concerns from business logic by declaring them once, in an aspect, and having the framework apply them to the methods you nominate.

What is a cross-cutting concern?

A requirement that cuts across many modules and does not belong to any of them. Logging, security, transactions, caching, monitoring, retries.

What two problems does AOP solve?

  • Tangling — business logic and plumbing interleaved in the same method, so neither reads clearly.
  • Scattering — the same plumbing duplicated across many modules, so changing it means finding every copy, and the one you miss is a bug.

Without AOP: a stopwatch and a try/finally copied into forty methods, and the forty-first is the one somebody forgot.

The vocabulary

You are expected to use these precisely.

TermWhat it is
Join pointA point in execution where advice could apply. In Spring AOP, always a method call.
PointcutAn expression selecting which join points.
AdviceThe code that runs at a selected join point.
AspectA class holding pointcuts and advice together.
TargetThe real bean being advised.
WeavingWiring the advice to the target. Spring does it at runtime, with proxies.

The distinction that gets tested: a join point is any point where advice could run; a pointcut is the expression that says where it does.

All of it in one class

@Slf4j
@Aspect
@Component
public class ServiceTimingAspect {

    private static final long SLOW_CALL_MILLIS = 250;

    // Named pointcuts are worth three extra lines: the expression is declared once and
    // the advice below reads as prose instead of as a regex.
    @Pointcut("execution(* com.pizza.api.entity..*ServiceImpl.*(..))")
    public void serviceLayer() {}

    @Pointcut("execution(* com.pizza.api.report.*ServiceImpl.*(..))")
    public void reportLayer() {}

    @Around("serviceLayer() || reportLayer()")
    public Object time(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        String target = joinPoint.getSignature().toShortString();

        try {
            return joinPoint.proceed();
        } catch (Throwable ex) {
            long millis = (System.nanoTime() - start) / 1_000_000;
            log.warn("{} failed after {} ms: {}", target, millis, ex.toString());
            throw ex;   // NEVER swallow it
        } finally {
            long millis = (System.nanoTime() - start) / 1_000_000;
            if (millis >= SLOW_CALL_MILLIS) {
                log.warn("SLOW {} took {} ms", target, millis);
            }
        }
    }
}

Note what the advice does not do: it never swallows the exception. An aspect that quietly turns a failure into a null is how observability code becomes the bug.

Enabling it

What do you have to do to have @Aspect detected?

Two things: the class must be a Spring bean (hence @Component), and auto-proxying must be on. In plain Spring that is @EnableAspectJAutoProxy; in Spring Boot the starter auto-configures it and you write nothing.

<!-- NOTE THE NAME. Boot 3 called this spring-boot-starter-aop.
     Boot 4 renamed it, and the old name is not in the BOM at all - so an
     unchanged Boot 3 pom fails with "'dependencies.dependency.version' is
     missing", an error that never mentions the rename. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>

The five advice types

AdviceRunsCan it stop the call?
@Beforebefore the methodonly by throwing
@AfterReturningafter a normal return; sees the valueno
@AfterThrowingonly when it throws; sees the exceptionno
@Afteralways, like finallyno
@Aroundwraps the whole callyes

Which two can you use to handle exceptions?

@AfterThrowing to observe one, and @Around to actually catch it. Only @Around can swallow or replace an exception, because only @Around controls whether proceed() is called and what happens afterwards.

What is the ordering when several aspects match?

Unspecified unless you say. Add @Order — lower runs first on the way in, last on the way out.

Reading pointcut expressions

execution(* com.pizza.api.entity..*ServiceImpl.*(..))

Left to right: any return type, in package com.pizza.api.entity or below (that is what .. means between packages), in a class whose name ends ServiceImpl, any method name, any arguments (that is what (..) means).

// public methods on classes annotated @Service
execution(public * *(..)) && within(@org.springframework.stereotype.Service *)

// every method in one package, exactly - no sub-packages
execution(* com.pizza.api.report.*.*(..))

// any method annotated @Transactional
@annotation(org.springframework.transaction.annotation.Transactional)

// beans whose NAME ends in Service - Spring-specific, not AspectJ
bean(*Service)

Combine with &&, || and !.

JoinPoint and ProceedingJoinPoint

What is JoinPoint for?

Reflective access to the call being advised: the signature, the arguments, the target object. Any advice type may declare it as its first parameter.

What is a ProceedingJoinPoint, and when is it used?

JoinPoint plus proceed(). Only @Around gets one, and it is what makes @Around different: until you call proceed() the target method has not run, and you may decline to call it at all.

@Around("serviceLayer()")
public Object advise(ProceedingJoinPoint pjp) throws Throwable {
    String method = pjp.getSignature().toShortString();
    Object[] args = pjp.getArgs();

    Object result = pjp.proceed();        // or proceed(newArgs) to alter them
    return result;                        // returning something else replaces the result
}

An @Around that forgets to call proceed() silently stops the method from running and returns null. It is the single easiest way to break an application with AOP.

Spring AOP versus AspectJ

Spring AOPAspectJ
Weavingruntime, via proxiescompile-time, post-compile or load-time
Join pointsmethod execution onlymethods, constructors, field access, static init…
Applies toSpring beans onlyany object
Setupnone beyond the startera weaver or a special compiler

Spring AOP handles the cases almost everyone actually has. Reach for AspectJ when you need to advise something that is not a Spring bean, or a join point that is not a method call.

⚠️ The limitations — this is the part that matters

Spring AOP is proxy-based, and that dictates everything below.

  1. Only Spring beans are advised. An object you created with new has no proxy around it.
  2. Only public methods (protected too, under CGLIB). Private, static and final methods are never advised.
  3. final classes cannot be proxied by CGLIB, and a class with no interface cannot be proxied by the JDK. Boot uses CGLIB by default, so the usual failure is a final class.
  4. Self-invocation is not advised. ⬇️
public void a() { b(); }   // b() is NOT advised - the call never leaves the object
public void b() { ... }    // advised only when ANOTHER bean calls it

Advice runs when a call arrives through the proxy. A call to a sibling method on this goes straight down the vtable, and the proxy never sees it.

This is not an AOP quirk you can file away. It is why:

  • a @Transactional method called from a sibling method runs with no transaction;
  • a @Cacheable method called internally always hits the database;
  • an @Async method called internally runs on the caller's thread;
  • a @PreAuthorize method called internally is not checked at all.

All four fail silently. If an annotation appears to do nothing, look for a self-invocation before you look at anything else. The fix is to move the annotated method to another bean — where it belongs anyway, because two responsibilities in one class is what put them together in the first place.

What to remember

  • Join point = where advice could run. Pointcut = where it does.
  • Five advice types; only @Around can stop or replace the call, and it must call proceed().
  • Spring AOP is runtime proxying, method execution only, Spring beans only.
  • Self-invocation bypasses the proxy — and therefore every annotation built on it.
  • Boot 4: the starter is spring-boot-starter-aspectj.