Full Stack • Java • System Design • Cloud • AI Engineering

Frameworks2024-02-03

Spring Aspect-Oriented Programming

Master Spring AOP with detailed concepts, examples, diagrams, and data flow. Learn aspects, advice types, pointcuts, and real-world applications.

Spring AOP

What is AOP (Aspect-Oriented Programming)?

AOP is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure.

Cross-Cutting Concerns

Definition: Functionality that spans multiple points of an application

Common Examples:

  • Logging
  • Security
  • Transaction management
  • Caching
  • Error handling
  • Performance monitoring
  • Auditing

Problem Without AOP

// Code duplication across multiple methods
public class UserService {
    public void createUser(User user) {
        // Logging - Cross-cutting concern
        logger.info("Creating user: " + user.getName());
        
        // Security check - Cross-cutting concern
        if (!securityContext.hasPermission("CREATE_USER")) {
            throw new SecurityException();
        }
        
        // Transaction start - Cross-cutting concern
        transactionManager.begin();
        
        try {
            // Actual business logic
            userRepository.save(user);
            
            // Transaction commit - Cross-cutting concern
            transactionManager.commit();
            
            // Logging - Cross-cutting concern
            logger.info("User created successfully");
        } catch (Exception e) {
            // Transaction rollback - Cross-cutting concern
            transactionManager.rollback();
            
            // Error logging - Cross-cutting concern
            logger.error("Failed to create user", e);
            throw e;
        }
    }
    
    // Same cross-cutting concerns repeated in every method!
    public void updateUser(User user) { /* ... */ }
    public void deleteUser(Long id) { /* ... */ }
}

Solution With AOP

// Clean business logic
@Service
public class UserService {
    
    @Loggable
    @Secured("CREATE_USER")
    @Transactional
    public void createUser(User user) {
        // Only business logic!
        userRepository.save(user);
    }
    
    @Loggable
    @Secured("UPDATE_USER")
    @Transactional
    public void updateUser(User user) {
        userRepository.update(user);
    }
}

// Cross-cutting concerns in separate aspects
@Aspect
@Component
public class LoggingAspect {
    @Around("@annotation(Loggable)")
    public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.info("Method called: " + joinPoint.getSignature());
        Object result = joinPoint.proceed();
        logger.info("Method completed");
        return result;
    }
}

Core AOP Concepts

1. Aspect

Definition: A modularization of a cross-cutting concern

Example:

@Aspect
@Component
public class LoggingAspect {
    // This entire class is an aspect
    // It modularizes logging concern
}

2. Join Point

Definition: A point during program execution where an aspect can be applied

Possible Join Points:

  • Method execution (most common in Spring AOP)
  • Method call
  • Constructor execution
  • Field access
  • Exception handling

Example:

// Each method execution is a join point
public class UserService {
    public void createUser() { }  // Join Point 1
    public void updateUser() { }  // Join Point 2
    public void deleteUser() { }  // Join Point 3
}

3. Advice

Definition: Action taken by an aspect at a particular join point

Types of Advice:

@Before: Executes before the join point

@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println("Before method: " + joinPoint.getSignature());
}

@After: Executes after the join point (regardless of outcome)

@After("execution(* com.example.service.*.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("After method: " + joinPoint.getSignature());
}

@AfterReturning: Executes after successful completion

@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "result"
)
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("Method returned: " + result);
}

@AfterThrowing: Executes if method throws exception

@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))",
    throwing = "error"
)
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable error) {
    System.out.println("Method threw: " + error.getMessage());
}

@Around: Wraps the join point (most powerful)

@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Before method");
    Object result = joinPoint.proceed(); // Execute actual method
    System.out.println("After method");
    return result;
}

4. Pointcut

Definition: A predicate that matches join points

Syntax:

execution(modifiers? return-type declaring-type? method-name(parameters) throws?)

Examples:

// All methods in UserService
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}

// All public methods
@Pointcut("execution(public * *(..))")
public void publicMethods() {}

// All methods starting with 'get'
@Pointcut("execution(* get*(..))")
public void getterMethods() {}

// All methods with @Transactional annotation
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethods() {}

// All methods in classes with @Service annotation
@Pointcut("within(@org.springframework.stereotype.Service *)")
public void serviceMethods() {}

