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
| Term | Meaning | Example |
|---|---|---|
| Aspect | A class with cross-cutting logic | LoggingAspect |
| Advice | The code that runs and when | @Before, @After, @Around |
| Join point | A point in the program where advice can run | A method call |
| Pointcut | An expression that selects join points | execution(* com.example.service..*(..)) |
| Weaving | Linking the aspect with the target | Done 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
- Self-invocation: if a method calls another method of the same class with this.method(), the proxy is bypassed and the advice does not run.
- Only calls from outside the bean are intercepted, and only public methods are advised by default.
- final classes and final methods cannot be proxied with CGLIB.
- For advice on private methods or self calls, use full AspectJ weaving.
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 proxy | CGLIB proxy | |
|---|---|---|
| Mechanism | java.lang.reflect.Proxy implements the interfaces | Generates a SUBCLASS of your class at runtime |
| Needs an interface | Yes | No |
| final class / final method | Not relevant | Cannot be proxied (final methods are not intercepted) |
| Injection type | Only by interface | By class or interface |
| Spring Boot default | spring.aop.proxy-target-class=false to force | true (default) |
Pointcut expressions you will use
| Designator | Example | Matches |
|---|---|---|
| execution | execution(public * com.example.service..*Service.*(..)) | Methods by signature (most common) |
| within | within(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 |
| bean | bean(*Repository) | Beans by name (Spring only) |
| args | args(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 @AfterExample: 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.