Skip to content

Latest commit

 

History

History
1241 lines (914 loc) · 31.2 KB

File metadata and controls

1241 lines (914 loc) · 31.2 KB

Spring Boot AOP (Aspect-Oriented Programming) - Complete Guide

📚 Overview

This comprehensive guide covers Aspect-Oriented Programming (AOP) in Spring Boot 3, based on the luv2code LLC tutorial. AOP is a programming technique that helps solve cross-cutting concerns like logging, security, and transactions without tangling business logic.


🎯 What is AOP?

Aspect-Oriented Programming is a programming paradigm that encapsulates cross-cutting logic into reusable modules called Aspects.

Core Problems AOP Solves

  1. Code Tangling: Business logic mixed with infrastructure concerns (logging, security)
  2. Code Scattering: Same logic duplicated across multiple classes

Example Problem:

public void addAccount(Account theAccount, String userId) {
    // Logging code
    System.out.println("Logging...");
    
    // Security code
    if (!isAuthorized(userId)) {
        throw new SecurityException();
    }
    
    // Actual business logic
    entityManager.persist(theAccount);
}

Issues: Logging and security code must be added to EVERY method in EVERY layer (Controller, Service, DAO).


🔑 Key AOP Terminology

Term Definition
Aspect Module of code for a cross-cutting concern (logging, security)
Advice What action is taken and when it should be applied
Join Point When to apply code during program execution
Pointcut A predicate expression for where advice should be applied
Weaving Connecting aspects to target objects to create advised object

📋 Advice Types

Advice Type Description When It Runs
@Before Run before the method Before method execution
@AfterReturning Run after method (success) After successful execution
@AfterThrowing Run after method (exception) After exception is thrown
@After Run after method (finally) Always runs (success or failure)
@Around Run before and after method Complete control over execution

🚀 Getting Started

Step 1: Add Spring Boot AOP Starter Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Note: Spring Boot automatically enables AOP support. No need for @EnableAspectJAutoProxy.


💡 Complete Examples

Example 1: Basic @Before Advice

Step 1: Create Target Object (DAO)

public interface AccountDAO {
    void addAccount();
}

@Component
public class AccountDAOImpl implements AccountDAO {
    public void addAccount() {
        System.out.println("DOING MY DB WORK: ADDING AN ACCOUNT");
    }
}

Step 2: Create Main Application

@SpringBootApplication
public class AopdemoApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(AopdemoApplication.class, args);
    }
    
    @Bean
    public CommandLineRunner commandLineRunner(AccountDAO theAccountDAO) {
        return runner -> {
            demoTheBeforeAdvice(theAccountDAO);
        };
    }
    
    private void demoTheBeforeAdvice(AccountDAO theAccountDAO) {
        // call the business method
        theAccountDAO.addAccount();
    }
}

Step 3: Create Aspect with @Before Advice

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    @Before("execution(public void addAccount())")
    public void beforeAddAccountAdvice() {
        System.out.println("Executing @Before advice on addAccount()");
    }
}

Output:

Executing @Before advice on addAccount()
DOING MY DB WORK: ADDING AN ACCOUNT

🎯 Pointcut Expressions

Pointcut Expression Syntax

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

? indicates optional pattern

Common Pointcut Examples

1. Match Specific Method in Specific Class

@Before("execution(public void com.luv2code.aopdemo.dao.AccountDAO.addAccount())")

2. Match Any addAccount() Method

@Before("execution(public void addAccount())")

3. Match Methods Starting with "add"

@Before("execution(public void add*())")

4. Match Any Return Type

@Before("execution(* processCreditCard*())")

5. Match Methods with No Parameters

@Before("execution(* addAccount())")

6. Match Methods with Account Parameter

@Before("execution(* addAccount(com.luv2code.aopdemo.Account))")

7. Match Methods with Any Number of Parameters

@Before("execution(* addAccount(..))")

8. Match Any Method in Package

@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")

🔄 Pointcut Declarations (Reusable Pointcuts)

Problem: Repeating Pointcut Expressions

@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")
public void beforeAddAccountAdvice() { ... }

@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")
public void performApiAnalytics() { ... }

Solution: Create Pointcut Declaration

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    // Pointcut declaration
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.*(..))")
    private void forDaoPackage() {}
    
    // Apply to multiple advices
    @Before("forDaoPackage()")
    public void beforeAddAccountAdvice() {
        System.out.println("Executing @Before advice");
    }
    
    @Before("forDaoPackage()")
    public void performApiAnalytics() {
        System.out.println("Performing API analytics");
    }
}