// Combine pointcuts
@Pointcut("publicMethods() && serviceMethods()")
public void publicServiceMethods() {}

5. Target Object

Definition: Object being advised by one or more aspects

@Service
public class UserService {  // This is the target object
    public void createUser() { }
}

6. AOP Proxy

Definition: Object created by AOP framework to implement aspect contracts

Types:

  • JDK Dynamic Proxy: For interfaces
  • CGLIB Proxy: For classes
Client → Proxy → Target Object
         ↓
      Aspects Applied

7. Weaving

Definition: Process of linking aspects with target objects

Types:

  • Compile-time: AspectJ compiler
  • Load-time: AspectJ weaver
  • Runtime: Spring AOP (proxy-based)

Data Flow Diagrams

1. Method Execution Flow Without AOP

Client
  ↓
  ├─→ Call method
  ↓
Target Object
  ├─→ Execute business logic
  ├─→ Handle logging manually
  ├─→ Handle security manually
  ├─→ Handle transactions manually
  ↓
Return result
  ↓
Client

2. Method Execution Flow With AOP

Client
  ↓
  ├─→ Call method
  ↓
AOP Proxy
  ├─→ @Before Advice (Logging, Security)
  ↓
  ├─→ Proceed to target
  ↓
Target Object
  ├─→ Execute business logic only
  ↓
  ├─→ Return to proxy
  ↓
AOP Proxy
  ├─→ @AfterReturning Advice (Logging)
  ↓
Return result
  ↓
Client

3. @Around Advice Flow

Client calls method
        ↓
    AOP Proxy
        ↓
@Around Advice starts
        ↓
    Before logic
        ↓
joinPoint.proceed()
        ↓
  Target method executes
        ↓
  Returns result
        ↓
    After logic
        ↓
@Around Advice returns
        ↓
    AOP Proxy
        ↓
Client receives result

4. Exception Handling Flow

Client calls method
        ↓
    AOP Proxy
        ↓
  @Before Advice
        ↓
  Target method
        ↓
  Exception thrown
        ↓
@AfterThrowing Advice
        ↓
  Log exception
        ↓
  @After Advice (finally)
        ↓
Exception propagated
        ↓
      Client

Complete Examples

Example 1: Logging Aspect

@Aspect
@Component
@Slf4j
public class LoggingAspect {
    
    // Pointcut for all service methods
    @Pointcut("within(@org.springframework.stereotype.Service *)")
    public void serviceMethods() {}
    
    // Log method entry
    @Before("serviceMethods()")
    public void logMethodEntry(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();
        
        log.info("→ Entering {}.{}() with arguments: {}", 
            className, methodName, Arrays.toString(args));
    }
    
    // Log method exit with result
    @AfterReturning(
        pointcut = "serviceMethods()",
        returning = "result"
    )
    public void logMethodExit(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        log.info("← Exiting {}.{}() with result: {}", 
            className, methodName, result);
    }
    
    // Log exceptions
    @AfterThrowing(
        pointcut = "serviceMethods()",
        throwing = "error"
    )
    public void logException(JoinPoint joinPoint, Throwable error) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        log.error("✗ Exception in {}.{}(): {}", 
            className, methodName, error.getMessage(), error);
    }
}

Output:

→ Entering UserService.createUser() with arguments: [User(name=John)]
← Exiting UserService.createUser() with result: User(id=1, name=John)

Example 2: Performance Monitoring Aspect

@Aspect
@Component
@Slf4j
public class PerformanceAspect {
    
    @Around("@annotation(com.example.annotation.Monitored)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        
        // Start timer
        long startTime = System.currentTimeMillis();
        
        log.info("⏱ Starting performance monitoring for: {}", methodName);
        
        Object result = null;
        try {
            // Execute method
            result = joinPoint.proceed();
            return result;
        } finally {
            // Calculate execution time
            long executionTime = System.currentTimeMillis() - startTime;
            
            // Log performance
            if (executionTime > 1000) {
                log.warn("⚠ SLOW: {} took {} ms", methodName, executionTime);
            } else {
                log.info("✓ {} completed in {} ms", methodName, executionTime);
            }
            
            // Store metrics (optional)
            metricsService.recordExecutionTime(methodName, executionTime);
        }
    }
}

// Custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
}

// Usage
@Service
public class UserService {
    
