Spring AOP¶
What is AOP?¶
Aspect-Oriented Programming enables modularization of cross-cutting concerns (logging, security, transactions) that span multiple classes.
AOP Terminology¶
Visual Representation¶
Enabling AOP¶
@Configuration
@EnableAspectJAutoProxy // Enable AOP support
public class AopConfig {
}
// Spring Boot auto-enables with spring-boot-starter-aop
// Just add dependency:
// implementation 'org.springframework.boot:spring-boot-starter-aop'
Advice Types¶
@Before - Run before method¶
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("Calling: {} with args: {}",
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()));
}
}
@AfterReturning - Run after successful return¶
@Aspect
@Component
public class AuditAspect {
@AfterReturning(
pointcut = "execution(* com.example.service.OrderService.create*(..))",
returning = "result"
)
public void auditOrderCreation(JoinPoint joinPoint, Order result) {
auditLog.info("Order created: {} by {}",
result.getId(),
SecurityContextHolder.getContext().getAuthentication().getName());
}
}
@AfterThrowing - Run after exception¶
@Aspect
@Component
public class ExceptionLoggingAspect {
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex"
)
public void logException(JoinPoint joinPoint, Exception ex) {
log.error("Exception in {}: {}",
joinPoint.getSignature().toShortString(),
ex.getMessage());
}
}
@After - Run always (finally)¶
@Aspect
@Component
public class CleanupAspect {
@After("execution(* com.example.service.*.*(..))")
public void cleanup(JoinPoint joinPoint) {
// Runs whether method succeeded or threw exception
MDC.clear(); // Clear logging context
}
}
@Around - Full control (most powerful)¶
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // Execute target method
return result;
} finally {
long duration = System.currentTimeMillis() - start;
log.info("{} executed in {} ms",
joinPoint.getSignature().toShortString(), duration);
}
}
// Modify arguments
@Around("execution(* com.example.service.*.*(String))")
public Object trimStringArgs(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
args[i] = ((String) args[i]).trim();
}
}
return joinPoint.proceed(args); // Proceed with modified args
}
}
Pointcut Expressions¶
Execution Pointcut (Most Common)¶
// Pattern: execution(modifiers? return-type declaring-type? method-name(params) throws?)
// All methods in service package
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
// All public methods
@Pointcut("execution(public * *(..))")
public void publicMethods() {}
// Methods returning void
@Pointcut("execution(void com.example.service.*.*(..))")
public void voidMethods() {}
// Methods starting with "get"
@Pointcut("execution(* com.example.service.*.get*(..))")
public void getters() {}
// Methods with specific parameter
@Pointcut("execution(* com.example.service.*.*(String))")
public void methodsWithStringArg() {}
// Methods with any parameters
@Pointcut("execution(* com.example.service.*.*(*))")
public void methodsWithOneArg() {}
// Methods with String as first arg
@Pointcut("execution(* com.example.service.*.*(String, ..))")
public void methodsStartingWithString() {}
// Subpackages
@Pointcut("execution(* com.example..*.*(..))") // Note: .. for subpackages
public void allInPackageAndSub() {}
Other Pointcut Designators¶
// Within - matches all methods in class/package
@Pointcut("within(com.example.service.*)")
public void inServicePackage() {}
@Pointcut("within(com.example.service..*)") // Including subpackages
public void inServicePackageAndSub() {}
// @within - classes annotated with
@Pointcut("@within(org.springframework.stereotype.Service)")
public void inServiceClasses() {}
// @annotation - methods annotated with
@Pointcut("@annotation(com.example.Loggable)")
public void loggableMethods() {}
// bean - specific Spring bean
@Pointcut("bean(orderService)")
public void orderServiceBean() {}
@Pointcut("bean(*Service)") // Beans ending with Service
public void serviceBeans() {}
// this - proxy implements type
@Pointcut("this(com.example.service.PaymentService)")
public void paymentServiceProxy() {}
// target - target object implements type
@Pointcut("target(com.example.service.PaymentService)")
public void paymentServiceTarget() {}
// args - method arguments
@Pointcut("args(String, ..)")
public void firstArgString() {}
@Pointcut("args(com.example.model.Order)")
public void orderArgument() {}
Combining Pointcuts¶
@Aspect
@Component
public class CombinedAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("execution(* com.example.repository.*.*(..))")
public void repositoryLayer() {}
@Pointcut("@annotation(com.example.Audited)")
public void auditedMethods() {}
// AND
@Pointcut("serviceLayer() && auditedMethods()")
public void auditedServiceMethods() {}
// OR
@Pointcut("serviceLayer() || repositoryLayer()")
public void dataAccessLayer() {}
// NOT
@Pointcut("serviceLayer() && !auditedMethods()")
public void nonAuditedServiceMethods() {}
@Before("auditedServiceMethods()")
public void audit(JoinPoint joinPoint) {
// ...
}
}
Accessing Context¶
JoinPoint¶
@Before("execution(* com.example.service.*.*(..))")
public void logMethodCall(JoinPoint joinPoint) {
// Method signature
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
// Arguments
Object[] args = joinPoint.getArgs();
// Full signature
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Class<?> returnType = signature.getReturnType();
String[] paramNames = signature.getParameterNames();
log.info("{}#{} called with {}", className, methodName, Arrays.toString(args));
}
Binding Annotations¶
// Bind annotation to parameter
@Before("@annotation(audited)")
public void audit(JoinPoint joinPoint, Audited audited) {
String action = audited.action(); // Access annotation attributes
log.info("Auditing action: {}", action);
}
// Bind arguments
@Before("execution(* com.example.service.*.*(..)) && args(order, ..)")
public void validateOrder(JoinPoint joinPoint, Order order) {
if (order.getAmount() > 10000) {
log.warn("High-value order: {}", order.getId());
}
}
// Bind target
@Before("execution(* com.example.service.*.*(..)) && target(service)")
public void logTarget(JoinPoint joinPoint, Object service) {
log.info("Target class: {}", service.getClass().getName());
}
Practical Examples¶
Retry Aspect¶
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
int maxAttempts() default 3;
long delay() default 1000;
Class<? extends Exception>[] retryOn() default {Exception.class};
}
@Aspect
@Component
public class RetryAspect {
@Around("@annotation(retryable)")
public Object retry(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable {
int attempts = 0;
Exception lastException = null;
while (attempts < retryable.maxAttempts()) {
try {
return joinPoint.proceed();
} catch (Exception e) {
lastException = e;
boolean shouldRetry = Arrays.stream(retryable.retryOn())
.anyMatch(ex -> ex.isAssignableFrom(e.getClass()));
if (!shouldRetry) throw e;
attempts++;
if (attempts < retryable.maxAttempts()) {
Thread.sleep(retryable.delay());
log.warn("Retry attempt {} for {}", attempts,
joinPoint.getSignature().toShortString());
}
}
}
throw lastException;
}
}
// Usage
@Service
public class ExternalApiService {
@Retryable(maxAttempts = 3, delay = 2000, retryOn = {IOException.class})
public Response callExternalApi() {
// May fail transiently
}
}
Caching Aspect¶
@Aspect
@Component
public class SimpleCacheAspect {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Around("@annotation(cacheable)")
public Object cache(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
String key = generateKey(joinPoint, cacheable.key());
if (cache.containsKey(key)) {
log.debug("Cache hit for key: {}", key);
return cache.get(key);
}
Object result = joinPoint.proceed();
cache.put(key, result);
log.debug("Cached result for key: {}", key);
return result;
}
private String generateKey(ProceedingJoinPoint joinPoint, String keyExpression) {
return joinPoint.getSignature().toShortString() +
Arrays.toString(joinPoint.getArgs());
}
}
Security Aspect¶
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String[] value();
}
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(requiresRole)")
public void checkRole(JoinPoint joinPoint, RequiresRole requiresRole) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Set<String> userRoles = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
boolean hasRole = Arrays.stream(requiresRole.value())
.anyMatch(userRoles::contains);
if (!hasRole) {
throw new AccessDeniedException("User lacks required role");
}
}
}
// Usage
@Service
public class AdminService {
@RequiresRole({"ROLE_ADMIN", "ROLE_SUPER_ADMIN"})
public void deleteUser(Long userId) {
// Only admins can delete users
}
}
Metrics Aspect¶
@Aspect
@Component
public class MetricsAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(timed)")
public Object recordTime(ProceedingJoinPoint joinPoint, Timed timed) throws Throwable {
Timer timer = meterRegistry.timer(timed.value());
return timer.record(() -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void countExceptions(JoinPoint joinPoint, Exception ex) {
meterRegistry.counter("exceptions",
"class", joinPoint.getTarget().getClass().getSimpleName(),
"method", joinPoint.getSignature().getName(),
"exception", ex.getClass().getSimpleName()
).increment();
}
}
Aspect Ordering¶
@Aspect
@Component
@Order(1) // Lower number = higher priority (runs first)
public class SecurityAspect {
// Runs before LoggingAspect
}
@Aspect
@Component
@Order(2)
public class LoggingAspect {
// Runs after SecurityAspect
}
Execution order for @Around:
Security @Around (before proceed) → Logging @Around (before proceed) →
Target Method →
Logging @Around (after proceed) → Security @Around (after proceed)
Spring AOP vs AspectJ¶
Self-Invocation Problem¶
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// ...
validateOrder(order); // Self-invocation - NOT proxied!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validateOrder(Order order) {
// This won't run in a new transaction!
}
}
// Solutions:
// 1. Inject self (proxy)
@Service
public class OrderService {
@Autowired
private OrderService self; // Proxy
public void processOrder(Order order) {
self.validateOrder(order); // Through proxy
}
}
// 2. Use AopContext
@Service
public class OrderService {
public void processOrder(Order order) {
((OrderService) AopContext.currentProxy()).validateOrder(order);
}
}
// 3. Extract to separate service
@Service
public class OrderValidationService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validateOrder(Order order) {
// Works correctly
}
}
Common Interview Questions¶
- What is AOP?
- Modularizes cross-cutting concerns (logging, security, transactions)
-
Separates business logic from infrastructure concerns
-
Difference between @Before and @Around?
- @Before: Runs before, can't prevent method execution
-
@Around: Full control, can prevent/modify execution
-
What are pointcut designators?
-
execution, within, @annotation, bean, args, this, target
-
Spring AOP vs AspectJ?
- Spring: Runtime proxy, method-level only, simpler
-
AspectJ: Compile-time weaving, more powerful, complex
-
Self-invocation problem?
- Internal method calls bypass proxy
-
Solutions: inject self, AopContext, separate service
-
When to use AOP?
- Logging, security, transactions, caching, metrics, auditing
- *