🔗 Combining Pointcut Expressions

Logical Operators

  • AND (&&): Both conditions must be true
  • OR (||): Either condition must be true
  • NOT (!): Negate condition

Example: Exclude Getter/Setter Methods

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    // All methods in DAO package
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.*(..))")
    private void forDaoPackage() {}
    
    // Getter methods
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.get*(..))")
    private void getter() {}
    
    // Setter methods
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.set*(..))")
    private void setter() {}
    
    // Combine: Include DAO package, exclude getters/setters
    @Pointcut("forDaoPackage() && !(getter() || setter())")
    private void forDaoPackageNoGetterSetter() {}
    
    @Before("forDaoPackageNoGetterSetter()")
    public void beforeAddAccountAdvice() {
        System.out.println("Executing @Before advice");
    }
}

📊 Controlling Aspect Order with @Order

Problem: Undefined Order

When multiple aspects apply to same method, execution order is undefined.

Solution: Use @Order Annotation

Lower numbers = Higher precedence

@Aspect
@Component
@Order(1)
public class MyCloudLogAspect {
    @Before("com.luv2code.aopdemo.aspect.LuvAopExpressions.forDaoPackage()")
    public void logToCloud() {
        System.out.println("Logging to cloud");
    }
}

@Aspect
@Component
@Order(2)
public class MyLoggingDemoAspect {
    @Before("com.luv2code.aopdemo.aspect.LuvAopExpressions.forDaoPackage()")
    public void logToConsole() {
        System.out.println("Logging to console");
    }
}

@Aspect
@Component
@Order(3)
public class MyApiAnalyticsAspect {
    @Before("com.luv2code.aopdemo.aspect.LuvAopExpressions.forDaoPackage()")
    public void performAnalytics() {
        System.out.println("Performing analytics");
    }
}

Execution Order: MyCloudLogAspect → MyLoggingDemoAspect → MyApiAnalyticsAspect

Note: Range is Integer.MIN_VALUE to Integer.MAX_VALUE. Negative numbers allowed.


🔍 Reading Method Arguments with JoinPoint

Access Method Signature and Arguments

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    @Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")
    public void beforeAddAccountAdvice(JoinPoint theJoinPoint) {
        
        // Display method signature
        MethodSignature methodSig = (MethodSignature) theJoinPoint.getSignature();
        System.out.println("Method: " + methodSig);
        
        // Display method arguments
        Object[] args = theJoinPoint.getArgs();
        for (Object tempArg : args) {
            System.out.println("Argument: " + tempArg);
            
            if (tempArg instanceof Account) {
                Account theAccount = (Account) tempArg;
                System.out.println("Account name: " + theAccount.getName());
                System.out.println("Account level: " + theAccount.getLevel());
            }
        }
    }
}

✅ @AfterReturning Advice

Runs after method completes successfully (no exceptions).

Use Cases

  • Logging
  • Audit logging
  • Post-processing data
  • Enriching/formatting data

Example: Access Return Value

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    @AfterReturning(
        pointcut = "execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))",
        returning = "result")
    public void afterReturningFindAccountsAdvice(
            JoinPoint theJoinPoint, List<Account> result) {
        
        // Print method signature
        String method = theJoinPoint.getSignature().toShortString();
        System.out.println("Executing @AfterReturning on method: " + method);
        
        // Print results
        System.out.println("Result is: " + result);
    }
}

Modifying Return Value

@AfterReturning(
    pointcut = "execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))",
    returning = "result")
public void afterReturningFindAccountsAdvice(
        JoinPoint theJoinPoint, List<Account> result) {
    
    // Modify the result - convert names to uppercase
    if (!result.isEmpty()) {
        for (Account account : result) {
            String upperName = account.getName().toUpperCase();
            account.setName(upperName);
        }
    }
}

⚠️ Warning: Modifying return values should be done with caution!


❌ @AfterThrowing Advice

Runs after method throws an exception.

Use Cases

  • Log exceptions
  • Audit logging
  • Notify DevOps team via email/SMS
  • Exception tracking

Example: Access Exception

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    @AfterThrowing(
        pointcut = "execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))",
        throwing = "theExc")
    public void afterThrowingFindAccountsAdvice(
            JoinPoint theJoinPoint, Throwable theExc) {
        
        // Print method name
        String method = theJoinPoint.getSignature().toShortString();
        System.out.println("Executing @AfterThrowing on method: " + method);
        
        // Log exception
        System.out.println("Exception is: " + theExc);
    }
}