    @Monitored
    public User createUser(User user) {
        // Business logic
        return userRepository.save(user);
    }
}

Output:

⏱ Starting performance monitoring for: UserService.createUser(..)
✓ UserService.createUser(..) completed in 245 ms

Example 3: Security Aspect

@Aspect
@Component
@Slf4j
public class SecurityAspect {
    
    @Autowired
    private SecurityContext securityContext;
    
    @Before("@annotation(secured)")
    public void checkSecurity(JoinPoint joinPoint, Secured secured) {
        String requiredRole = secured.value();
        String currentUser = securityContext.getCurrentUser();
        
        log.info("🔒 Security check for {}: required role = {}", 
            joinPoint.getSignature().getName(), requiredRole);
        
        if (!securityContext.hasRole(requiredRole)) {
            log.error("❌ Access denied for user: {}", currentUser);
            throw new SecurityException(
                "User " + currentUser + " does not have role: " + requiredRole
            );
        }
        
        log.info("✓ Access granted for user: {}", currentUser);
    }
}

// Custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured {
    String value();
}

// Usage
@Service
public class UserService {
    
    @Secured("ADMIN")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }
}

Data Flow:

1. Client calls deleteUser()
2. AOP Proxy intercepts
3. SecurityAspect.checkSecurity() executes
   ├─ Get current user
   ├─ Check if user has ADMIN role
   ├─ If yes: proceed
   └─ If no: throw SecurityException
4. If authorized, target method executes
5. Return result to client

Example 4: Transaction Management Aspect

@Aspect
@Component
@Slf4j
public class TransactionAspect {
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        
        // Start transaction
        TransactionStatus status = transactionManager.getTransaction(
            new DefaultTransactionDefinition()
        );
        
        log.info("🔄 Transaction started for: {}", methodName);
        
        try {
            // Execute method
            Object result = joinPoint.proceed();
            
            // Commit transaction
            transactionManager.commit(status);
            log.info("✓ Transaction committed for: {}", methodName);
            
            return result;
            
        } catch (Exception e) {
            // Rollback transaction
            transactionManager.rollback(status);
            log.error("✗ Transaction rolled back for: {}", methodName);
            throw e;
        }
    }
}

Transaction Flow:

1. Method with @Transactional called
2. TransactionAspect intercepts
3. Begin transaction
   ↓
4. Execute business logic
   ↓
5. Success?
   ├─ Yes → Commit transaction
   └─ No → Rollback transaction
   ↓
6. Return result or throw exception

Example 5: Caching Aspect

@Aspect
@Component
@Slf4j
public class CachingAspect {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    
    @Around("@annotation(cacheable)")
    public Object cacheResult(ProceedingJoinPoint joinPoint, Cacheable cacheable) 
            throws Throwable {
        
        // Generate cache key
        String cacheKey = generateCacheKey(joinPoint, cacheable.key());
        
        // Check cache
        if (cache.containsKey(cacheKey)) {
            log.info("✓ Cache HIT for key: {}", cacheKey);
            return cache.get(cacheKey);
        }
        
        log.info("✗ Cache MISS for key: {}", cacheKey);
        
        // Execute method
        Object result = joinPoint.proceed();
        
        // Store in cache
        cache.put(cacheKey, result);
        log.info("→ Cached result for key: {}", cacheKey);
        
        return result;
    }
    
    private String generateCacheKey(ProceedingJoinPoint joinPoint, String key) {
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();
        return methodName + ":" + key + ":" + Arrays.toString(args);
    }
}

// Custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
    String key();
}

// Usage
@Service
public class UserService {
    
