Aspect Oriented Programming
Aspec Oriented Programming
In enterprise applications, certain tasks like logging, transaction management, and data validation are needed across various objects and modules. These tasks, known as crosscutting concerns, affect many parts of the application. In Object-Oriented Programming (OOP), we organize code using classes. In Aspect-Oriented Programming (AOP), we use aspects to handle these crosscutting tasks, and they apply to multiple classes. Spring AOP helps remove the need for classes to directly handle crosscutting tasks, something that regular OOP can’t do. Spring and Spring Boot offer strong AOP support, enabling you to manage concerns like logging, security, and transactions separately from your main application logic.
What is AOP?
- AOP allows you to dynamically integrate cross-cutting concerns like logging, security, or transactions into your code before, after, or around your core logic using simple, pluggable configurations. This means you can add or change functionality without touching the main business logic.
- AOP helps separate non-business logic concerns such as logging, auditing, security, and transaction management from the core code. This keeps your business logic clean and easier to maintain by centralizing these repetitive tasks into reusable components.
- AOP is a programming approach designed to improve modularity by separating cross-cutting concerns. It works by adding extra behaviors (e.g., logging or security checks) to your code without actually modifying the core code itself, making the system more modular and maintainable.
Key Terminology in AOP (AOP Jargons)
When defining an aspect or setting up configurations, we need to follow the "WWW" approach.
- WHAT -> Aspect
- WHEN -> Advice
- WHICH -> Pointcut
- WHAT code or logic we want the spring to execute when you call a specific metho. This is called as Aspect.
- WHEN the spring need to execute the given Aspect. For example is it before or after method call. This is called as Advice.
- WHICH method inside app that framework needs to intercept and execute the given Aspect. This is called as a Pointcut.
- Join Point is a specific point in the execution of a program, such as a method call, constructor invocation, or field access.
Advice
This is the action taken by an aspect at a particular join point. There are five types of advice:
- Before : Executed before the method call.
- After : Executed after the method call, regardless of its outcome.
- AfterReturning : Executed after the method returns a result, but not if an exception occurs.
- Around : Surrounds the method execution, allowing you to control the method execution and its result.
- AfterThrowing : Executed if the method throws an exception.
Writing Expressions in AOP
In Spring AOP, expressions are used to define pointcuts, which specify where advice should be applied. These expressions are typically written using AspectJ expression language.
1. Basic Structure of Pointcut Expressions
- Execution: This specifies a method execution join point.
- Modifiers: You can specify the access level (e.g., public, protected).
- Return Type: Define what the method returns.
- Method Name: Specify the method to match.
- Parameters: Define what parameters the method can accept.
- Package Name: Specify the package where the method resides.
2. Common Expression Patterns
- Match All Methods in a Package
@Pointcut("execution(* com.example.service.*.*(..))")This expression matches all methods in any class within the com.example.service package.
- Match Specific Method
@Pointcut("execution(void com.example.service.MyService.performTask())")This matches the performTask method in the MyService class.
- Match All Methods with Specific Return Type
@Pointcut("execution(String com.example.service.*.get*(..))")This matches all methods in any class in the package that start with "get" and return a String.
- Match Methods with Specific Parameters
@Pointcut("execution(* com.example.service.*.addUser(String, ..))")This matches the addUser method that takes a String as the first parameter, regardless of the second parameter type.
- Match Methods with Annotations
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")This matches any method annotated with @GetMapping.
- Combining Pointcuts
@Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.repository.*.*(..))")This expression matches all methods in both the service and repository packages.
3. Using Annotatio Example
Example: Using Pointcut Expressions in Advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// Pointcut expression to match all methods in the service package
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// Advice that runs before the matched methods
@Before("serviceMethods()")
public void logBefore() {
System.out.println("Log: A service method is about to be executed.");
}
}
AOP Proxies in Spring
What is Proxy?
A proxy is an object that represents another object. In Spring AOP, proxies are used to apply aspects to target objects
without modifying the actual code of those objects. Proxies act as intermediaries that handle method calls to the target
objects and enable the application of cross-cutting concerns like logging, security, or transactions..
Types of Proxies
Spring AOP can create two types of proxies:
- JDK Dynamic Proxy: This proxy is created when the target object implements at least one interface.
The proxy will implement the same interfaces and delegate calls to the actual object.
- CGLIB Proxy: This proxy is used when the target object does not implement any interfaces.
It creates a subclass of the target class and overrides its methods to add the AOP functionality.
When to Use Which Proxy
- JDK Dynamic Proxy: Use this when the target object implements one or more interfaces.
- CGLIB Proxy: Your target class does not implement any interfaces.
You specifically need to proxy the class methods directly (e.g., when using final classes or methods).
Combining It All with Example
1. Logging
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethodExecution(JoinPoint joinPoint) {
System.out.println("Entering method: " + joinPoint.getSignature().getName());
}
@AfterReturning("execution(* com.example.service.*.*(..))")
public void logAfterMethodExecution(JoinPoint joinPoint) {
System.out.println("Exiting method: " + joinPoint.getSignature().getName());
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void logAfterException(JoinPoint joinPoint, Throwable error) {
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " with error: " + error.getMessage());
}
}
2. Transaction Management
@Aspect
@Component
public class TransactionAspect {
@Autowired
private PlatformTransactionManager transactionManager;
@Around("execution(* com.example.service.*.*(..))")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
Object result;
try {
result = joinPoint.proceed();
transactionManager.commit(status);
} catch (Throwable ex) {
transactionManager.rollback(status);
throw ex;
}
return result;
}
}
3. Security
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.service.*.*(..)) && @annotation(Secured)")
public void checkSecurity(JoinPoint joinPoint) {
// Security logic here
if (!isUserAuthorized()) {
throw new SecurityException("User is not authorized to perform this operation.");
}
}
private boolean isUserAuthorized() {
// Implement your security check logic
return true; // Dummy implementation
}
}
4. Performance Monitoring
@Aspect
@Component
public class PerformanceMonitoringAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Execution time of " + joinPoint.getSignature().getName() + " : " + (endTime - startTime) + " ms");
return result;
}
}
5. Exception handling
@Aspect
@Component
public class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void handleException(JoinPoint joinPoint, Throwable ex) {
// Centralized exception handling logic
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " with message: " + ex.getMessage());
}
}
6. Audit Logging
@Aspect
@Component
public class AuditLoggingAspect {
@After("execution(* com.example.service.*.*(..)) && @annotation(Auditable)")
public void logAudit(JoinPoint joinPoint) {
// Audit logging logic
System.out.println("Audit log for method: " + joinPoint.getSignature().getName());
}
}
What is Proxy?
A proxy is an object that represents another object. In Spring AOP, proxies are used to apply aspects to target objects without modifying the actual code of those objects. Proxies act as intermediaries that handle method calls to the target objects and enable the application of cross-cutting concerns like logging, security, or transactions..Types of Proxies
Spring AOP can create two types of proxies:- JDK Dynamic Proxy: This proxy is created when the target object implements at least one interface. The proxy will implement the same interfaces and delegate calls to the actual object.
- CGLIB Proxy: This proxy is used when the target object does not implement any interfaces. It creates a subclass of the target class and overrides its methods to add the AOP functionality.
When to Use Which Proxy
- JDK Dynamic Proxy: Use this when the target object implements one or more interfaces.
- CGLIB Proxy: Your target class does not implement any interfaces. You specifically need to proxy the class methods directly (e.g., when using final classes or methods).
Combining It All with Example
1. Logging
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethodExecution(JoinPoint joinPoint) {
System.out.println("Entering method: " + joinPoint.getSignature().getName());
}
@AfterReturning("execution(* com.example.service.*.*(..))")
public void logAfterMethodExecution(JoinPoint joinPoint) {
System.out.println("Exiting method: " + joinPoint.getSignature().getName());
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void logAfterException(JoinPoint joinPoint, Throwable error) {
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " with error: " + error.getMessage());
}
}
2. Transaction Management
@Aspect
@Component
public class TransactionAspect {
@Autowired
private PlatformTransactionManager transactionManager;
@Around("execution(* com.example.service.*.*(..))")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
Object result;
try {
result = joinPoint.proceed();
transactionManager.commit(status);
} catch (Throwable ex) {
transactionManager.rollback(status);
throw ex;
}
return result;
}
}
3. Security
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.service.*.*(..)) && @annotation(Secured)")
public void checkSecurity(JoinPoint joinPoint) {
// Security logic here
if (!isUserAuthorized()) {
throw new SecurityException("User is not authorized to perform this operation.");
}
}
private boolean isUserAuthorized() {
// Implement your security check logic
return true; // Dummy implementation
}
}
4. Performance Monitoring
@Aspect
@Component
public class PerformanceMonitoringAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Execution time of " + joinPoint.getSignature().getName() + " : " + (endTime - startTime) + " ms");
return result;
}
}
5. Exception handling
@Aspect
@Component
public class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void handleException(JoinPoint joinPoint, Throwable ex) {
// Centralized exception handling logic
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " with message: " + ex.getMessage());
}
}
6. Audit Logging
@Aspect
@Component
public class AuditLoggingAspect {
@After("execution(* com.example.service.*.*(..)) && @annotation(Auditable)")
public void logAudit(JoinPoint joinPoint) {
// Audit logging logic
System.out.println("Audit log for method: " + joinPoint.getSignature().getName());
}
}
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethodExecution(JoinPoint joinPoint) {
System.out.println("Entering method: " + joinPoint.getSignature().getName());
}
@AfterReturning("execution(* com.example.service.*.*(..))")
public void logAfterMethodExecution(JoinPoint joinPoint) {
System.out.println("Exiting method: " + joinPoint.getSignature().getName());
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void logAfterException(JoinPoint joinPoint, Throwable error) {
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " with error: " + error.getMessage());
}
}
@Aspect
@Component
public class TransactionAspect {
@Autowired
private PlatformTransactionManager transactionManager;
@Around("execution(* com.example.service.*.*(..))")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
Object result;
try {
result = joinPoint.proceed();
transactionManager.commit(status);
} catch (Throwable ex) {
transactionManager.rollback(status);
throw ex;
}
return result;
}
}
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.service.*.*(..)) && @annotation(Secured)")
public void checkSecurity(JoinPoint joinPoint) {
// Security logic here
if (!isUserAuthorized()) {
throw new SecurityException("User is not authorized to perform this operation.");
}
}
private boolean isUserAuthorized() {
// Implement your security check logic
return true; // Dummy implementation
}
}
@Aspect
@Component
public class PerformanceMonitoringAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Execution time of " + joinPoint.getSignature().getName() + " : " + (endTime - startTime) + " ms");
return result;
}
}
@Aspect
@Component
public class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void handleException(JoinPoint joinPoint, Throwable ex) {
// Centralized exception handling logic
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " with message: " + ex.getMessage());
}
}
@Aspect
@Component
public class AuditLoggingAspect {
@After("execution(* com.example.service.*.*(..)) && @annotation(Auditable)")
public void logAudit(JoinPoint joinPoint) {
// Audit logging logic
System.out.println("Audit log for method: " + joinPoint.getSignature().getName());
}
}