If you’ve ever duplicated the same logging, validation, or security code across multiple services, congratulations — you’ve met cross-cutting concerns. AOP solves this problem beautifully. Instead of scattering boilerplate everywhere, you write the logic once and let Spring weave it into your application automatically. This article will show you how AOP actually works, when to use it, and how it can make your codebase dramatically cleaner.
Aspect-Oriented Programming (AOP) is a programming technique used to separate cross-cutting concerns from the main logic of an application. In simpler terms, it's a way to add extra functionality to certain parts of your code without changing the core logic.
What is a "Cross-Cutting Concern"?
Cross-cutting concerns are tasks that are needed across many parts of an application but are not the main business logic. Common examples include:
Logging: Tracking actions taken by the system, like logging method calls.
Security: Checking if a user has the right permissions.
Transaction Management: Ensuring data consistency in database operations.
Error Handling: Managing exceptions across the application.
What is AOP?
Aspect-Oriented Programming (AOP) is a way to add common functionality to your application without cluttering your business logic. Think of it as a "wrapper" around your methods.
The Problem AOP Solves
Imagine you need to add logging to 50 methods across your application:
Without AOP (Repetitive Code):
public void transferMoney(Account from, Account to, BigDecimal amount) {
logger.info("Starting transfer"); // Repeated in every method
from.debit(amount);
to.credit(amount);
logger.info("Transfer completed"); // Repeated in every method
}
public void createUser(User user) {
logger.info("Starting user creation"); // Repeated again!
userRepository.save(user);
logger.info("User created"); // Repeated again!
}
With AOP (Write Once, Apply Everywhere):
// Just write your business logic
public void transferMoney(Account from, Account to, BigDecimal amount) {
from.debit(amount);
to.credit(amount);
}
public void createUser(User user) {
userRepository.save(user);
}
// AOP automatically adds logging to ALL service methods
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object addLogging(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Starting: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
System.out.println("Completed: " + joinPoint.getSignature().getName());
return result;
}
}
When to Use AOP?
Use AOP for functionality that is needed across many classes:
✅ Good Use Cases:
- Logging method calls
- Measuring performance (how long methods take)
- Security checks (who can call this method?)
- Transaction management
- Exception handling
- Audit logging (tracking what users do)
❌ Don't Use AOP For:
- Business logic specific to one class
- Simple operations
- Code that's only used in 1-2 places
Core Concepts
Let's understand the key terms with simple explanations:
1. Aspect
What: A class that contains your cross-cutting logic (logging, security, etc.)
Think of it as: A helper class that automatically runs at certain points in your code
@Aspect // This marks it as an aspect
@Component // Make it a Spring bean so Spring can use it
public class LoggingAspect {
// Your logging code goes here
}
2. Advice
What: The actual code that runs (the method inside your aspect)
Think of it as: What you want to do (log, check permission, etc.)
@Before("execution(* com.example.service.*.*(..))") // This is advice
public void logMethodCall(JoinPoint joinPoint) {
System.out.println("Method called: " + joinPoint.getSignature().getName());
}
3. Pointcut
What: An expression that defines WHERE your advice should run
Think of it as: A filter that selects which methods to intercept
// This pointcut means: "All methods in the service package"
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
4. Join Point
What: A specific point in your program where an aspect can be applied
In Spring AOP: Always a method execution
Think of it as: The exact moment when your method is called
// When getUserById() is called, that's a join point
public User getUserById(Long id) {
return userRepository.findById(id);
}
5. How It Works (Proxy)
Spring creates a "wrapper" around your beans. When you call a method, it goes through this wrapper first.
Your Code → Spring Proxy (runs AOP logic) → Your Actual Method
Setting Up Spring AOP
Step 1: Add Dependency (Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
That's it! Spring Boot automatically enables AOP when you add this dependency.
Step 2: Create Your First Aspect
@Aspect // Tells Spring this is an aspect
@Component // Makes it a Spring bean
public class MyFirstAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod() {
System.out.println("A method is about to run!");
}
}
What this does:
- Runs before every method in the
com.example.servicepackage - Prints a message each time
Advice Types {#advice-types}
There are 5 types of advice. Think of them as "when" your code should run.
1. @Before - Run Before the Method
Use this when: You want to do something BEFORE a method executes
Common uses: Validation, logging entry, security checks
Example: Validate user input before saving
@Aspect
@Component
public class ValidationAspect {
/**
* This runs BEFORE createUser() method
* Perfect for checking if data is valid
* If validation fails, the main method never runs
*/
@Before("execution(* com.example.service.UserService.createUser(..))")
public void validateUser(JoinPoint joinPoint) {
// Get the method arguments
Object[] args = joinPoint.getArgs();
User user = (User) args[0];
// Check if email is valid
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Email must be valid!");
}
System.out.println("Validation passed for: " + user.getEmail());
}
}
Key Points:
- Runs before the target method
- If this throws an exception, the main method doesn't run
- Cannot access the method's return value
- Cannot prevent the method from running (unless you throw an exception)
2. @After - Run After the Method (Always)
Use this when: You need cleanup code that ALWAYS runs (like a finally block)
Common uses: Closing resources, cleanup operations
Example: Always log when method completes
@Aspect
@Component
public class CleanupAspect {
/**
* Runs AFTER the method completes
* Runs whether method succeeds or fails
* Like a 'finally' block in try-catch-finally
*/
@After("execution(* com.example.service.FileService.*(..))")
public void cleanup(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Method completed: " + methodName);
// Do cleanup - close files, connections, etc.
}
}
Key Points:
- ALWAYS runs (success or failure)
- Cannot access return value
- Cannot prevent exceptions from propagating
3. @AfterReturning - Run After Successful Completion
Use this when: You only care about successful method execution
Common uses: Logging success, processing return values, sending notifications
Example: Log successful operations
@Aspect
@Component
public class SuccessLoggingAspect {
/**
* Only runs if method completes successfully (no exception)
* Can access the return value
* 'returning' parameter name must match method parameter name
*/
@AfterReturning(
pointcut = "execution(* com.example.service.OrderService.createOrder(..))",
returning = "order" // Name must match parameter below
)
public void logSuccessfulOrder(JoinPoint joinPoint, Order order) {
System.out.println("Order created successfully!");
System.out.println("Order ID: " + order.getId());
System.out.println("Amount: $" + order.getAmount());
// Could send email notification here
// Could update cache
// Could trigger other events
}
}
Key Points:
- Only runs on success (no exceptions)
- Can read the return value
- Cannot modify the return value
- Does NOT run if method throws an exception
4. @AfterThrowing - Run When Exception Occurs
Use this when: You need to handle or log exceptions
Common uses: Error logging, sending alerts, exception tracking
Example: Centralized exception logging
@Aspect
@Component
public class ExceptionLoggingAspect {
/**
* Only runs if method throws an exception
* Can access the exception
* Exception still propagates to caller
* 'throwing' parameter must match method parameter name
*/
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "exception" // Name must match parameter below
)
public void logException(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
System.err.println("ERROR in " + className + "." + methodName);
System.err.println("Exception: " + exception.getMessage());
// Could send alert to monitoring system
// Could log to database
// Could send email to admins
}
}
Key Points:
- Only runs when method throws an exception
- Can read the exception
- Cannot suppress the exception (it still propagates)
- Cannot access return value (method didn't return anything)
5. @Around - Full Control (Most Powerful)
Use this when: You need complete control over method execution
Common uses: Performance monitoring, caching, retry logic, modifying behavior
Example 1: Measure method execution time
@Aspect
@Component
public class PerformanceAspect {
/**
* Most powerful advice type
* MUST call joinPoint.proceed() to run the actual method
* Can decide not to call proceed() (skip method execution)
* Can modify arguments before calling proceed()
* Can modify or replace return value
* MUST use ProceedingJoinPoint (not JoinPoint)
*/
@Around("execution(* com.example.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
// Code BEFORE method execution
System.out.println("Starting: " + methodName);
long startTime = System.currentTimeMillis();
// Execute the actual method
Object result = joinPoint.proceed();
// Code AFTER method execution
long duration = System.currentTimeMillis() - startTime;
System.out.println("Completed: " + methodName + " in " + duration + "ms");
// Warn if method is slow
if (duration > 1000) {
System.out.println("WARNING: Slow method detected!");
}
return result; // Return the original result
}
}
Example 2: Simple caching
@Aspect
@Component
public class CachingAspect {
private Map<String, Object> cache = new HashMap<>();
@Around("execution(* com.example.service.ProductService.getProduct(..))")
public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
// Create cache key from method name and arguments
String key = joinPoint.getSignature().getName() + Arrays.toString(joinPoint.getArgs());
// Check if result is in cache
if (cache.containsKey(key)) {
System.out.println("Cache hit! Returning cached result");
return cache.get(key); // Return cached value, skip method execution
}
// Not in cache, execute method
System.out.println("Cache miss. Calling method...");
Object result = joinPoint.proceed();
// Store result in cache
cache.put(key, result);
return result;
}
}
Key Points:
- Most flexible and powerful
- You MUST call
joinPoint.proceed()or the method won't run - Use
ProceedingJoinPoint, notJoinPoint - Must return Object (or throw exception)
- Can modify arguments:
joinPoint.proceed(newArgs) - Can modify return value:
return something different
Pointcut Expressions {#pointcut-expressions}
Pointcut expressions tell Spring WHERE to apply your aspect. Think of it as a filter.
Basic Syntax
execution(modifiers? return-type declaring-type? method-name(params))
Parts explained:
-
execution- keyword (always use this) -
modifiers- public, private (optional, usually skip this) -
return-type- void, String, * (any) -
declaring-type- package and class name (optional) -
method-name- method name or pattern -
params- parameter types
Wildcards
- = any ONE thing (any return type, any method name, any class)
-
..= any number of things (any parameters, any sub-packages)
Common Patterns You'll Actually Use
@Aspect
@Component
public class CommonPointcuts {
/**
* Pattern 1: All methods in a specific class
* Matches: UserService.getUser(), UserService.createUser(), etc.
*/
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// Breakdown:
// * = any return type
// com.example.service.UserService = specific class
// * = any method name
// (..) = any number of parameters
/**
* Pattern 2: All methods in a package
* Matches: All methods in service package (not sub-packages)
*/
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePackage() {}
// Breakdown:
// * = any return type
// com.example.service.* = any class in service package
// * = any method
// (..) = any parameters
/**
* Pattern 3: All methods in package and sub-packages
* Matches: service.UserService, service.admin.AdminService, etc.
*/
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceAndSubPackages() {}
// Note the .. after service - includes all sub-packages
/**
* Pattern 4: Methods starting with 'get'
* Matches: getUser(), getUserById(), getAllUsers(), etc.
*/
@Pointcut("execution(* com.example.service.*.get*(..))")
public void getterMethods() {}
// get* = any method starting with 'get'
/**
* Pattern 5: Methods starting with 'save' or 'update'
* Matches: saveUser(), updateUser(), saveProduct(), etc.
*/
@Pointcut("execution(* com.example.service.*.save*(..)) || " +
"execution(* com.example.service.*.update*(..))")
public void modifyingMethods() {}
// || = OR operator
/**
* Pattern 6: Methods with specific return type
* Matches: Only methods that return User object
*/
@Pointcut("execution(com.example.model.User com.example.service.*.*(..))")
public void methodsReturningUser() {}
/**
* Pattern 7: Methods with specific parameters
* Matches: Only methods with exactly (String, Long) parameters
*/
@Pointcut("execution(* com.example.service.*.*(String, Long))")
public void withStringAndLongParams() {}
/**
* Pattern 8: Methods with no parameters
* Matches: getAllUsers(), refresh(), init(), etc.
*/
@Pointcut("execution(* com.example.service.*.*())")
public void noParameterMethods() {}
// () = exactly zero parameters
/**
* Pattern 9: Using @annotation (RECOMMENDED - Most Readable)
* Matches: Any method with @LogExecutionTime annotation
*/
@Pointcut("@annotation(com.example.annotation.LogExecutionTime)")
public void annotatedMethods() {}
/**
* Pattern 10: Combining pointcuts
* Matches: Service methods except getters
*/
@Pointcut("servicePackage() && !getterMethods()")
public void serviceMethodsExceptGetters() {}
// && = AND operator
// ! = NOT operator
}
Using Annotations (Cleanest Approach)
This is the most readable and maintainable way:
// Step 1: Create custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
// Step 2: Create aspect
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(LogExecutionTime)")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
System.out.println("Took: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
// Step 3: Use it on any method
@Service
public class UserService {
@LogExecutionTime // Just add annotation - clean and clear!
public User createUser(User user) {
return userRepository.save(user);
}
}
Working with JoinPoint
JoinPoint gives you information about the method being called.
What You Can Get from JoinPoint
@Aspect
@Component
public class JoinPointExample {
@Before("execution(* com.example.service.*.*(..))")
public void demonstrateJoinPoint(JoinPoint joinPoint) {
// 1. Get method name
String methodName = joinPoint.getSignature().getName();
System.out.println("Method name: " + methodName);
// 2. Get class name
String className = joinPoint.getTarget().getClass().getSimpleName();
System.out.println("Class name: " + className);
// 3. Get method arguments
Object[] args = joinPoint.getArgs();
System.out.println("Arguments: " + Arrays.toString(args));
// 4. Get full method signature
String fullSignature = joinPoint.getSignature().toShortString();
System.out.println("Full signature: " + fullSignature);
}
}
ProceedingJoinPoint (Only for @Around)
@Around("execution(* com.example.service.*.*(..))")
public Object useProceeding(ProceedingJoinPoint pjp) throws Throwable {
// Has all JoinPoint methods PLUS:
// 1. Execute method with original arguments
Object result = pjp.proceed();
// 2. Execute method with modified arguments
Object[] newArgs = new Object[]{"modified argument"};
result = pjp.proceed(newArgs);
return result;
}
Real-World Use Cases {#use-cases}
Use Case 1: Logging (Most Common)
Goal: Log every method call in your service layer
@Aspect
@Component
public class LoggingAspect {
/**
* Logs method entry, exit, duration, and exceptions
* Applies to all service layer methods
*/
@Around("execution(* com.example.service..*.*(..))")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
// Log entry
System.out.println("→ Entering " + className + "." + methodName);
long startTime = System.currentTimeMillis();
try {
// Execute method
Object result = joinPoint.proceed();
// Log successful exit
long duration = System.currentTimeMillis() - startTime;
System.out.println("← Exiting " + className + "." + methodName +
" | Duration: " + duration + "ms");
return result;
} catch (Exception e) {
// Log exception
System.err.println("✗ Exception in " + className + "." + methodName +
": " + e.getMessage());
throw e; // Re-throw so caller knows about error
}
}
}
Output example:
→ Entering UserService.createUser
← Exiting UserService.createUser | Duration: 45ms
Use Case 2: Security Check
Goal: Check if user has permission before executing method
// Step 1: Create annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String value(); // The required role
}
// Step 2: Create aspect
@Aspect
@Component
public class SecurityAspect {
@Autowired
private SecurityService securityService; // Your security service
/**
* Checks if user has required role before method execution
* Throws exception if user lacks permission
*/
@Before("@annotation(requiresRole)")
public void checkSecurity(RequiresRole requiresRole) {
String requiredRole = requiresRole.value();
String currentUser = securityService.getCurrentUser();
if (!securityService.hasRole(currentUser, requiredRole)) {
throw new SecurityException(
"User " + currentUser + " does not have role: " + requiredRole
);
}
System.out.println("Security check passed for: " + currentUser);
}
}
// Step 3: Use it
@Service
public class UserService {
@RequiresRole("ADMIN") // Only admins can delete users
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
@RequiresRole("USER") // Any logged-in user can view their profile
public User getProfile(Long userId) {
return userRepository.findById(userId).orElse(null);
}
}
Use Case 3: Performance Monitoring
Goal: Alert when methods are running slowly
@Aspect
@Component
public class PerformanceMonitor {
private static final long SLOW_THRESHOLD = 1000; // 1 second
/**
* Measures execution time and alerts on slow methods
* Helps identify performance bottlenecks
*/
@Around("execution(* com.example.service..*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = pjp.getSignature().getName();
Object result = pjp.proceed();
long executionTime = System.currentTimeMillis() - startTime;
if (executionTime > SLOW_THRESHOLD) {
System.out.println("⚠️ SLOW METHOD: " + methodName +
" took " + executionTime + "ms");
// Could send alert to monitoring system
}
return result;
}
}
Use Case 4: Automatic Retry
Goal: Automatically retry failed operations (useful for network calls)
// Step 1: Create annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int maxAttempts() default 3;
long delayMs() default 1000;
}
// Step 2: Create aspect
@Aspect
@Component
public class RetryAspect {
/**
* Automatically retries failed methods
* Useful for network calls, external APIs
*/
@Around("@annotation(retry)")
public Object retryOnFailure(ProceedingJoinPoint pjp, Retry retry) throws Throwable {
int maxAttempts = retry.maxAttempts();
long delay = retry.delayMs();
int attempt = 1;
while (attempt <= maxAttempts) {
try {
// Try to execute method
return pjp.proceed();
} catch (Exception e) {
if (attempt == maxAttempts) {
// Last attempt failed, give up
System.err.println("Failed after " + maxAttempts + " attempts");
throw e;
}
// Retry
System.out.println("Attempt " + attempt + " failed, retrying...");
Thread.sleep(delay);
attempt++;
}
}
throw new RuntimeException("Should not reach here");
}
}
// Step 3: Use it
@Service
public class PaymentService {
@Retry(maxAttempts = 5, delayMs = 2000)
public PaymentResult processPayment(PaymentRequest request) {
// Call external payment gateway
// Automatically retries if it fails
return paymentGateway.charge(request);
}
}
Use Case 5: Audit Logging
Goal: Track all important user actions for compliance
// Step 1: Create annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
String action(); // What action is performed
}
// Step 2: Create aspect
@Aspect
@Component
public class AuditAspect {
@Autowired
private AuditRepository auditRepository;
/**
* Records all auditable actions to database
* Required for compliance (GDPR, SOX, HIPAA)
*/
@AfterReturning(
pointcut = "@annotation(auditable)",
returning = "result"
)
public void auditAction(JoinPoint jp, Auditable auditable, Object result) {
String username = getCurrentUsername();
String action = auditable.action();
String details = Arrays.toString(jp.getArgs());
AuditLog log = new AuditLog();
log.setUsername(username);
log.setAction(action);
log.setDetails(details);
log.setTimestamp(LocalDateTime.now());
log.setSuccess(true);
auditRepository.save(log);
System.out.println("Audit logged: " + username + " performed " + action);
}
private String getCurrentUsername() {
// Get from SecurityContext or session
return "currentUser";
}
}
// Step 3: Use it
@Service
public class UserService {
@Auditable(action = "CREATE_USER")
public User createUser(User user) {
return userRepository.save(user);
}
@Auditable(action = "DELETE_USER")
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}
Database will have records like:
Username | Action | Details | Timestamp
------------|--------------|--------------|------------------
john.doe | CREATE_USER | [User{...}] | 2024-10-18 10:30
admin | DELETE_USER | [123] | 2024-10-18 10:35
Use Case 6: Input Validation
Goal: Validate method arguments before execution
@Aspect
@Component
public class ValidationAspect {
/**
* Validates User objects passed to any service method
* Throws exception if validation fails
*/
@Before("execution(* com.example.service.*.*(com.example.model.User, ..))")
public void validateUser(JoinPoint jp) {
// Get the User argument (first parameter)
Object[] args = jp.getArgs();
User user = (User) args[0];
// Validate
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Valid email required");
}
if (user.getAge() != null && (user.getAge() < 0 || user.getAge() > 150)) {
throw new IllegalArgumentException("Age must be between 0 and 150");
}
System.out.println("Validation passed for user: " + user.getEmail());
}
}
Common Mistakes to Avoid {#mistakes}
Mistake 1: Calling Methods on 'this'
Problem: AOP doesn't work when you call another method in the same class
@Service
public class UserService {
@LogExecutionTime
public void methodA() {
this.methodB(); // ❌ AOP won't work on methodB
}
@LogExecutionTime
public void methodB() {
// Logic
}
}
Why? Spring uses proxies. When you call this.methodB(), you bypass the proxy.
Solution: Move methodB to a different service
@Service
public class UserService {
@Autowired
private UserHelper helper;
@LogExecutionTime
public void methodA() {
helper.methodB(); // ✅ Works! External call goes through proxy
}
}
@Service
public class UserHelper {
@LogExecutionTime
public void methodB() {
// Logic
}
}
Mistake 2: Using AOP on Private Methods
Problem: AOP doesn't work on private methods
@Service
public class UserService {
@LogExecutionTime // ❌ Won't work - method is private
private void privateMethod() {
// Logic
}
}
Solution: Make method public or protected
@Service
public class UserService {
@LogExecutionTime // ✅ Works - method is public
public void publicMethod() {
// Logic
}
}
Mistake 3: Forgetting to Call proceed() in @Around
Problem: Method doesn't execute
@Around("execution(* com.example.service.*.*(..))")
public Object forgotProceed(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Before");
// ❌ Forgot to call pjp.proceed()
return null; // Method never runs!
}
Solution: Always call proceed()
@Around("execution(* com.example.service.*.*(..))")
public Object correctUsage(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Before");
Object result = pjp.proceed(); // ✅ Method executes
System.out.println("After");
return result;
}
Mistake 4: Too Broad Pointcut Expressions
Problem: Aspect applies to too many methods, causing performance issues
@Before("execution(* *.*(..))") // ❌ Matches EVERY method in entire application!
public void logEverything(JoinPoint jp) {
System.out.println(jp.getSignature().getName());
}
Solution: Be specific
@Before("execution(* com.example.service.*.*(..))") // ✅ Only service layer
public void logServiceMethods(JoinPoint jp) {
System.out.println(jp.getSignature().getName());
}
Mistake 5: Not Handling Exceptions in @Around
Problem: Exceptions get swallowed
@Around("execution(* com.example.service.*.*(..))")
public Object badExceptionHandling(ProceedingJoinPoint pjp) {
try {
return pjp.proceed();
} catch (Throwable e) {
System.out.println("Error occurred");
return null; // ❌ Exception is swallowed! Caller doesn't know about error
}
}
Solution: Re-throw exceptions
@Around("execution(* com.example.service.*.*(..))")
public Object goodExceptionHandling(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} catch (Exception e) {
System.out.println("Error occurred: " + e.getMessage());
throw e; // ✅ Re-throw so caller knows about error
}
}
Best Practices {#best-practices}
1. Use Annotations for Pointcuts (Most Readable)
Good:
// Clear and explicit
@LogExecutionTime
public void createUser(User user) {
userRepository.save(user);
}
Avoid:
// Hard to know which methods have logging
// Need to check aspect class
@Pointcut("execution(* com.example.service.UserService.create*(..))")
2. One Aspect = One Concern
Good:
@Aspect
@Component
public class LoggingAspect {
// Only logging logic
}
@Aspect
@Component
public class SecurityAspect {
// Only security logic
}
Avoid:
@Aspect
@Component
public class EverythingAspect {
// Logging, security, caching, validation - too much!
}
3. Be Specific with Pointcuts
Good:
// Specific - only user service
@Pointcut("execution(* com.example.service.UserService.*(..))")
Avoid:
// Too broad - entire application
@Pointcut("execution(* *.*(..))")
4. Reuse Pointcut Definitions
Good:
@Aspect
@Component
public class CommonPointcuts {
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceLayer() {}
}
// Use in multiple aspects
@Before("com.example.aspect.CommonPointcuts.serviceLayer()")
public void logBefore() { }
@After("com.example.aspect.CommonPointcuts.serviceLayer()")
public void logAfter() { }
Avoid:
// Repeating same pointcut everywhere
@Before("execution(* com.example.service..*.*(..))")
public void logBefore() { }
@After("execution(* com.example.service..*.*(..))")
public void logAfter() { }
5. Use @Around Sparingly
Use @Around only when you need:
- Modify arguments
- Modify return value
- Decide whether to execute method
- Measure execution time
- Implement caching
For simple cases, use:
- @Before for pre-processing
- @AfterReturning for post-processing
- @AfterThrowing for error handling
Good:
// Simple case - use @Before
@Before("execution(* save*(..))")
public void validateInput(JoinPoint jp) {
// Validation logic
}
Avoid:
// Overkill - @Around not needed
@Around("execution(* save*(..))")
public Object validateInput(ProceedingJoinPoint pjp) throws Throwable {
// Just validation, but using @Around
return pjp.proceed();
}
6. Order Multiple Aspects
When multiple aspects apply to same method, control execution order:
@Aspect
@Component
@Order(1) // Executes first
public class SecurityAspect {
// Check security first
}
@Aspect
@Component
@Order(2) // Executes second
public class LoggingAspect {
// Log after security check passes
}
@Aspect
@Component
@Order(3) // Executes third
public class TransactionAspect {
// Start transaction after logging
}
Lower numbers = Higher priority = Executes first
7. Document Your Aspects
Add clear comments explaining what the aspect does:
@Aspect
@Component
public class AuditAspect {
/**
* Logs all user actions for compliance requirements.
*
* Applies to: All methods annotated with @Auditable
* Saves to: audit_log table
* Used for: GDPR compliance, security audits
*
* Example:
* @Auditable(action = "DELETE_USER")
* public void deleteUser(Long id)
*/
@AfterReturning(pointcut = "@annotation(auditable)", returning = "result")
public void auditAction(JoinPoint jp, Auditable auditable, Object result) {
// Implementation
}
}
8. Test Your Aspects
Always test that aspects work correctly:
@SpringBootTest
public class LoggingAspectTest {
@Autowired
private UserService userService;
@Test
public void testLoggingAspect() {
// Capture console output
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
// Execute method
userService.createUser(new User("test@example.com"));
// Verify logging happened
String output = outContent.toString();
assertTrue(output.contains("Entering UserService.createUser"));
assertTrue(output.contains("Exiting UserService.createUser"));
}
}
Complete Example: E-Commerce Application
Let's see how everything works together in a real application:
// ============= MODELS =============
public class Order {
private Long id;
private String customerId;
private BigDecimal amount;
private String status;
// getters and setters
}
// ============= CUSTOM ANNOTATIONS =============
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
String action();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
// ============= ASPECTS =============
// Security Aspect - Runs First
@Aspect
@Component
@Order(1)
public class SecurityAspect {
@Autowired
private SecurityService securityService;
@Before("@annotation(requiresRole)")
public void checkSecurity(RequiresRole requiresRole) {
String requiredRole = requiresRole.value();
if (!securityService.hasRole(requiredRole)) {
throw new SecurityException("Access denied. Required role: " + requiredRole);
}
}
}
// Logging Aspect - Runs Second
@Aspect
@Component
@Order(2)
public class LoggingAspect {
@Around("@annotation(LogExecutionTime)")
public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
System.out.println("→ Starting: " + methodName);
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println("← Completed: " + methodName + " in " + duration + "ms");
return result;
}
}
// Audit Aspect - Runs Third
@Aspect
@Component
@Order(3)
public class AuditAspect {
@Autowired
private AuditRepository auditRepository;
@AfterReturning(pointcut = "@annotation(auditable)", returning = "result")
public void audit(JoinPoint jp, Auditable auditable, Object result) {
AuditLog log = new AuditLog();
log.setAction(auditable.action());
log.setUser(getCurrentUser());
log.setTimestamp(LocalDateTime.now());
log.setDetails("Method: " + jp.getSignature().getName());
auditRepository.save(log);
System.out.println("Audit logged: " + auditable.action());
}
private String getCurrentUser() {
return "currentUser"; // Get from security context
}
}
// ============= SERVICE =============
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
/**
* Create new order
* 1. Security check - ensures user has USER role
* 2. Logs execution time
* 3. Audits the action
*/
@RequiresRole("USER")
@LogExecutionTime
@Auditable(action = "CREATE_ORDER")
public Order createOrder(Order order) {
order.setStatus("PENDING");
return orderRepository.save(order);
}
/**
* Cancel order
* 1. Security check - only ADMIN can cancel
* 2. Logs execution time
* 3. Audits the action
*/
@RequiresRole("ADMIN")
@LogExecutionTime
@Auditable(action = "CANCEL_ORDER")
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus("CANCELLED");
orderRepository.save(order);
}
/**
* Get order details
* 1. Security check - any logged-in user
* 2. Logs execution time
* 3. No audit (read operations typically not audited)
*/
@RequiresRole("USER")
@LogExecutionTime
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElse(null);
}
}
When createOrder() is called, this happens:
1. SecurityAspect checks if user has "USER" role
↓
2. LoggingAspect starts timer and logs "Starting: createOrder"
↓
3. Actual createOrder() method executes
↓
4. LoggingAspect logs "Completed: createOrder in 45ms"
↓
5. AuditAspect saves audit log with action "CREATE_ORDER"
Console Output:
Security check passed for user: john.doe
→ Starting: createOrder
← Completed: createOrder in 45ms
Audit logged: CREATE_ORDER
Summary
Key Takeaways
- AOP separates cross-cutting concerns - Keeps business logic clean
-
5 Advice Types:
-
@Before- Before method execution -
@After- After method (always) -
@AfterReturning- After successful execution -
@AfterThrowing- When exception occurs -
@Around- Full control (most powerful)
-
-
Common Uses:
- Logging
- Security
- Performance monitoring
- Audit logging
- Retry logic
- Validation
-
Best Practices:
- Use custom annotations for clarity
- Keep aspects focused (one concern per aspect)
- Be specific with pointcut expressions
- Test your aspects
- Document what each aspect does
-
Common Mistakes:
- Don't call methods on
this(won't work) - AOP doesn't work on private methods
- Always call
proceed()in@Around - Don't make pointcuts too broad
- Don't call methods on
When to Use AOP
✅ Use AOP when:
- Same logic is needed across many classes
- You want to keep business logic clean
- For logging, security, monitoring
- Cross-cutting concerns that affect multiple layers
❌ Don't use AOP when:
- Logic is specific to one class
- Simple operations
- When straightforward code is clearer
Next Steps
- Add Spring AOP dependency to your project
- Start with simple logging aspect
- Create custom annotations for your use cases
- Test thoroughly
- Add more aspects as needed
Remember: AOP is powerful but use it wisely. Not everything needs to be an aspect!
Quick Reference Card
// Setup
@Aspect
@Component
public class MyAspect { }
// Before advice
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint jp) { }
// After advice
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint jp) { }
// After returning
@AfterReturning(pointcut = "...", returning = "result")
public void afterReturning(JoinPoint jp, Object result) { }
// After throwing
@AfterThrowing(pointcut = "...", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) { }
// Around advice
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
return result;
}
// Common pointcuts
execution(* com.example.service.*.*(..)) // All methods in package
execution(* com.example.service..*.*(..)) // Including sub-packages
execution(* get*(..)) // Methods starting with 'get'
@annotation(LogExecutionTime) // Methods with annotation
This completes your comprehensive Spring AOP guide! 🎉
Top comments (0)