Important: Exception is still propagated to the calling program. Use @Around to stop propagation.


🔄 @After Advice (Finally)

Runs after method completes (success or failure) - like finally block.

Use Cases

  • Logging regardless of outcome
  • Resource cleanup
  • Audit logging

Example

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    @After("execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))")
    public void afterFinallyFindAccountsAdvice(JoinPoint theJoinPoint) {
        
        String method = theJoinPoint.getSignature().toShortString();
        System.out.println("Executing @After (finally) on method: " + method);
    }
}

Note: @After advice does NOT have access to exception. Use @AfterThrowing if you need exception details.


🎭 @Around Advice

Most powerful advice type - complete control over method execution.

Use Cases

  • Logging, auditing, security
  • Pre/post-processing data
  • Instrumentation/profiling (measure execution time)
  • Exception management (handle/swallow/rethrow)

Example 1: Measure Execution Time

@Aspect
@Component
public class MyDemoLoggingAspect {
    
    @Around("execution(* com.luv2code.aopdemo.service.*.getFortune(..))")
    public Object aroundGetFortune(ProceedingJoinPoint theProceedingJoinPoint) 
            throws Throwable {
        
        // Print method name
        String method = theProceedingJoinPoint.getSignature().toShortString();
        System.out.println("Executing @Around on method: " + method);
        
        // Get begin timestamp
        long begin = System.currentTimeMillis();
        
        // Execute the method
        Object result = theProceedingJoinPoint.proceed();
        
        // Get end timestamp
        long end = System.currentTimeMillis();
        
        // Calculate and display duration
        long duration = end - begin;
        System.out.println("Duration: " + duration / 1000.0 + " seconds");
        
        return result;
    }
}

Example 2: Handle Exception

@Around("execution(* com.luv2code.aopdemo.service.*.getFortune(..))")
public Object aroundGetFortune(ProceedingJoinPoint theProceedingJoinPoint) 
        throws Throwable {
    
    Object result = null;
    
    try {
        // Execute the method
        result = theProceedingJoinPoint.proceed();
        
    } catch (Exception exc) {
        // Log exception
        System.out.println("@Around advice: We have a problem - " + exc);
        
        // Handle/swallow exception - return default value
        result = "Nothing exciting here. Move along!";
        
        // OR rethrow exception
        // throw exc;
    }
    
    return result;
}

Example 3: Rethrow Exception

@Around("execution(* com.luv2code.aopdemo.service.*.getFortune(..))")
public Object aroundGetFortune(ProceedingJoinPoint theProceedingJoinPoint) 
        throws Throwable {
    
    try {
        Object result = theProceedingJoinPoint.proceed();
        return result;
        
    } catch (Exception exc) {
        // Log exception
        System.out.println("@Around advice: We have a problem - " + exc);
        
        // Rethrow exception to calling program
        throw exc;
    }
}

🆚 JoinPoint vs ProceedingJoinPoint

Feature JoinPoint ProceedingJoinPoint
Used with @Before, @After, @AfterReturning, @AfterThrowing @Around only
Purpose Read method information Execute target method
Methods getSignature(), getArgs() proceed() + JoinPoint methods

🏗️ Real-World Example: Spring MVC CRUD with AOP

Architecture

Web Browser → Controller → Service → DAO → Database
                 ↓           ↓        ↓
            Logging Aspect (AOP)

Step 1: Add AOP Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Step 2: Create Logging Aspect

@Aspect
@Component
public class DemoLoggingAspect {
    
    private Logger myLogger = Logger.getLogger(getClass().getName());
    
    // Pointcut for controller package
    @Pointcut("execution(* com.luv2code.springboot.cruddemo.controller.*.*(..))")
    private void forControllerPackage() {}
    
    // Pointcut for service package
    @Pointcut("execution(* com.luv2code.springboot.cruddemo.service.*.*(..))")
    private void forServicePackage() {}
    
    // Pointcut for dao package
    @Pointcut("execution(* com.luv2code.springboot.cruddemo.dao.*.*(..))")
    private void forDaoPackage() {}
    
    // Combine all pointcuts
    @Pointcut("forControllerPackage() || forServicePackage() || forDaoPackage()")
    private void forAppFlow() {}
    
