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.