Skip to content

Spring AOP

What is AOP?

Aspect-Oriented Programming enables modularization of cross-cutting concerns (logging, security, transactions) that span multiple classes.

Cross-Cutting Concerns


AOP Terminology

AOP Concepts

Visual Representation

AOP in Action


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

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

  1. What is AOP?
  2. Modularizes cross-cutting concerns (logging, security, transactions)
  3. Separates business logic from infrastructure concerns

  4. Difference between @Before and @Around?

  5. @Before: Runs before, can't prevent method execution
  6. @Around: Full control, can prevent/modify execution

  7. What are pointcut designators?

  8. execution, within, @annotation, bean, args, this, target

  9. Spring AOP vs AspectJ?

  10. Spring: Runtime proxy, method-level only, simpler
  11. AspectJ: Compile-time weaving, more powerful, complex

  12. Self-invocation problem?

  13. Internal method calls bypass proxy
  14. Solutions: inject self, AopContext, separate service

  15. When to use AOP?

  16. Logging, security, transactions, caching, metrics, auditing

  • *