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
| Term | Meaning |
|---|---|
| Aspect | A class holding cross-cutting behavior (@Aspect + @Component) |
| Join point | A point in execution where an aspect can run — in Spring AOP, always a method execution |
| Pointcut | An expression that selects which join points to advise |
| Advice | The action taken at a join point (@Before, @Around, etc.) |
| Weaving | Linking 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)):
| Pattern | Matches |
|---|---|
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.
| Advice | Runs | Can change flow? |
|---|---|---|
@Before | Before the method | No (can throw) |
@After | After the method (finally — success or failure) | No |
@AfterReturning | After a successful return | Reads return value |
@AfterThrowing | After the method throws | Reads the exception |
@Around | Wraps the method | Yes — 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 aLoggable loggableargument). You get type-safe access to attributes likeloggable.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
thisbypasses the proxy, so the advice does not run. Move the advised method to another bean, or inject a proxy reference of self. privateandfinalmethods: cannot be overridden by the proxy, so they cannot be advised. CGLIB also cannot subclassfinalclasses.staticmethods: 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.