Home / Spring Boot / Core Concepts / Aspect-Oriented Programming (AOP)

Aspect-Oriented Programming (AOP)

AOP lets you write cross-cutting code (logging, timing, security, transactions) once, and apply it to many methods without changing them. @Transactional, @Cacheable and @Async are all implemented with AOP.

Terms

TermMeaningExample
AspectA class with cross-cutting logicLoggingAspect
AdviceThe code that runs and when@Before, @After, @Around
Join pointA point in the program where advice can runA method call
PointcutAn expression that selects join pointsexecution(* com.example.service..*(..))
WeavingLinking the aspect with the targetDone at runtime with proxies in Spring

Spring AOP is proxy based. Spring wraps your bean in a proxy (JDK dynamic proxy if there is an interface, otherwise CGLIB subclass). Calls from outside go through the proxy, and the proxy runs the advice before and after your method.

<!-- Boot 3.x dependency (in Boot 4 the starter is spring-boot-starter-aspectj) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Logging and timing aspect

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
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);

    @Pointcut("execution(* com.example.demo.service..*(..))")
    public void serviceMethods() { }

    @Before("serviceMethods()")
    public void before(JoinPoint jp) {
        log.info("Calling {}", jp.getSignature().toShortString());
    }

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

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

    @Around("serviceMethods()")
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();                       // call the real method
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("{} took {} ms", pjp.getSignature().getName(), ms);
        }
    }
}

Custom annotation with AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit { String action(); }

@Aspect
@Component
public class AuditAspect {
    @Around("@annotation(audit)")
    public Object audit(ProceedingJoinPoint pjp, Audit audit) throws Throwable {
        System.out.println("AUDIT start: " + audit.action());
        Object result = pjp.proceed();
        System.out.println("AUDIT end: " + audit.action());
        return result;
    }
}

@Service
public class UserService {
    @Audit(action = "DELETE_USER")
    public void deleteUser(Long id) { /* ... */ }
}

Limits of proxy based AOP

Internal workflow: how a proxy is created and how a call flows

STARTUP
 AnnotationAwareAspectJAutoProxyCreator (a BeanPostProcessor, registered by AopAutoConfiguration)
   for every bean, after initialization:
     1. find all advisors: @Aspect classes (advice + pointcut) and infrastructure advisors (@Transactional, @Cacheable, @Async, @PreAuthorize)
     2. does any pointcut match a method of this bean?  no ─► the bean is used as it is
     3. yes ─► ProxyFactory creates a proxy:  CGLIB subclass (Boot default) or JDK dynamic proxy, holding the advisor chain and the target

CALL  caller ─► proxy.placeOrder()
   ▼ CglibAopProxy.DynamicAdvisedInterceptor.intercept
   1. get the interceptor chain for this method (cached): [TransactionInterceptor, LoggingAspect@Around, ...] sorted by @Order
   2. ReflectiveMethodInvocation.proceed()   ── each interceptor calls proceed() to go to the next one ──►
        TransactionInterceptor: begin tx ─► proceed ─►  LoggingAspect: log ─► proceed ─► TARGET method ─► back out: log, commit tx
   3. result (or exception) returns through the same chain in reverse order
// The same thing without Boot magic: create a proxy by hand to see the mechanism
ProxyFactory factory = new ProxyFactory(new OrderService());
factory.addAdvice((MethodInterceptor) invocation -> {
    System.out.println("before " + invocation.getMethod().getName());
    Object result = invocation.proceed();                              // next interceptor or the real method
    System.out.println("after " + invocation.getMethod().getName());
    return result;
});
OrderService proxy = (OrderService) factory.getProxy();
proxy.placeOrder(42);                                                  // prints: before placeOrder ... after placeOrder
JDK dynamic proxyCGLIB proxy
Mechanismjava.lang.reflect.Proxy implements the interfacesGenerates a SUBCLASS of your class at runtime
Needs an interfaceYesNo
final class / final methodNot relevantCannot be proxied (final methods are not intercepted)
Injection typeOnly by interfaceBy class or interface
Spring Boot defaultspring.aop.proxy-target-class=false to forcetrue (default)

Pointcut expressions you will use

DesignatorExampleMatches
executionexecution(public * com.example.service..*Service.*(..))Methods by signature (most common)
withinwithin(com.example.web..*)All methods in these types
@annotation@annotation(com.example.Audit)Methods with an annotation
@within@within(org.springframework.stereotype.Service)All methods of classes with an annotation
beanbean(*Repository)Beans by name (Spring only)
argsargs(String, ..)Methods by argument types

Advice order

@Aspect @Component @Order(1)     // lower number = outer = runs first on the way in, last on the way out
class SecurityAspect { }

@Aspect @Component @Order(2)
class TimingAspect { }
// call order: SecurityAspect(before) -> TimingAspect(before) -> method -> TimingAspect(after) -> SecurityAspect(after)
// Within ONE aspect: @Around wraps, then @Before, then the method, then @AfterReturning/@AfterThrowing, then @After

Example: retry aspect for flaky calls

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryOnFailure {
    int attempts() default 3;
    long delayMs() default 200;
}

@Aspect
@Component
public class RetryAspect {

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

    @Around("@annotation(retry)")
    public Object retry(ProceedingJoinPoint pjp, RetryOnFailure retry) throws Throwable {
        Throwable last = null;
        for (int attempt = 1; attempt <= retry.attempts(); attempt++) {
            try {
                return pjp.proceed();
            } catch (Throwable ex) {
                last = ex;
                log.warn("Attempt {}/{} of {} failed: {}", attempt, retry.attempts(), pjp.getSignature().getName(), ex.toString());
                if (attempt < retry.attempts()) Thread.sleep(retry.delayMs() * attempt);      // simple linear backoff
            }
        }
        throw last;
    }
}

Fixing self-invocation (calls inside the same class)

// Problem: this.inner() does not go through the proxy, so @Transactional/@Cacheable/@Async/@PreAuthorize on inner() are ignored
@Service
public class ReportService {
    public void run() { inner(); }
    @Transactional public void inner() { }
}

// Fix 1 (best): move inner() to another bean and inject it
// Fix 2: self injection
@Service
public class ReportService2 {
    private final ReportService2 self;
    public ReportService2(@Lazy ReportService2 self) { this.self = self; }
    public void run() { self.inner(); }                    // goes through the proxy
    @Transactional public void inner() { }
}
// Fix 3: @EnableAspectJAutoProxy(exposeProxy = true) and ((ReportService3) AopContext.currentProxy()).inner()   (ugly)
// Fix 4: AspectJ compile-time or load-time weaving (real bytecode weaving, no proxies)

Common mistake: Calling an @Around or @Transactional method from another method in the same class and expecting the aspect to run. The call does not go through the proxy.

Interview questions

What is the difference between @Before, @After and @Around?

@Before runs before the method, @After after it (always), and @Around wraps the call and controls whether and how the method is executed.

Which proxy types does Spring use?

JDK dynamic proxies for beans that implement interfaces, CGLIB subclass proxies otherwise. Spring Boot uses CGLIB by default (spring.aop.proxy-target-class=true).

Where does Spring itself use AOP?

Transactions (@Transactional), caching (@Cacheable), async (@Async), method security (@PreAuthorize) and retry.

Why does an @Around advice on a private or final method do nothing?

Proxies intercept public and protected method calls that arrive through the proxy. Private methods are never called through it, and CGLIB cannot override final methods.

What is the difference between Spring AOP and AspectJ?

Spring AOP is proxy based, works only on Spring beans and method executions. AspectJ weaves bytecode at compile or load time, so it can advise any class, field access, constructors and self invocations.