Spring AOP: hiểu từ khái niệm đến proxy bên dưới
On this page
@Transactional,@Cacheable,@Async,@PreAuthorize… tất cả đều chạy trên cùng một nền móng: Spring AOP. Hiểu AOP là hiểu vì sao những annotation này “thần kỳ” — và vì sao đôi khi chúng im lặng không hoạt động.
TL;DR#
- AOP (Aspect-Oriented Programming) tách các cross-cutting concern (logging, transaction, security, caching, metrics) ra khỏi business logic.
- Spring AOP hoạt động bằng proxy lúc runtime: Spring bọc bean của bạn trong một proxy, proxy chặn lời gọi method và chạy thêm logic (advice) trước/sau.
- Hai loại proxy: JDK Dynamic Proxy (dựa trên interface) và CGLIB (tạo subclass). Spring Boot mặc định dùng CGLIB.
- 5 loại advice:
@Before,@After,@AfterReturning,@AfterThrowing,@Around. - Giới hạn quan trọng nhất: chỉ chặn được lời gọi đi qua proxy. Gọi nội bộ (
this.method()— self-invocation), methodprivate, method/classfinal, object không phải Spring bean → AOP không có tác dụng. - Cần sức mạnh lớn hơn (field access, constructor, self-invocation) → dùng AspectJ weaving thật sự.
flowchart LR
C[Caller] --> P["Proxy (chứa advice)"]
P --> T[Target bean thật]
T -. "this.otherMethod() - không qua proxy" .-> T
1. Vấn đề AOP giải quyết#
Giả sử bạn cần đo thời gian thực thi và log cho mọi method trong service layer:
public Order placeOrder(OrderRequest req) {
long start = System.currentTimeMillis();
log.info("placeOrder start");
try {
// ... business logic thật sự, chỉ vài dòng ...
return order;
} finally {
log.info("placeOrder took {} ms", System.currentTimeMillis() - start);
}
}
Lặp lại đoạn này ở 200 method thì: code trùng lặp (code scattering), business logic bị lẫn với technical concern (code tangling), sửa format log phải sửa 200 chỗ.
AOP cho phép viết logic đó một lần và khai báo nơi áp dụng.
2. Thuật ngữ cốt lõi#
| Thuật ngữ | Ý nghĩa | Ví dụ |
|---|---|---|
| Aspect | Module chứa cross-cutting concern | Class LoggingAspect |
| Join Point | Điểm trong quá trình thực thi có thể chèn logic. Với Spring AOP, join point luôn là method execution | Lời gọi placeOrder() |
| Pointcut | Biểu thức chọn ra các join point | execution(* com.shop.service..*(..)) |
| Advice | Logic được chạy tại join point, kèm thời điểm chạy | @Around đo thời gian |
| Target | Object gốc bị advise | Bean OrderService |
| Proxy | Object bọc target, do Spring tạo ra | OrderService$$SpringCGLIB$$0 |
| Weaving | Quá trình gắn aspect vào target | Spring: runtime; AspectJ: compile/load-time |
Công thức dễ nhớ: Aspect = Pointcut (ở đâu) + Advice (làm gì, khi nào).
3. Viết aspect đầu tiên#
Dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring Boot tự bật @EnableAspectJAutoProxy khi starter này có trên classpath.
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Pointcut("execution(* com.shop.service..*(..))")
public void serviceLayer() {}
@Around("serviceLayer()")
public Object measure(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);
}
}
}
Lưu ý: @Aspect không tự biến class thành bean — vẫn cần @Component (hoặc khai báo @Bean).
4. Năm loại Advice#
sequenceDiagram
participant C as Caller
participant P as Proxy
participant T as Target
C->>P: gọi method
Note over P: @Around - phần trước proceed()
Note over P: @Before
P->>T: invoke method thật
alt return bình thường
T-->>P: kết quả
Note over P: @AfterReturning
else throw exception
T-->>P: exception
Note over P: @AfterThrowing
end
Note over P: @After - luôn chạy, như finally
Note over P: @Around - phần sau proceed()
P-->>C: kết quả / exception
| Advice | Chạy khi | Thay đổi được kết quả? | Use case |
|---|---|---|---|
@Before |
Trước method | Không (chỉ có thể throw để chặn) | Validate, check quyền, log input |
@AfterReturning |
Method return thành công | Không đổi reference trả về | Log kết quả, audit |
@AfterThrowing |
Method throw exception | Không nuốt được exception | Log lỗi, dịch exception, alert |
@After |
Luôn chạy (như finally) |
Không | Dọn dẹp tài nguyên, MDC |
@Around |
Bao quanh toàn bộ | Có: đổi argument, đổi return, nuốt/dịch exception, không gọi method | Transaction, retry, cache, đo thời gian |
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logResult(JoinPoint jp, Object result) { ... }
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logError(JoinPoint jp, Exception ex) { ... }
Nguyên tắc: dùng advice “yếu” nhất đủ cho nhu cầu. @Around mạnh nhưng dễ quên gọi proceed() hoặc quên return kết quả — method thật sẽ không chạy hoặc trả về null.
Thứ tự trong cùng một aspect (từ Spring 5.2.7): @Around → @Before → @After → @AfterReturning/@AfterThrowing theo thứ tự ưu tiên vào; chiều ra thì ngược lại.
5. Pointcut expression#
5.1. execution — phổ biến nhất#
execution( modifiers? return-type declaring-type? method-name(params) throws? )
| Biểu thức | Khớp với |
|---|---|
execution(* com.shop.service.*.*(..)) |
Mọi method của class trong package service (không gồm sub-package) |
execution(* com.shop.service..*(..)) |
Như trên, kể cả sub-package |
execution(public * *(..)) |
Mọi method public |
execution(* *..*Service.find*(..)) |
Method bắt đầu bằng find trong class tên kết thúc bằng Service |
execution(* *(String, ..)) |
Method có tham số đầu là String |
Ký hiệu: * là một phần tử bất kỳ, .. là “0 hoặc nhiều” (package hoặc parameter).
5.2. Các designator khác#
| Designator | Ý nghĩa |
|---|---|
within(com.shop.service..*) |
Mọi method trong các type thuộc package |
@annotation(com.shop.Audited) |
Method có annotation @Audited |
@within(org.springframework.stereotype.Service) |
Method trong class có annotation @Service |
bean(*Service) |
Bean có tên khớp pattern (riêng của Spring) |
args(java.lang.Long, ..) |
Theo kiểu argument lúc runtime |
this(...) / target(...) |
Theo kiểu của proxy / của target |
Kết hợp bằng &&, ||, !:
@Pointcut("within(com.shop..*) && @annotation(com.shop.Audited)")
public void auditedMethods() {}
5.3. Pattern thực tế: custom annotation#
Cách sạch và rõ ràng nhất — method nào cần thì gắn annotation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String action();
}
@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {
private final AuditRepository auditRepository;
@AfterReturning("@annotation(audited)") // bind annotation vào tham số
public void audit(JoinPoint jp, Audited audited) {
auditRepository.save(new AuditLog(audited.action(), currentUser(), Instant.now()));
}
}
@Service
public class AccountService {
@Audited(action = "WITHDRAW")
public void withdraw(Long accountId, BigDecimal amount) { ... }
}
6. Bên dưới: Proxy hoạt động thế nào#
6.1. Proxy được tạo khi nào?#
Trong bean lifecycle, AnnotationAwareAspectJAutoProxyCreator (một BeanPostProcessor) kiểm tra ở bước postProcessAfterInitialization: nếu bean có method khớp pointcut nào đó, nó trả về proxy thay cho bean gốc. Mọi nơi inject bean này thực chất đều nhận proxy.
flowchart TB
A[Tạo instance OrderService] --> B[Inject dependency]
B --> C["@PostConstruct / init"]
C --> D{"BeanPostProcessor: có pointcut khớp?"}
D -- Không --> E[Đăng ký bean gốc]
D -- Có --> F[Tạo proxy bọc bean gốc]
F --> G[Đăng ký PROXY vào ApplicationContext]
6.2. JDK Dynamic Proxy vs CGLIB#
flowchart LR
subgraph JDK["JDK Dynamic Proxy"]
I[OrderService interface] --> P1["$Proxy42 implements OrderService"]
I --> T1[OrderServiceImpl]
P1 -. "ủy quyền" .-> T1
end
subgraph CG["CGLIB"]
T2[OrderService class] --> P2["OrderService$$SpringCGLIB$$0 extends OrderService"]
P2 -. "ủy quyền" .-> T2
end
| JDK Dynamic Proxy | CGLIB | |
|---|---|---|
| Cơ chế | Implement interface của target | Tạo subclass của target |
| Yêu cầu | Target phải implement interface | Class không final |
| Method áp dụng được | Chỉ method khai báo trong interface | Method không private, không final, không static |
| Inject theo class cụ thể | ❌ Lỗi (proxy không phải instance của class đó) | ✅ Được |
| Mặc định | Spring Framework thuần (khi có interface) | Spring Boot (spring.aop.proxy-target-class=true) |
Lỗi kinh điển với JDK proxy: BeanNotOfRequiredTypeException khi inject OrderServiceImpl thay vì interface OrderService.
Một hệ quả thú vị của CGLIB: constructor của class có thể bị gọi thêm lần nữa khi tạo proxy (các version hiện đại dùng Objenesis để tránh điều này), nên đừng đặt logic có side effect trong constructor.
7. Giới hạn của Spring AOP — phần hay bị hỏi nhất#
7.1. Self-invocation#
@Service
public class ReportService {
public void generateAll() {
for (Long id : ids) {
generate(id); // this.generate(id) → KHÔNG qua proxy
}
}
@Cacheable("reports")
public Report generate(Long id) { ... } // cache không bao giờ được dùng
}
sequenceDiagram
participant C as Controller
participant P as Proxy
participant T as ReportService thật
C->>P: generateAll()
P->>T: generateAll()
T->>T: this.generate(id)
Note over T: Gọi thẳng trên object thật, proxy không biết gì
Proxy chỉ nằm “bên ngoài” object. Khi code bên trong gọi this.xxx(), this là object thật chứ không phải proxy.
Cách khắc phục (theo thứ tự khuyến nghị):
- Tách method sang một bean khác — sạch nhất, thường cũng là thiết kế tốt hơn.
- Self-injection: inject chính bean đó (qua
@LazyhoặcObjectProvider) và gọi qua reference đó. AopContext.currentProxy()với@EnableAspectJAutoProxy(exposeProxy = true)— hoạt động nhưng làm code phụ thuộc vào AOP, nên tránh.- Chuyển sang AspectJ weaving.
7.2. Các trường hợp khác AOP không hoạt động#
- Method
privatehoặcstatic: không override được. - Method hoặc class
final(với CGLIB). Chú ý với Kotlin: class mặc định làfinal, cần pluginkotlin-spring(all-open). - Object tự
new, không do Spring quản lý. - Gọi method trong constructor hoặc
@PostConstructcủa chính bean đó — lúc đó proxy chưa tồn tại hoặc bạn đang ở trong object thật. - Aspect bắt chính aspect khác: aspect không được advise bởi aspect khác.
8. Thứ tự giữa nhiều aspect#
Khi nhiều aspect cùng áp dụng cho một method, dùng @Order (hoặc implement Ordered). Số nhỏ hơn = ưu tiên cao hơn = nằm ngoài cùng.
flowchart LR
C[Caller] --> A1["Security Aspect @Order 1"]
A1 --> A2["Transaction @Order 2"]
A2 --> A3["Logging Aspect @Order 3"]
A3 --> T[Target method]
Chiều vào: aspect có order nhỏ chạy @Before trước. Chiều ra: aspect có order nhỏ chạy @After sau cùng — giống các lớp của củ hành.
Ví dụ thực tế: nếu bạn viết aspect retry cho lỗi optimistic locking, aspect đó phải nằm ngoài transaction advice (order nhỏ hơn). Nếu nằm trong, việc retry diễn ra trong cùng một transaction đã hỏng và vô nghĩa. Transaction advisor mặc định có order Ordered.LOWEST_PRECEDENCE, có thể chỉnh qua @EnableTransactionManagement(order = ...).
9. Spring AOP vs AspectJ#
| Spring AOP | AspectJ | |
|---|---|---|
| Weaving | Runtime, bằng proxy | Compile-time, post-compile, hoặc load-time (LTW) |
| Join point | Chỉ method execution | Method call/execution, constructor, field get/set, static initializer… |
| Self-invocation | ❌ | ✅ |
| Private/final method | ❌ | ✅ |
| Object ngoài Spring | ❌ | ✅ |
| Setup | Đơn giản, chỉ cần starter | Cần compiler ajc hoặc java agent |
| Performance | Có overhead nhỏ mỗi lời gọi qua proxy | Gần như không overhead lúc chạy |
Spring AOP dùng cú pháp annotation của AspectJ (@Aspect, @Around, pointcut expression), nhưng không dùng AspectJ weaver. Đây là điểm dễ nhầm.
Chọn Spring AOP cho 95% trường hợp. Chỉ cân nhắc AspectJ khi thực sự cần join point ngoài method hoặc cần advise object không do Spring quản lý.
10. Các annotation quen thuộc được xây dựng trên AOP#
| Annotation | Advice bên dưới | Hệ quả |
|---|---|---|
@Transactional |
TransactionInterceptor |
Self-invocation bỏ qua transaction |
@Cacheable, @CacheEvict |
CacheInterceptor |
Self-invocation bỏ qua cache |
@Async |
AsyncExecutionInterceptor |
Self-invocation chạy đồng bộ |
@PreAuthorize, @Secured |
AuthorizationManagerBeforeMethodInterceptor |
Self-invocation bỏ qua kiểm tra quyền — lỗ hổng bảo mật |
@Retryable (Spring Retry) |
RetryOperationsInterceptor |
Self-invocation không retry |
@Validated trên class |
MethodValidationInterceptor |
Self-invocation không validate |
Hiểu một cái là hiểu tất cả: cùng cơ chế proxy, cùng giới hạn.
11. Best practices#
- Ưu tiên pointcut theo annotation (
@annotation(...)) thay vì pattern tên method rộng — rõ ràng, người đọc code biết method nào bị ảnh hưởng. - Giữ aspect nhẹ: advice chạy trên mọi lời gọi khớp. Tránh I/O nặng hoặc logic phức tạp.
- Pointcut càng hẹp càng tốt:
execution(* *(..))sẽ proxy gần như mọi bean, kể cả bean của thư viện. - Không đặt business logic trong aspect — aspect dành cho technical concern.
- Với
@Around: luôn gọiproceed()(trừ khi cố ý chặn), luôn return kết quả, không nuốt exception vô tình. - Test aspect: dùng
@SpringBootTesthoặc tạo proxy thủ công vớiAspectJProxyFactoryđể unit test.
12. Câu hỏi phỏng vấn thường gặp#
- AOP là gì, giải quyết vấn đề gì? → Tách cross-cutting concern, tránh scattering và tangling.
- Spring AOP hoạt động thế nào? →
BeanPostProcessortạo proxy runtime; caller gọi proxy, proxy chạy chuỗi advice rồi ủy quyền cho target. - JDK proxy khác CGLIB thế nào? Spring Boot dùng cái nào? → Interface vs subclass; Boot mặc định CGLIB.
- Tại sao
@Transactionalgọi từ method cùng class không có tác dụng? → Self-invocation không qua proxy. @Aroundkhác@Beforethế nào? →@Aroundkiểm soát toàn bộ việc gọi method, đổi được input/output và exception.- Spring AOP khác AspectJ thế nào? → Proxy runtime, chỉ method execution vs weaving thật, mọi loại join point.
- Nhiều aspect cùng áp dụng thì chạy theo thứ tự nào? →
@Order, số nhỏ nằm ngoài cùng.