    @Cacheable(key = "user")
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Caching Flow:

First Call:
Client → Proxy → CachingAspect
                    ↓
              Check cache (MISS)
                    ↓
              Execute method
                    ↓
              Store in cache
                    ↓
              Return result

Second Call (same parameters):
Client → Proxy → CachingAspect
                    ↓
              Check cache (HIT)
                    ↓
              Return cached result
              (method not executed!)

Pointcut Expressions

Execution Pointcuts

// All methods in UserService
execution(* com.example.service.UserService.*(..))

// All public methods
execution(public * *(..))

// All methods returning User
execution(com.example.model.User *(..))

// All methods starting with 'get'
execution(* get*(..))

// All methods with specific parameters
execution(* *(Long, String))

// All methods with any parameters
execution(* *(*))

// All methods with no parameters
execution(* *())

Within Pointcuts

// All methods in service package
within(com.example.service.*)

// All methods in service package and subpackages
within(com.example.service..*)

// All methods in classes with @Service
within(@org.springframework.stereotype.Service *)

Annotation Pointcuts

// Methods with @Transactional
@annotation(org.springframework.transaction.annotation.Transactional)

// Classes with @Service
@within(org.springframework.stereotype.Service)

// Methods with parameter annotated with @Valid
@args(javax.validation.Valid)

Combining Pointcuts

@Pointcut("execution(public * *(..))")
public void publicMethods() {}

@Pointcut("within(com.example.service..*)")
public void inServiceLayer() {}

@Pointcut("publicMethods() && inServiceLayer()")
public void publicServiceMethods() {}

// Use combined pointcut
@Before("publicServiceMethods()")
public void beforePublicServiceMethod() {
    // Advice logic
}

Best Practices

1. Keep Aspects Focused

// ❌ Bad: One aspect doing everything
@Aspect
public class MegaAspect {
    public void doLogging() { }
    public void doSecurity() { }
    public void doTransaction() { }
    public void doCaching() { }
}

// ✅ Good: Separate aspects for each concern
@Aspect
public class LoggingAspect { }

@Aspect
public class SecurityAspect { }

@Aspect
public class TransactionAspect { }

2. Use Specific Pointcuts

// ❌ Bad: Too broad
@Before("execution(* *(..))")

// ✅ Good: Specific
@Before("execution(* com.example.service.*.*(..))")

3. Handle Exceptions Properly

@Around("serviceMethods()")
public Object handleExceptions(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        return joinPoint.proceed();
    } catch (BusinessException e) {
        // Handle business exceptions
        log.error("Business error", e);
        throw e;
    } catch (Exception e) {
        // Handle technical exceptions
        log.error("Technical error", e);
        throw new TechnicalException(e);
    }
}

4. Use Custom Annotations

// ✅ Good: Clear intent
@Monitored
@Secured("ADMIN")
@Cacheable(key = "user")
public User getUser(Long id) {
    return userRepository.findById(id);
}

5. Order Aspects

@Aspect
@Order(1)  // Executes first
public class SecurityAspect { }

@Aspect
@Order(2)  // Executes second
public class LoggingAspect { }

@Aspect
@Order(3)  // Executes third
public class TransactionAspect { }

Common Use Cases

1. Audit Logging

@Aspect
@Component
public class AuditAspect {
    
    @AfterReturning("@annotation(Auditable)")
    public void auditAction(JoinPoint joinPoint) {
        String user = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        String action = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        auditLog.record(user, action, args, LocalDateTime.now());
    }
}

2. Retry Logic

@Aspect
@Component
public class RetryAspect {
    
    @Around("@annotation(retryable)")
    public Object retry(ProceedingJoinPoint joinPoint, Retryable retryable) 
            throws Throwable {
        int maxAttempts = retryable.maxAttempts();
        int attempt = 0;
        
        while (attempt < maxAttempts) {
            try {
                return joinPoint.proceed();
            } catch (Exception e) {
                attempt++;
                if (attempt >= maxAttempts) {
                    throw e;
                }
                Thread.sleep(retryable.delay());
            }
        }
        return null;
    }
}

3. Rate Limiting

@Aspect
@Component
public class RateLimitAspect {
    
    private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
    
    @Before("@annotation(rateLimit)")
    public void checkRateLimit(JoinPoint joinPoint, RateLimit rateLimit) {
        String key = joinPoint.getSignature().toShortString();
        RateLimiter limiter = limiters.computeIfAbsent(
            key, 
            k -> RateLimiter.create(rateLimit.permitsPerSecond())
        );
        
        if (!limiter.tryAcquire()) {
            throw new RateLimitExceededException();
        }
    }
}

Conclusion

Spring AOP is a powerful tool for implementing cross-cutting concerns in a modular and maintainable way.

Key Takeaways:

  • AOP separates cross-cutting concerns from business logic
  • Use appropriate advice types (@Before, @After, @Around, etc.)
  • Write specific pointcut expressions
  • Keep aspects focused and single-purpose
  • Use custom annotations for clarity
  • Order aspects when necessary

Happy coding! 🚀