AOP & Cross-Cutting
Aspects, pointcuts, advice types, logging and transaction management via AOP
Aspect-Oriented Programming (AOP) separates cross-cutting concerns — logging, security, transaction management, caching, metrics — from business logic. Spring AOP uses JDK dynamic proxies or CGLIB to wrap beans with aspect behaviour at runtime, without modifying the target class.
Key Points
- Aspect: a class annotated with @Aspect that modularises a cross-cutting concern
- Join Point: a point in execution where an aspect can be plugged in — in Spring AOP, always a method execution
- Pointcut: expression that matches join points — @Pointcut("execution(* com.example.service.*.*(..))")
- Advice types: @Before (before method), @AfterReturning (after success), @AfterThrowing (after exception), @After (always), @Around (full control)
- @Around is the most powerful — ProceedingJoinPoint.proceed() calls the target method; wrap with try/catch for error handling
- Spring AOP works via proxies — self-invocation (this.method()) bypasses the proxy and skips aspects
- @Transactional works via AOP — calling a @Transactional method from the same class bypasses the transaction
- AspectJ: compile-time / load-time weaving — works for self-calls and non-Spring beans; Spring AOP is runtime proxy only
- AOP use cases: logging method entry/exit, measuring execution time, enforcing security, retrying on exception, caching results
Spring AOP: @Around logging/timing aspect, custom @Retryable annotation with retry logic, pointcut expressions
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
// Pointcut — all methods in service package
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
// @Around — full control, measure time, log, handle errors
@Around("serviceLayer()")
public Object logAndTime(ProceedingJoinPoint pjp) throws Throwable {
String method = pjp.getSignature().toShortString();
log.debug("→ {}", method);
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
log.debug("← {} ({}ms)", method, System.currentTimeMillis() - start);
return result;
} catch (Exception e) {
log.error("✗ {} threw {}", method, e.getMessage());
throw e;
}
}
}
// Retry aspect — @Around with retry logic
@Aspect @Component
public class RetryAspect {
@Around("@annotation(Retryable)")
public Object retry(ProceedingJoinPoint pjp) throws Throwable {
int attempts = 3;
for (int i = 1; i <= attempts; i++) {
try {
return pjp.proceed();
} catch (TransientException e) {
if (i == attempts) throw e;
Thread.sleep(200L * i); // exponential backoff
}
}
throw new IllegalStateException("unreachable");
}
}
// Custom annotation target
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {}
@Service
public class PaymentService {
@Retryable
@Transactional
public Receipt processPayment(Payment payment) { ... }
}Real-World Example
@Transactional is the most-used Spring AOP feature. The two most common bugs: (1) calling a @Transactional method from within the same class — the proxy is bypassed, no transaction starts; fix by injecting self or using ApplicationContext.getBean(); (2) @Transactional on a private method — Spring AOP cannot proxy private methods, transaction silently not applied.