    // @Before advice
    @Before("forAppFlow()")
    public void before(JoinPoint theJoinPoint) {
        
        // Display method being called
        String method = theJoinPoint.getSignature().toShortString();
        myLogger.info("====>> in @Before: calling method: " + method);
        
        // Display arguments
        Object[] args = theJoinPoint.getArgs();
        for (Object tempArg : args) {
            myLogger.info("====>> argument: " + tempArg);
        }
    }
    
    // @AfterReturning advice
    @AfterReturning(
        pointcut = "forAppFlow()",
        returning = "theResult")
    public void afterReturning(JoinPoint theJoinPoint, Object theResult) {
        
        // Display method name
        String method = theJoinPoint.getSignature().toShortString();
        myLogger.info("====>> in @AfterReturning: from method: " + method);
        
        // Display result
        myLogger.info("====>> result: " + theResult);
    }
}

🎓 Spring AOP vs AspectJ

Spring AOP

Advantages Disadvantages
✅ Simpler to use ❌ Only method-level join points
✅ Uses Proxy pattern ❌ Only works with Spring beans
✅ Can migrate to AspectJ ❌ Minor performance cost (runtime weaving)

AspectJ

Advantages Disadvantages
✅ All join points (method, constructor, field) ❌ Requires extra compilation step
✅ Works with any POJO ❌ Complex pointcut syntax
✅ Faster (compile-time weaving) ❌ Steeper learning curve

Recommendation

  1. Start with Spring AOP - easy to get started
  2. Move to AspectJ if you need:
    • Constructor/field interception
    • Better performance
    • Non-Spring beans

⚠️ Best Practices

1. Keep Aspects Small and Fast

// ✅ GOOD - Fast and focused
@Before("forDaoPackage()")
public void logBefore(JoinPoint theJoinPoint) {
    logger.info("Method called: " + theJoinPoint.getSignature());
}

// ❌ BAD - Slow database/network calls
@Before("forDaoPackage()")
public void logBefore(JoinPoint theJoinPoint) {
    sendEmailNotification();  // Slow!
    updateDatabaseLog();      // Slow!
}

2. Narrow Pointcut Expressions

// ❌ BAD - Too broad, may catch framework classes
@Before("execution(* com..*.*(..))")

// ✅ GOOD - Specific to your package
@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")

3. Use Pointcut Declarations for Reusability

// ✅ GOOD - Reusable pointcut
@Pointcut("execution(* com.luv2code.aopdemo.dao.*.*(..))")
private void forDaoPackage() {}

@Before("forDaoPackage()")
@AfterReturning("forDaoPackage()")

4. Use @Order for Multiple Aspects

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

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

🐛 Common Pitfalls

1. IntelliJ Ultimate - JMX Conflict

Error: BeanCreationException with mbeanExporter

Solution: Narrow pointcut expression

// Instead of
@Before("execution(* com.luv2code..add*(..))")

// Use
@Before("execution(* com.luv2code.aopdemo.dao.add*(..))")

2. Forgetting to Return Result in @Around

// ❌ WRONG - No return statement
@Around("execution(* getFortune(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object result = pjp.proceed();
    // Missing return!
}

// ✅ CORRECT
@Around("execution(* getFortune(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object result = pjp.proceed();
    return result;  // Must return!
}

3. Using Wrong JoinPoint Type

// ❌ WRONG - ProceedingJoinPoint with @Before
@Before("execution(* addAccount(..))")
public void before(ProceedingJoinPoint pjp) { }

// ✅ CORRECT - Use JoinPoint
@Before("execution(* addAccount(..))")
public void before(JoinPoint jp) { }

📚 Additional Resources

  • Spring Documentation: spring.io
  • Book: AspectJ in Action by Ramnivas Laddad
  • Book: Aspect-Oriented Development with Use Cases by Ivar Jacobson

🎯 Quick Reference Cheat Sheet

// Basic Aspect Structure
@Aspect
@Component
public class MyAspect {
    
    // Pointcut declaration
    @Pointcut("execution(* com.example.service.*.*(..))")
    private void forServiceLayer() {}
    
    // Before advice
    @Before("forServiceLayer()")
    public void before(JoinPoint jp) { }
    
    // After returning
    @AfterReturning(pointcut = "forServiceLayer()", returning = "result")
    public void afterReturning(JoinPoint jp, Object result) { }
    
    // After throwing
    @AfterThrowing(pointcut = "forServiceLayer()", throwing = "exc")
    public void afterThrowing(JoinPoint jp, Throwable exc) { }
    
    // After finally
    @After("forServiceLayer()")
    public void after(JoinPoint jp) { }
    
