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.
Aspect-Oriented Programming is a programming paradigm that encapsulates cross-cutting logic into reusable modules called Aspects.
- Code Tangling: Business logic mixed with infrastructure concerns (logging, security)
- Code Scattering: Same logic duplicated across multiple classes
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).
| 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 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 |
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>Note: Spring Boot automatically enables AOP support. No need for
@EnableAspectJAutoProxy.
public interface AccountDAO {
void addAccount();
}
@Component
public class AccountDAOImpl implements AccountDAO {
public void addAccount() {
System.out.println("DOING MY DB WORK: ADDING AN ACCOUNT");
}
}@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();
}
}@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
execution(modifiers-pattern? return-type-pattern declaring-type-pattern?
method-name-pattern(param-pattern) throws-pattern?)
?indicates optional pattern
@Before("execution(public void com.luv2code.aopdemo.dao.AccountDAO.addAccount())")@Before("execution(public void addAccount())")@Before("execution(public void add*())")@Before("execution(* processCreditCard*())")@Before("execution(* addAccount())")@Before("execution(* addAccount(com.luv2code.aopdemo.Account))")@Before("execution(* addAccount(..))")@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")
public void beforeAddAccountAdvice() { ... }
@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")
public void performApiAnalytics() { ... }@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");
}
}- AND (
&&): Both conditions must be true - OR (
||): Either condition must be true - NOT (
!): Negate condition
@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");
}
}When multiple aspects apply to same method, execution order is undefined.
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_VALUEtoInteger.MAX_VALUE. Negative numbers allowed.
@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());
}
}
}
}Runs after method completes successfully (no exceptions).
- Logging
- Audit logging
- Post-processing data
- Enriching/formatting data
@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);
}
}@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!
Runs after method throws an exception.
- Log exceptions
- Audit logging
- Notify DevOps team via email/SMS
- Exception tracking
@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
@Aroundto stop propagation.
Runs after method completes (success or failure) - like finally block.
- Logging regardless of outcome
- Resource cleanup
- Audit logging
@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:
@Afteradvice does NOT have access to exception. Use@AfterThrowingif you need exception details.
Most powerful advice type - complete control over method execution.
- Logging, auditing, security
- Pre/post-processing data
- Instrumentation/profiling (measure execution time)
- Exception management (handle/swallow/rethrow)
@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;
}
}@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;
}@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;
}
}| Feature | JoinPoint | ProceedingJoinPoint |
|---|---|---|
| Used with | @Before, @After, @AfterReturning, @AfterThrowing |
@Around only |
| Purpose | Read method information | Execute target method |
| Methods | getSignature(), getArgs() |
proceed() + JoinPoint methods |
Web Browser → Controller → Service → DAO → Database
↓ ↓ ↓
Logging Aspect (AOP)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>@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);
}
}| 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) |
| 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 |
- Start with Spring AOP - easy to get started
- Move to AspectJ if you need:
- Constructor/field interception
- Better performance
- Non-Spring beans
// ✅ 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!
}// ❌ BAD - Too broad, may catch framework classes
@Before("execution(* com..*.*(..))")
// ✅ GOOD - Specific to your package
@Before("execution(* com.luv2code.aopdemo.dao.*.*(..))")// ✅ GOOD - Reusable pointcut
@Pointcut("execution(* com.luv2code.aopdemo.dao.*.*(..))")
private void forDaoPackage() {}
@Before("forDaoPackage()")
@AfterReturning("forDaoPackage()")@Aspect
@Component
@Order(1) // Runs first
public class SecurityAspect { }
@Aspect
@Component
@Order(2) // Runs second
public class LoggingAspect { }Error: BeanCreationException with mbeanExporter
Solution: Narrow pointcut expression
// Instead of
@Before("execution(* com.luv2code..add*(..))")
// Use
@Before("execution(* com.luv2code.aopdemo.dao.add*(..))")// ❌ 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!
}// ❌ WRONG - ProceedingJoinPoint with @Before
@Before("execution(* addAccount(..))")
public void before(ProceedingJoinPoint pjp) { }
// ✅ CORRECT - Use JoinPoint
@Before("execution(* addAccount(..))")
public void before(JoinPoint jp) { }- Spring Documentation: spring.io
- Book: AspectJ in Action by Ramnivas Laddad
- Book: Aspect-Oriented Development with Use Cases by Ivar Jacobson
// 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;
}
}@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;
}
}
}@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
}
}@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;
}
}
}@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);
}
}@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();
}
}// 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() {}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)")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
}
}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)")@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
}
}- Use Targeted Pointcuts: Avoid matching all methods
- Prefer Annotations: Annotation-based pointcuts are faster
- Keep Aspect Code Light: Avoid heavy operations in aspects
- Cache Results: Use caching aspects for expensive operations
@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);
}
}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!
Logging → @Before/@After
Performance → @Around
Caching → @Around
Security → @Before
Auditing → @AfterReturning/@AfterThrowing
Transaction → @Around
Error Handling → @AfterThrowing
Happy Coding! 🚀