Spring AOP Explained: Aspects, Advice and Pointcuts

Introduction

This tutorial explains aspect-oriented programming (AOP) in Spring. We will see the problem AOP solves - code such as logging, auditing and timing that every service needs but that belongs to none of them - and the terms of AOP: aspect, join point, advice, pointcut and proxy. Then we will write aspects with Spring Boot 4: a first @Before advice, all five kinds of advice and the order in which they run, pointcut expressions such as execution(), within(), args() and @annotation(), an @Around advice that times a method, and the order of several aspects. Every output below comes from running the code.

The problem: cross-cutting concerns

Most classes of an application have one job: an order service handles orders, an invoice service writes invoices. But some needs cut across all of them: log every call, check permissions, time slow methods, start and commit a transaction, write an audit record. Written by hand, each of these concerns appears as a few lines at the start and end of many methods, in every service -
  • the same code is repeated in dozens of places, and a change to it means changing them all;
  • the business logic of each method is buried under lines that have nothing to do with it;
  • forgetting the lines in one new method goes unnoticed, until the audit record is missing.
AOP moves each such concern into one class of its own, an aspect, together with a rule that says which methods it applies to. The services stay free of it, and Spring applies the aspect to them at runtime. Cross-cutting concerns - logging, security, transactions and auditing cut across the order, invoice and customer services; AOP moves each concern into one aspect

The terms of AOP

AOP comes with its own vocabulary. The terms sound abstract, but each has a simple meaning in Spring -
  • Aspect: a class that holds the code of one cross-cutting concern. In Spring, a bean annotated with @Aspect.
  • Join point: a point in the running program where an aspect can step in. In Spring AOP, a join point is always the execution of a method of a bean.
  • Advice: the code that runs at a join point - a method of the aspect. Its annotation says when it runs: before the method, after it, or around it.
  • Pointcut: an expression that selects join points, such as "every public method of the classes in the shop package". Each advice is tied to a pointcut.
  • Target: the bean whose methods are advised, here OrderService.
  • Proxy: the object that Spring puts in the application context in place of the target. Callers call the proxy; the proxy runs the matching advice and calls the target.
  • Weaving: joining aspects with targets. Spring AOP weaves at runtime, by creating proxies. The full AspectJ compiler can weave at compile time or class-load time instead, and supports more join points (constructors, field access), but needs a special compiler or a Java agent.
A caller calls the proxy that Spring put in the context; the proxy runs the advice of the aspects whose pointcut matches the method, and calls the target bean

Setting up

In Spring Boot 4 the AOP starter is spring-boot-starter-aspectj (it replaces spring-boot-starter-aop of Spring Boot 3). It brings the AspectJ annotations and expression parser, and with it on the classpath Spring Boot switches on @EnableAspectJAutoProxy by itself -
pom.xml - dependency
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
Spring AOP uses the AspectJ annotations and pointcut language, but not the AspectJ weaver's compile-time weaving: it is still the proxy-based Spring AOP described above.

The services to advise

The examples advise two small services of a shop. Neither of them knows about logging or auditing -
src/main/java/.../shop/Audited.java - a marker annotation, used later
//Audited.java
package com.javabrahman.aop.shop;

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 Audited {
  String value();   // what the audited action is, for the audit record
}
src/main/java/.../shop/OrderService.java
//OrderService.java
package com.javabrahman.aop.shop;

import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
  private final Map<Long, String> orders = new LinkedHashMap<>();
  private long lastId = 100;

  public long place(String item, int quantity) {
    if (quantity <= 0) {
      throw new IllegalArgumentException("quantity must be positive, was " + quantity);
    }
    long id = ++lastId;
    orders.put(id, quantity + " x " + item);
    System.out.println("      OrderService.place() runs: order " + id);
    return id;
  }

  public String find(long id) {
    System.out.println("      OrderService.find() runs");
    return orders.get(id);
  }

  @Audited("cancel an order")
  public void cancel(long id) {
    System.out.println("      OrderService.cancel() runs");
    orders.remove(id);
  }

  public String report() throws InterruptedException {
    Thread.sleep(150);   // stands for a slow database query
    return "orders in the shop: " + orders.size();
  }
}
src/main/java/.../shop/InvoiceService.java
//InvoiceService.java
package com.javabrahman.aop.shop;