    // Around
    @Around("forServiceLayer()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object result = pjp.proceed();
        return result;
    }
}

🌍 Real-World Use Cases

1. Logging with Execution Time

@Aspect
@Component
public class PerformanceMonitoringAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class);
    
    @Around("execution(* com.example.service.*.*(...))")
    public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        String methodName = pjp.getSignature().getName();
        
        try {
            Object result = pjp.proceed();
            
            long executionTime = System.currentTimeMillis() - startTime;
            if (executionTime > 1000) {  // > 1 second
                logger.warn("Method {} took {} ms - SLOW!", methodName, executionTime);
            } else {
                logger.info("Method {} executed in {} ms", methodName, executionTime);
            }
            
            return result;
        } catch (Exception e) {
            long executionTime = System.currentTimeMillis() - startTime;
            logger.error("Method {} failed after {} ms", methodName, executionTime, e);
            throw e;
        }
    }
}

2. Security & Authorization Checks

@Aspect
@Component
public class SecurityAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(SecurityAspect.class);
    
    @Before("@annotation(requiresPermission)")
    public void checkPermissions(JoinPoint jp, RequiresPermission requiresPermission) {
        String requiredRole = requiresPermission.role();
        String currentUser = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        
        boolean hasRole = SecurityContextHolder.getContext()
            .getAuthentication()
            .getAuthorities()
            .stream()
            .anyMatch(auth -> auth.getAuthority().equals("ROLE_" + requiredRole));
        
        if (!hasRole) {
            logger.warn("User {} attempted unauthorized access to {}", 
                currentUser, jp.getSignature().getName());
            throw new AccessDeniedException("User does not have required role: " + requiredRole);
        }
    }
}

// Custom Annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresPermission {
    String role();
}

// Usage
@Service
public class SensitiveService {
    
    @RequiresPermission(role = "ADMIN")
    public void deleteAllData() {
        // Only ADMIN can call this
    }
}

3. Transaction Management

@Aspect
@Component
public class TransactionManagementAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(TransactionManagementAspect.class);
    
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object manageTransaction(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        
        try {
            logger.info("Starting transaction for method: {}", methodName);
            Object result = pjp.proceed();
            logger.info("Transaction committed for method: {}", methodName);
            return result;
        } catch (Exception e) {
            logger.error("Transaction rolled back for method: {} - Reason: {}", 
                methodName, e.getMessage());
            throw e;
        }
    }
}

4. Caching Results

@Aspect
@Component
public class CachingAspect {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    private static final Logger logger = LoggerFactory.getLogger(CachingAspect.class);
    
    @Around("@annotation(com.example.annotation.Cacheable)")
    public Object cacheResults(ProceedingJoinPoint pjp) throws Throwable {
        String cacheKey = generateCacheKey(pjp);
        
        if (cache.containsKey(cacheKey)) {
            logger.info("Cache hit for key: {}", cacheKey);
            return cache.get(cacheKey);
        }
        
        Object result = pjp.proceed();
        cache.put(cacheKey, result);
        logger.info("Result cached for key: {}", cacheKey);
        
        return result;
    }
    
    private String generateCacheKey(ProceedingJoinPoint pjp) {
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        return methodName + "_" + Arrays.hashCode(args);
    }
}

5. Audit Logging

@Aspect
@Component
public class AuditAspect {
    
    @Autowired
    private AuditRepository auditRepository;
    
    private static final Logger logger = LoggerFactory.getLogger(AuditAspect.class);
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.*.*(..)) && " +
                   "(execution(* *.save*(..)) || execution(* *.delete*(..)) || execution(* *.update*(..)))",
        returning = "result"
    )
    public void auditDatabaseChanges(JoinPoint jp, Object result) {
        String userName = getCurrentUser();
        String methodName = jp.getSignature().getName();
        String className = jp.getTarget().getClass().getName();
        
        AuditLog auditLog = new AuditLog();
        auditLog.setUsername(userName);
        auditLog.setAction(methodName);
        auditLog.setEntity(className);
        auditLog.setTimestamp(LocalDateTime.now());
        
        auditRepository.save(auditLog);
        
        logger.info("Audit: User {} performed action {} on {}", 
            userName, methodName, className);
    }
    
    private String getCurrentUser() {
        return SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();
    }
}

🎯 Pointcut Expression Patterns

Common Patterns

// Match all methods in service package
@Pointcut("execution(* com.example.service.*.*(..))")
public void allServiceMethods() {}

// Match specific annotation
@Pointcut("@annotation(com.example.annotation.CustomAnnotation)")
public void customAnnotatedMethods() {}

