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! 🚀