import org.springframework.stereotype.Service;

@Service
public class InvoiceService {

  public String invoice(long orderId) {
    System.out.println("      InvoiceService.invoice() runs");
    return "INV-" + orderId;
  }
}

A first aspect: @Before

An aspect is a class annotated with @Aspect, and also a Spring bean, here through @Component. Its @Before method runs before every method that its pointcut matches: here, every method of OrderService -
src/main/java/.../first/CallLogAspect.java
//CallLogAspect.java
package com.javabrahman.aop.first;

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

  @Before("execution(* com.javabrahman.aop.shop.OrderService.*(..))")
  public void logCall(JoinPoint joinPoint) {
    System.out.println("[before] " + joinPoint.getSignature().toShortString()
        + " with " + Arrays.toString(joinPoint.getArgs()));
  }
}
src/main/java/.../first/FirstAspectApp.java
//FirstAspectApp.java
package com.javabrahman.aop.first;

import com.javabrahman.aop.shop.InvoiceService;
import com.javabrahman.aop.shop.OrderService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(scanBasePackages = {"com.javabrahman.aop.shop", "com.javabrahman.aop.first"})
public class FirstAspectApp {

  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = SpringApplication.run(FirstAspectApp.class, args)) {
      OrderService orders = context.getBean(OrderService.class);
      System.out.println("The OrderService bean is a " + orders.getClass().getSimpleName());

      long id = orders.place("Clean Code", 2);
      System.out.println("found: " + orders.find(id));
      System.out.println("invoice: " + context.getBean(InvoiceService.class).invoice(id));
    }
  }
}
 OUTPUT of the above code
The OrderService bean is a OrderService$$SpringCGLIB$$0
[before] OrderService.place(..) with [Clean Code, 2]
      OrderService.place() runs: order 101
[before] OrderService.find(..) with [101]
      OrderService.find() runs
found: 2 x Clean Code
      InvoiceService.invoice() runs
invoice: INV-101
  • The advice ran before place() and find(), and received their arguments through the JoinPoint, which also describes the method (getSignature()) and the target (getTarget()).
  • It did not run for InvoiceService.invoice(): the pointcut names only OrderService.
  • The bean of type OrderService in the context is not an OrderService but a subclass that Spring generated at runtime with CGLIB - the proxy. How it works, and the traps it brings, are the subject of the next tutorialClick to read tutorial: How Spring Proxies Work and the Self-Invocation Trap.

Pointcut expressions

The string in @Before is an AspectJ pointcut expression. The most used designator, execution(), matches methods by their signature. Its parts follow the order of a method declaration, and * and .. are wildcards - The parts of an execution pointcut - optional modifiers, return type, optional declaring type, method name pattern, parameter pattern and optional throws clause, with what * and .. match Some useful forms, and the other designators that Spring AOP supports -
PointcutMatches
execution(public * *(..))every public method of every bean
execution(* com.javabrahman.aop.shop..*.*(..))every method of every class in shop and its sub-packages
execution(* place*(String, ..))methods whose name starts with place and whose first parameter is a String
within(com.javabrahman.aop.shop..*)every method of the types in shop and below - a shorter way to select by package
args(id) / args(long, ..)methods whose arguments at runtime match; with a name, it also passes the argument to the advice
@annotation(Audited)methods annotated with @Audited
@within(org.springframework.stereotype.Service)methods of classes annotated with @Service
bean(invoiceService)the methods of the bean with that name (a Spring-only designator; * allowed)
this(Type) / target(Type)beans whose proxy / target object is of that type
Expressions combine with &&, || and !. A pointcut that is used more than once gets a name: an empty method annotated with @Pointcut, whose name other advice uses in place of the expression, as the next example shows.

