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.
| Term | What it is |
|---|---|
| Join point | A point in execution where advice could apply. In Spring AOP, always a method call. |
| Pointcut | An expression selecting which join points. |
| Advice | The code that runs at a selected join point. |
| Aspect | A class holding pointcuts and advice together. |
| Target | The real bean being advised. |
| Weaving | Wiring 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
| Advice | Runs | Can it stop the call? |
|---|---|---|
@Before | before the method | only by throwing |
@AfterReturning | after a normal return; sees the value | no |
@AfterThrowing | only when it throws; sees the exception | no |
@After | always, like finally | no |
@Around | wraps the whole call | yes |
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 AOP | AspectJ | |
|---|---|---|
| Weaving | runtime, via proxies | compile-time, post-compile or load-time |
| Join points | method execution only | methods, constructors, field access, static init… |
| Applies to | Spring beans only | any object |
| Setup | none beyond the starter | a 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.
- Only Spring beans are advised. An object you created with
newhas no proxy around it. - Only public methods (protected too, under CGLIB). Private, static and
finalmethods are never advised. finalclasses 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 afinalclass.- 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 itAdvice 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
@Transactionalmethod called from a sibling method runs with no transaction; - a
@Cacheablemethod called internally always hits the database; - an
@Asyncmethod called internally runs on the caller's thread; - a
@PreAuthorizemethod 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
@Aroundcan stop or replace the call, and it must callproceed(). - 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.