4.1 ~ 4.4 AOP (Aspect-Oriented Programming)
1. Why AOP? — Separating Cross-cutting Concerns
While writing clean code, you quickly encounter a frustrating problem: boilerplate code like logging, execution-time measurement, and transaction management keeps repeating across every method.
public class OrderService {
public void placeOrder(String item) {
long start = System.currentTimeMillis(); // Common code ①
System.out.println("[LOG] placeOrder started"); // Common code ②
// ✅ Core business logic (just one line)
System.out.println(item + " order placed!");
long end = System.currentTimeMillis(); // Common code ①
System.out.println("[LOG] Execution time: " + (end - start) + "ms"); // Common code ②
}
public void cancelOrder(String item) {
long start = System.currentTimeMillis(); // Common code ①
System.out.println("[LOG] cancelOrder started"); // Common code ②
// ✅ Core business logic (just one line)
System.out.println(item + " order cancelled!");
long end = System.currentTimeMillis(); // Common code ①
System.out.println("[LOG] Execution time: " + (end - start) + "ms"); // Common code ②
}
}
The actual business logic is just one line, but the repeated boilerplate bloats every method. With 100 methods, you'd need to update 100 places for a single change.
AOP (Aspect-Oriented Programming) addresses this by completely separating Core Concerns(business logic) from ** Cross-cutting Concerns**(logging, transactions, etc.) that repeatedly appear alongside them.
2. AOP Key Terminology
| Term | Description | Example |
|---|---|---|
| Aspect | A module encapsulating a cross-cutting concern | An @Aspect class |
| Target | The actual business object where AOP is applied | OrderService |
| Advice | The actual shared code to be executed (the "what") | Logging, time measurement |
| Pointcut | An expression that defines where Advice is applied (the "where") | execution(* com.example..*(..)) |
| JoinPoint | A point in program execution where Advice can be applied | The moment placeOrder() is called |
| Weaving | The process of linking an Aspect to a Target | Spring creating a proxy |
Types of Advice (the "when")
| Type | Description |
|---|---|
@Before | Runs before the method executes |
@AfterReturning | Runs after the method returns normally |
@AfterThrowing | Runs after the method throws an exception |
@After | Runs regardless of outcome (like finally) |
@Around | Controls both before and after the method (most powerful) |
@AfterReturning | Runs after the method returns successfully to process the result |
@AfterThrowing | Runs after an exception is thrown to handle error info |
Pointcut Expression Guide
How do you write a pointcut like execution(* com.example.service.*.*(..))?
*: Matches any return type / method name / package path..: Matches zero or more parameters / all sub-packages(..): Matches any number of parametersexecution: Matches method execution join points@annotation: Matches only methods with a specific annotation
3. Spring AOP in Practice (Execution Time Tracking)
With AOP, we can completely remove boilerplate from business logic.
Step 1: Add the dependency
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
Step 2: Write the Aspect class
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect // Declares this class as an AOP Aspect
@Component // Registers it as a Spring Bean
public class TimeTraceAop {
// Pointcut: apply to all methods in com.example and subpackages
@Around("execution(* com.example..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("[AOP] START: " + joinPoint.toString());
try {
return joinPoint.proceed(); // ✅ Calls the actual business method
} finally {
long finish = System.currentTimeMillis();
System.out.println("[AOP] END: " + joinPoint.toString() + " " + (finish - start) + "ms");
}
}
}
Step 3: Keep business logic pure
@Service
public class OrderService {
public void placeOrder(String item) {
// ✅ Only core logic remains!
System.out.println(item + " order placed!");
}
public void cancelOrder(String item) {
// ✅ Clean and focused on its single responsibility!
System.out.println(item + " order cancelled!");
}
}
TimeTraceAop monitors all methods and measures time automatically, so OrderService can focus purely on its own responsibility — handling orders.
4. Proxy Pattern: How It Works Under the Hood
You might wonder how AOP works its magic. Spring AOP is built on the Runtime Dynamic Proxy pattern.
Client Code Spring-injected Proxy Object Real Object
OrderController --> OrderService (Proxy) [AOP code inside] --> OrderService (Real)
- When Spring detects
@Aspect, it does not inject the realOrderServicedirectly. Instead, it injects a proxy object that wraps the real one. - The client calls methods on the proxy, unaware it is not the real class.
- The proxy executes the Advice (shared code) first, then delegates to the real object's method.
Summary: AOP is an elegant technique that transparently injects cross-cutting behavior without ever touching the business code. Spring's
@Transactionalannotation is itself implemented using this exact AOP proxy mechanism.
5. Practical Tip: Combining Custom Annotations with AOP
When you want to apply AOP selectively to specific methods rather than an entire package, use Custom Annotations.
Step 1: Create the annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime { } // Used simply as a marker
Step 2: Detect the annotation in the Aspect
@Around("@annotation(LogExecutionTime)") // Only for methods with this annotation!
public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in: " + executionTime + "ms");
return proceed;
}
Step 3: Apply to the desired method
@LogExecutionTime
public void heavyJob() {
// Perform logic...
}