The five kinds of advice

An advice annotation decides when the advice runs, relative to the method -
  • @Before: before the method. It cannot stop the call, except by throwing an exception.
  • @AfterReturning: after the method returned normally. returning = "name" passes the return value to the advice.
  • @AfterThrowing: after the method threw. throwing = "name" passes the exception; the exception still reaches the caller.
  • @After: after the method, whether it returned or threw - like a finally block.
  • @Around: around the method. It receives a ProceedingJoinPoint and decides itself whether and when to call proceed(), which runs the method. It can change the arguments, the return value, or the exception. The most powerful kind, and so the one to use only when the others do not suffice.
This aspect declares all five on one named pointcut, and the application calls place() once with a valid quantity and once with an invalid one -
src/main/java/.../kinds/AdviceKindsAspect.java
//AdviceKindsAspect.java
package com.javabrahman.aop.kinds;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AdviceKindsAspect {

  @Pointcut("execution(long com.javabrahman.aop.shop.OrderService.place(String, int))")
  void placingAnOrder() {
  }

  @Before("placingAnOrder()")
  public void before(JoinPoint joinPoint) {
    System.out.println("   @Before          quantity " + joinPoint.getArgs()[1]);
  }

  @AfterReturning(pointcut = "placingAnOrder()", returning = "orderId")
  public void afterReturning(long orderId) {
    System.out.println("   @AfterReturning  returned order " + orderId);
  }

  @AfterThrowing(pointcut = "placingAnOrder()", throwing = "error")
  public void afterThrowing(IllegalArgumentException error) {
    System.out.println("   @AfterThrowing   " + error.getMessage());
  }

  @After("placingAnOrder()")
  public void after() {
    System.out.println("   @After           (always)");
  }

  @Around("placingAnOrder()")
  public Object around(ProceedingJoinPoint call) throws Throwable {
    System.out.println("   @Around          before proceed()");
    try {
      Object result = call.proceed();
      System.out.println("   @Around          after proceed(), result " + result);
      return result;
    } catch (IllegalArgumentException e) {
      System.out.println("   @Around          proceed() threw");
      throw e;
    }
  }
}
src/main/java/.../kinds/AdviceKindsApp.java
//AdviceKindsApp.java
package com.javabrahman.aop.kinds;

import com.javabrahman.aop.shop.OrderService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(scanBasePackages = {"com.javabrahman.aop.shop", "com.javabrahman.aop.kinds"})
public class AdviceKindsApp {

  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = SpringApplication.run(AdviceKindsApp.class, args)) {
      OrderService orders = context.getBean(OrderService.class);

      System.out.println("place(\"Clean Code\", 2):");
      orders.place("Clean Code", 2);

      System.out.println("place(\"Refactoring\", 0):");
      try {
        orders.place("Refactoring", 0);
      } catch (IllegalArgumentException e) {
        System.out.println("caller caught: " + e.getMessage());
      }
    }
  }
}
 OUTPUT of the above code
place("Clean Code", 2):
   @Around          before proceed()
   @Before          quantity 2
      OrderService.place() runs: order 101
   @AfterReturning  returned order 101
   @After           (always)
   @Around          after proceed(), result 101
place("Refactoring", 0):
   @Around          before proceed()
   @Before          quantity 0
   @AfterThrowing   quantity must be positive, was 0
   @After           (always)
   @Around          proceed() threw
