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.
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
shoppackage". 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.
Setting up
In Spring Boot 4 the AOP starter isspring-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>
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));
}
}
}
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()andfind(), and received their arguments through theJoinPoint, which also describes the method (getSignature()) and the target (getTarget()). - It did not run for
InvoiceService.invoice(): the pointcut names onlyOrderService. - The bean of type
OrderServicein the context is not anOrderServicebut 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 -
Some useful forms, and the other designators that Spring AOP supports -
| Pointcut | Matches |
|---|---|
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 |
&&, || 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 afinallyblock.@Around: around the method. It receives aProceedingJoinPointand decides itself whether and when to callproceed(), 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.
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());
}
}
}
}
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
@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.
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);
}
}
}
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 singlelongargument, so only its own output appears.find(),invoice()andcancel()all take onelong, 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,@Cacheableand@Asyncwork 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());
}
}
}
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
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
neware 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.
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.Spring AOP, Caching, Scheduling and Events Tutorials
How Spring Proxies Work and the Self-Invocation TrapClick to read tutorial: How Spring Proxies Work and the Self-Invocation Trap Caching in Spring Boot with @Cacheable and CaffeineClick to read tutorial: Caching in Spring Boot with @Cacheable and Caffeine Scheduling Tasks in Spring with @ScheduledClick to read tutorial: Scheduling Tasks in Spring with @Scheduled Async Processing in Spring with @Async and Virtual ThreadsClick to read tutorial: Async Processing in Spring with @Async and Virtual Threads Application Events in Spring with @EventListenerClick to read tutorial: Application Events in Spring with @EventListener Resilience in Spring 7: @Retryable and @ConcurrencyLimitClick to read tutorial: Resilience in Spring 7: @Retryable and @ConcurrencyLimit
How Spring Proxies Work and the Self-Invocation TrapClick to read tutorial: How Spring Proxies Work and the Self-Invocation Trap Caching in Spring Boot with @Cacheable and CaffeineClick to read tutorial: Caching in Spring Boot with @Cacheable and Caffeine Scheduling Tasks in Spring with @ScheduledClick to read tutorial: Scheduling Tasks in Spring with @Scheduled Async Processing in Spring with @Async and Virtual ThreadsClick to read tutorial: Async Processing in Spring with @Async and Virtual Threads Application Events in Spring with @EventListenerClick to read tutorial: Application Events in Spring with @EventListener Resilience in Spring 7: @Retryable and @ConcurrencyLimitClick to read tutorial: Resilience in Spring 7: @Retryable and @ConcurrencyLimit