AOP & proxy: thêm hành vi mà không sửa method
Tiếp đi, giờ tới AOP. Mình nghe @Transactional, @Async đều chạy bằng proxy?
Đúng vậy. Ở bài trước ta thấy class bean là PaymentService$$SpringCGLIB$$0. Bài này giải thích proxy đó là gì và vì sao nó là nền móng của @Transactional, @Async, @Cacheable, @PreAuthorize.
1. Bài toán: log thời gian chạy mọi method service
Team muốn biết createOrder, charge, sendOrderConfirmation chạy bao lâu. Cách đầu tiên:
public Order createOrder(CreateOrderRequest req) {
long start = System.currentTimeMillis();
try {
Order order = orderRepository.save(Order.from(req));
paymentService.charge(order);
emailService.sendOrderConfirmation(order);
return order;
} finally {
log.info("createOrder took {}ms", System.currentTimeMillis() - start);
}
}
2. Vấn đề
- Copy đoạn đo thời gian vào 100 method. Đổi format log là sửa 100 chỗ.
- Business code lẫn với code hạ tầng (log, transaction, security, retry).
- Quên một method là mất số liệu.
Những việc "cắt ngang" nhiều class như vậy gọi là cross-cutting concern.
3. Giải pháp: AOP
| Khái niệm | Nghĩa | Ví dụ |
|---|---|---|
| Aspect | Module chứa logic cross-cutting | TimingAspect |
| Join point | Điểm có thể chen vào (Spring: lời gọi method) | OrderService.createOrder() |
| Pointcut | Biểu thức chọn join point | within(com.example.shop..*Service) |
| Advice | Code chạy ở join point | @Before, @After, @AfterReturning, @AfterThrowing, @Around |
| Proxy | Object bọc bean thật, chạy advice rồi gọi bean | OrderService$$SpringCGLIB$$0 |
4. Code từng bước
Bước 1: Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Boot tự bật auto-proxy, không cần @EnableAspectJAutoProxy.
Bước 2: Viết Aspect
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("within(com.example.shop..*) && @within(org.springframework.stereotype.Service)")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // gọi method thật
} finally {
long ms = (System.nanoTime() - start) / 1_000_000;
log.info("{} took {}ms", pjp.getSignature().toShortString(), ms);
}
}
}
Bước 3: Code business không đổi gì
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest req) {
Order order = orderRepository.save(Order.from(req));
paymentService.charge(order);
emailService.sendOrderConfirmation(order);
return order;
}
}
Bước 4: Test
curl -X POST localhost:8080/orders -H "Content-Type: application/json" -d '{"productId":1,"quantity":2}'
# log:
PaymentService.charge(..) took 180ms
EmailService.sendOrderConfirmation(..) took 40ms
OrderService.createOrder(..) took 235ms
5. Cách hoạt động
OrderController
| orderService.createOrder(req)
v
┌───────────────────────────────┐
│ OrderService PROXY (CGLIB) │
│ TimingAspect: start timer │
│ pjp.proceed() ──────────────┼──→ OrderService THẬT.createOrder()
│ TimingAspect: log time │
└───────────────────────────────┘
|
v
return order
Controller được inject proxy, không phải object thật. Proxy được tạo ở bước postProcessAfterInitialization (bài 4.1.2).
JDK dynamic proxy và CGLIB
JDK proxy: class $Proxy42 implements PaymentGateway { ... }
→ chỉ proxy được method của INTERFACE
→ inject theo class VnpayGateway sẽ lỗi
CGLIB: class OrderService$$SpringCGLIB$$0 extends OrderService { ... }
→ subclass, override method
→ không proxy được method final / private / static, class final
6. So sánh
| JDK dynamic proxy | CGLIB | |
|---|---|---|
| Cơ chế | Implement interface (java.lang.reflect.Proxy) | Tạo subclass bằng bytecode |
| Yêu cầu | Bean phải có interface | Class không final |
| Không chặn được | Method không có trong interface | Method final, private, static |
| Spring Boot mặc định | CGLIB (spring.aop.proxy-target-class=true) | |
7. Khi nào nên dùng / không nên dùng
- Nên: logging, metrics, audit, retry, security check, idempotency. Tức là logic lặp lại, không phải business.
- Không nên: nhét business rule vào aspect. Người đọc
createOrderkhông thấy nó, debug rất khổ. - Pointcut quá rộng (
execution(* *(..))) làm chậm và log rác.
Tôi có @Transactional trên method saveOrder() và gọi nó từ createOrder() trong cùng class, sao không rollback?
Đây là bẫy hay hỏi nhất: self-invocation. Gọi this.saveOrder() là gọi thẳng object thật, không qua proxy, nên advice transaction không chạy.
@Service
public class OrderService {
public void createOrder(CreateOrderRequest req) {
saveOrder(req); // = this.saveOrder(req) → KHÔNG qua proxy
}
@Transactional
public void saveOrder(CreateOrderRequest req) {
orderRepository.save(Order.from(req));
inventoryRepository.decrease(req.productId(), req.quantity());
throw new RuntimeException("boom"); // không rollback!
}
}
Controller ──→ PROXY.createOrder() (createOrder không có @Transactional)
│
v
THẬT.createOrder()
│ this.saveOrder() ← "this" là object thật
v
THẬT.saveOrder() → không có transaction
@Async cũng y hệt: gọi trong cùng class thì chạy đồng bộ trên thread hiện tại. Cách sửa:
- Tách sang bean khác (khuyên dùng):
OrderWriter.saveOrder()được inject vàoOrderService. - Đặt
@Transactionallên method được gọi từ ngoài (createOrder). - Self-inject:
@Lazyinject chínhOrderServicerồi gọiself.saveOrder(). Chạy được nhưng khó đọc. - Dùng AspectJ weaving (compile/load-time) thay vì proxy. Ít dùng.
Thêm hai bẫy cùng gốc: method private hoặc final có @Transactional cũng bị bỏ qua vì CGLIB không override được.
Vậy AOP của Spring khác AspectJ chỗ nào?
Bạn hiểu gần đúng là chúng liên quan, chỉ cần chỉnh: Spring AOP mượn cú pháp của AspectJ (@Aspect, @Around, pointcut expression) nhưng chạy bằng proxy lúc runtime.
| Spring AOP | AspectJ | |
|---|---|---|
| Cơ chế | Proxy runtime | Sửa bytecode (compile-time / load-time weaving) |
| Join point | Chỉ method public của bean | Method, constructor, field, self-call, private |
| Đối tượng | Chỉ Spring bean | Mọi object |
| Cấu hình | Đơn giản | Cần agent/plugin compiler |
Hầu hết project chỉ cần Spring AOP.
Phỏng vấn hỏi AOP và proxy thì trả lời sao?
Q1. Spring AOP hoạt động thế nào?
🇻🇳 Tiếng Việt:
Spring tạo proxy bọc quanh bean khi bean khớp pointcut. Caller được inject proxy. Khi gọi method, proxy chạy advice (before, around, after) rồi gọi method thật. Spring Boot mặc định dùng CGLIB tạo subclass; nếu cấu hình dùng interface thì dùng JDK dynamic proxy.
🇬🇧 English:
Spring wraps a bean in a proxy when it matches a pointcut, and callers receive the proxy. On each call, the proxy runs the advice, such as before, around or after, and then delegates to the real method. Spring Boot uses CGLIB subclass proxies by default; JDK dynamic proxies are used for interface-based proxying.
Q2. Vì sao @Transactional không chạy khi gọi method trong cùng class?
🇻🇳 Tiếng Việt:
Vì transaction được áp dụng bởi proxy. Gọithis.method()là gọi trực tiếp object thật, không qua proxy, nên không có interceptor nào mở transaction. Cách sửa là tách method sang bean khác, hoặc đặt annotation lên method được gọi từ ngoài.@Asyncvà@Cacheablecũng bị y hệt.
🇬🇧 English:
Because transactions are applied by the proxy. Callingthis.method()goes straight to the target object and bypasses the proxy, so no interceptor starts a transaction. The fix is to move the method into another bean or put the annotation on the externally called method.@Asyncand@Cacheablehave the same limitation.
Q3. JDK proxy và CGLIB khác nhau thế nào?
🇻🇳 Tiếng Việt:
JDK proxy implement interface nên chỉ chặn method của interface. CGLIB tạo subclass nên proxy được class không có interface, nhưng không chặn được method final, private, static và không dùng được với class final.
🇬🇧 English:
A JDK proxy implements the bean's interfaces, so it only intercepts interface methods. CGLIB generates a subclass, so it works without interfaces, but it cannot intercept final, private or static methods and cannot proxy final classes.
Cách nhớ ngắn gọn
- AOP = proxy đứng trước bean, chạy advice rồi
proceed(). - Gọi qua proxy mới có magic.
this.x()= không magic. - CGLIB = subclass: cấm final/private.
@Transactional,@Async,@Cacheablechung một gốc: proxy.
Tự kiểm tra
pjp.proceed() làm gì? Quên gọi thì sao?
Gọi method thật (hoặc advice kế tiếp). Quên gọi thì method thật không chạy và caller nhận null.
Method private có @Transactional có tác dụng không?
Không. Proxy không chặn được method private.
Bean chỉ có class, không interface. Spring dùng proxy gì?
CGLIB (và Spring Boot mặc định luôn dùng CGLIB).
Sửa self-invocation cách nào sạch nhất?
Tách method cần proxy sang một bean khác và inject vào.
Spring AOP chặn được việc set field không?
Không. Spring AOP chỉ có join point là method call trên bean. Cần AspectJ weaving.