Why this matters
- Copy-pasting logging or timing code into every service method violates DRY and clutters business logic.
- Spring's
@Transactionaland@Cacheableare themselves AOP proxies — understanding AOP explains how they work. - Custom aspects handle BookStore-specific concerns like audit trails and performance monitoring.
Enable AOP
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring Boot auto-configures AOP when the starter is present.
A logging aspect
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
@Before("execution(* com.example.bookstore.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("Calling {}.{}()",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "execution(* com.example.bookstore.service.*.*(..))",
returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
log.debug("{}.{}() returned {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
result);
}
}
Every BookService, OrderService, etc. method is logged without touching their code.
Advice types
@Before— Runs before the method. Logging, argument validation.@AfterReturning— Runs after successful return. Result logging, metrics.@AfterThrowing— Runs when the method throws. Error logging, alerting.@After— Runs after method completes (success or failure). Cleanup.@Around— Wraps the method entirely. Timing, retry, transaction control.
@Around for execution timing
@Aspect
@Component
public class PerformanceAspect {
private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);
@Around("execution(* com.example.bookstore.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long elapsed = System.currentTimeMillis() - start;
if (elapsed > 500) {
log.warn("SLOW: {}.{}() took {}ms",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(), elapsed);
}
}
}
}
joinPoint.proceed() invokes the actual method. Code before proceed() is "before" advice; code in finally runs regardless of outcome.
Pointcut expressions
Target specific methods with precision:
// All methods in the service package
@Pointcut("execution(* com.example.bookstore.service.*.*(..))")
public void serviceLayer() {}
// Only methods annotated with @Transactional
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethods() {}
// Methods taking a specific parameter type
@Pointcut("execution(* com.example.bookstore.service.BookService.create(..))")
public void bookCreation() {}
@Around("serviceLayer() && bookCreation()")
public Object auditBookCreation(ProceedingJoinPoint jp) throws Throwable { ... }
Combine pointcuts with &&, ||, and ! for precise targeting.
Custom annotation + aspect
Cleaner than package-based pointcuts:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String action();
}
@Aspect
@Component
public class AuditAspect {
private final AuditService auditService;
@AfterReturning("@annotation(audited)")
public void recordAudit(JoinPoint jp, Audited audited) {
auditService.log(audited.action(), jp.getArgs());
}
}
Usage in BookStore:
@Service
public class BookService {
@Audited(action = "CREATE_BOOK")
@Transactional
public Book create(CreateBookRequest request) { ... }
}
How proxies work
Spring creates a proxy around your bean. External calls go through the proxy (which runs aspects), then delegate to the real object. Two proxy types:
Proxy types
- JDK dynamic proxy — Interface-based. Works when the bean implements an interface.
- CGLIB proxy — Subclass-based. Used for concrete classes without interfaces.
@Transactional on BookService (a concrete class) uses CGLIB.
AOP vs manual cross-cutting
| Approach | Pros | Cons |
|---|---|---|
| AOP aspect | DRY, declarative, reusable | Harder to debug, proxy overhead |
| Manual in each method | Explicit, easy to trace | Duplicated, clutters business logic |
| Interceptor / Filter | HTTP-level concerns | Cannot target service methods |
AOP aspect
ProsDRY, declarative, reusableConsHarder to debug, proxy overheadManual in each method
ProsExplicit, easy to traceConsDuplicated, clutters business logicInterceptor / Filter
ProsHTTP-level concernsConsCannot target service methods
Use AOP for service-layer cross-cutting. Use filters for HTTP-level concerns.
Quick recall
Everything you need if you only revisit this box.
- AOP separates cross-cutting concerns (logging, timing, auditing) from business logic.
@Aspectclasses define advice (@Before,@Around, etc.) applied via pointcut expressions.@AroundwithjoinPoint.proceed()wraps method execution for timing and retry logic.- Custom annotations (
@Audited) paired with aspects are cleaner than package-based pointcuts. - Spring uses CGLIB proxies for concrete classes, JDK proxies for interfaces.
- Self-invocation within a class bypasses the AOP proxy — extract to a separate bean.
Test yourself
Answer these before moving on — recall is what makes it stick.