caller caught: quantity must be positive, was 0
The order of the lines shows how Spring nests the advice of one aspect: @Around is the outermost, then @Before runs, then the method, and on the way out @AfterReturning or @AfterThrowing, then @After, and last the rest of @Around. The exception still reached the caller: @AfterThrowing sees an exception but does not catch it. The order in which the advice of one aspect runs - @Around first, then @Before, the target method, @AfterReturning or @AfterThrowing, @After, and the end of @Around Two details of the binding: the parameter type of an @AfterThrowing advice also filters - afterThrowing(IllegalArgumentException error) would not run for an IllegalStateException. And @AfterReturning with a long orderId parameter only matches methods whose return value fits that type.

Selecting by package, argument and annotation

execution() is not the only way to select methods. The aspect below uses three other designators, and binds what they match to advice parameters: args(id) passes the long argument to the advice, and @annotation(audited) passes the annotation itself, so that the advice can read its value() -
src/main/java/.../designators/AuditAspect.java
//AuditAspect.java
package com.javabrahman.aop.designators;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import com.javabrahman.aop.shop.Audited;

@Aspect
@Component
public class AuditAspect {

  // every method in the shop package (and below) that takes exactly one long argument
  @Before("within(com.javabrahman.aop.shop..*) && args(id)")
  public void idArgument(JoinPoint joinPoint, long id) {
    System.out.println("[id]    " + joinPoint.getSignature().toShortString() + " for id " + id);
  }

  // every method annotated with @Audited; the annotation is passed to the advice
  @AfterReturning("@annotation(audited)")
  public void audit(JoinPoint joinPoint, Audited audited) {
    System.out.println("[audit] user ann did: " + audited.value() + " " + joinPoint.getArgs()[0]);
  }
}
src/main/java/.../designators/DesignatorsApp.java
//DesignatorsApp.java
package com.javabrahman.aop.designators;

import com.javabrahman.aop.shop.InvoiceService;
import com.javabrahman.aop.shop.OrderService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(scanBasePackages = {"com.javabrahman.aop.shop", "com.javabrahman.aop.designators"})
public class DesignatorsApp {

  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = SpringApplication.run(DesignatorsApp.class, args)) {
      OrderService orders = context.getBean(OrderService.class);
      InvoiceService invoices = context.getBean(InvoiceService.class);

      long id = orders.place("Clean Code", 2);
      orders.find(id);
      invoices.invoice(id);
      orders.cancel(id);
    }
  }
}
 OUTPUT of the above code
      OrderService.place() runs: order 101
[id]    OrderService.find(..) for id 101
      OrderService.find() runs
[id]    InvoiceService.invoice(..) for id 101
      InvoiceService.invoice() runs
[id]    OrderService.cancel(..) for id 101
      OrderService.cancel() runs
[audit] user ann did: cancel an order 101
  • place(String, int) has no single long argument, so only its own output appears. find(), invoice() and cancel() all take one long, in two different classes, and the [id] advice ran for each.
  • Only cancel() carries @Audited, and the audit advice read the action's name from the annotation. Marking methods with an annotation of your own is often the clearest pointcut of all: the method shows that something is applied to it, and the aspect does not depend on package or method names that may change. Spring's own @Transactional, @Cacheable and @Async work this way.

@Around in practice: timing a method

A typical use of @Around measures how long a method takes. The advice is the only kind that can run code both before and after the method in one place, with a local variable shared between the two -
src/main/java/.../ordering/TimingAspect.java
//TimingAspect.java
package com.javabrahman.aop.ordering;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

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

  @Around("execution(* com.javabrahman.aop.shop.OrderService.report())")
  public Object time(ProceedingJoinPoint call) throws Throwable {
    System.out.println("TimingAspect   (order 1) starts the clock");
    long start = System.nanoTime();
    try {
      return call.proceed();
    } finally {
      long millis = (System.nanoTime() - start) / 1_000_000;
      System.out.println("TimingAspect   (order 1) " + call.getSignature().toShortString() + " took "
          + (millis >= 150 ? "at least 150 ms" : millis + " ms"));
    }
  }
}
src/main/java/.../ordering/SecurityAspect.java
//SecurityAspect.java
package com.javabrahman.aop.ordering;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(2)
public class SecurityAspect {