// Match methods returning specific type
@Pointcut("execution(User com.example.service.*.*(..))")
public void returnsUser() {}

// Match methods with specific parameters
@Pointcut("execution(* com.example.service.*.*(String, int))")
public void specificSignature() {}

// Combine multiple pointcuts with AND
@Pointcut("execution(* com.example.service.*.*(..)) && " +
          "@annotation(com.example.annotation.CacheResult)")
public void serviceMethodsWithCaching() {}

// Combine with OR
@Pointcut("execution(* com.example.service.*.*(..)) || " +
          "execution(* com.example.controller.*.*(..))")
public void serviceOrController() {}

// Negate pointcut
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* *.get*(..))")
public void serviceExceptGetters() {}

⚠️ Common AOP Mistakes & Solutions

Issue: Pointcut Not Matching

Problem: Aspect not being applied to expected methods

Solutions:

// ❌ WRONG - Pointcut too restrictive
@Pointcut("execution(public void com.example.service.*.save(..))")

// ✅ CORRECT - More flexible pattern
@Pointcut("execution(* com.example.service.*.save(..))")

// ✅ CORRECT - Use annotations for clarity
@Pointcut("@annotation(com.example.annotation.LogExecution)")

Issue: Self-Invocation Problem

Problem: AOP doesn't work for internal method calls

@Service
public class UserService {
    
    // ❌ Aspect won't apply for internal call
    public void createUser(User user) {
        saveUser(user);  // saveUser() aspect won't execute
    }
    
    @LogExecution
    public void saveUser(User user) {
        // Business logic
    }
}

// ✅ SOLUTION - Inject self reference
@Service
public class UserService {
    
    @Autowired
    private UserService self;
    
    public void createUser(User user) {
        self.saveUser(user);  // Now aspect will apply
    }
    
    @LogExecution
    public void saveUser(User user) {
        // Business logic
    }
}

Issue: Aspect Applied to Wrong Classes

Problem: Aspect applied too broadly and affects unintended classes

// ❌ TOO BROAD - Affects all methods everywhere
@Pointcut("execution(* *(..))")

// ✅ CORRECT - Specific package
@Pointcut("execution(* com.example.service.*.*(..))")

// ✅ CORRECT - With additional conditions
@Pointcut("execution(* com.example.service.*.*(..)) && " +
          "@annotation(com.example.annotation.Auditable)")

📊 Performance Considerations

AOP Overhead

@Aspect
@Component
public class OptimizedAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(OptimizedAspect.class);
    
    // ✅ GOOD - Lightweight pointcut
    @Before("@annotation(com.example.annotation.Monitor)")
    public void logAnnotated() {
        logger.info("Method called");
    }
    
    // ❌ EXPENSIVE - Complex regex patterns
    @Before("execution(* com.example..*.*(..)) && " +
            "args(*, String) && " +
            "this(com.example.service..*)")
    public void complexPointcut(JoinPoint jp) {
        // This will be slow with complex pointcuts
    }
}

Best Practices for Performance

  1. Use Targeted Pointcuts: Avoid matching all methods
  2. Prefer Annotations: Annotation-based pointcuts are faster
  3. Keep Aspect Code Light: Avoid heavy operations in aspects
  4. Cache Results: Use caching aspects for expensive operations

🧪 Testing AOP

@SpringBootTest
public class AspectTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private AuditRepository auditRepository;
    
    @Test
    public void testAuditAspectTriggered() {
        User user = new User("John", "Doe");
        userService.createUser(user);
        
        verify(auditRepository).save(any(AuditLog.class));
    }
    
    @Test
    public void testLoggingAspect() {
        // Should not throw exception
        userService.getUser(1);
    }
}

✅ Summary

AOP in Spring Boot provides a powerful way to:

  • Separate concerns - Keep business logic clean
  • Reduce code duplication - Write once, apply everywhere
  • Improve maintainability - Change logging/security in one place
  • Easy configuration - Simple annotations and pointcut expressions
  • Real-world applications - Logging, caching, security, auditing

Remember: Start simple with @Before and @AfterReturning, then move to advanced features like @Around and combined pointcuts as needed!

Common AOP Use Cases Quick Reference

Logging          → @Before/@After
Performance      → @Around
Caching          → @Around
Security         → @Before
Auditing         → @AfterReturning/@AfterThrowing
Transaction      → @Around
Error Handling   → @AfterThrowing

Happy Coding! 🚀