  @Around("execution(* com.javabrahman.aop.shop.OrderService.report())")
  public Object check(ProceedingJoinPoint call) throws Throwable {
    System.out.println("SecurityAspect (order 2) checks the user");
    Object result = call.proceed();
    System.out.println("SecurityAspect (order 2) done");
    return result;
  }
}
src/main/java/.../ordering/OrderingApp.java
//OrderingApp.java
package com.javabrahman.aop.ordering;

import com.javabrahman.aop.shop.OrderService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(scanBasePackages = {"com.javabrahman.aop.shop", "com.javabrahman.aop.ordering"})
public class OrderingApp {

  public static void main(String[] args) throws InterruptedException {
    try (ConfigurableApplicationContext context = SpringApplication.run(OrderingApp.class, args)) {
      OrderService orders = context.getBean(OrderService.class);
      orders.place("Clean Code", 2);
      System.out.println(orders.report());
    }
  }
}
 OUTPUT of the above code
      OrderService.place() runs: order 101
TimingAspect   (order 1) starts the clock
SecurityAspect (order 2) checks the user
SecurityAspect (order 2) done
TimingAspect   (order 1) OrderService.report() took at least 150 ms
orders in the shop: 1
The method sleeps for 150 ms, so the advice prints "at least 150 ms" instead of a number that changes from run to run. Note the finally block: the time is printed even when the method throws. And the advice returns the result of proceed(); an @Around advice that forgets the return makes every advised method return null.

The order of several aspects

When more than one aspect matches a method, their order is undefined unless they declare one, with @Order on the aspect class or by implementing Ordered. The lower number has the higher precedence: it runs first on the way in and last on the way out, so it wraps the others. In the output, TimingAspect (order 1) starts first and finishes last, and so its time includes the security check of SecurityAspect (order 2). Whether that is right depends on what the timing should measure; making the order explicit is what matters. Spring's own aspects take part in the same ordering: a transaction advice, for example, has the lowest precedence by default, and so runs inside your aspects.

What Spring AOP cannot do

Because Spring AOP works with proxies, an advice only runs when a call goes through the proxy -
  • Only beans: objects created with new are not proxies, and their methods are never advised.
  • Only calls from outside: when a method of a bean calls another method of the same bean (this.cancel(id)), the call does not go through the proxy, and no advice runs - the self-invocation trap.
  • Not private, static or final methods: a CGLIB proxy is a subclass, and a subclass cannot override them.
  • Only method executions: no advice on constructors or field access. These need the full AspectJ weaver.
How Spring proxies workClick to read tutorial: How Spring Proxies Work and the Self-Invocation Trap shows each of these limits with code, and the ways around them.

When to write your own aspect

  • Use what Spring already has first: transactions, caching, async execution, retries, security checks and metrics are all aspects that Spring or Spring Boot provide behind an annotation.
  • Write your own for a concern that is really cross-cutting and specific to your application: an audit trail, a tenant check, a custom metric, logging of slow calls.
  • Keep pointcuts narrow: execution(* *(..)) advises every method of every bean, including Spring's own, and makes startup slower and stack traces longer. A custom annotation or a package selects exactly what is meant.
  • Keep advice small and safe: an exception thrown by an advice breaks the business call it wraps.

Summary

In this tutorial we moved cross-cutting concerns out of the services and into aspects. An aspect is an @Aspect bean whose advice methods run at join points - the method executions - that their pointcut selects. We wrote a @Before advice, saw all five kinds of advice run in their fixed order, selected methods with execution(), within(), args() and @annotation(), timed a method with @Around, and ordered two aspects with @Order. Spring applies aspects through a proxy, a generated subclass that the context holds in place of the bean; the next tutorial, How Spring Proxies Work and the Self-Invocation TrapClick to read tutorial: How Spring Proxies Work and the Self-Invocation Trap, looks inside it.