AOP Proxy — JDK Dynamic Proxy vs CGLIB và pitfall self-call
Spring wrap bean trong proxy tại init phase để chèn @Transactional/@Async. So sánh JDK Dynamic Proxy vs CGLIB, khi nào chọn cái nào, và pitfall self-call bypass.
TL;DR: BeanPostProcessor.postProcessAfterInitialization là nơi Spring AOP wrap bean gốc trong một proxy object rồi thay thế nó trong container. Proxy tồn tại để chèn cross-cutting concern (transaction, security, async) mà không chỉnh business code. Có 2 cơ chế proxy: JDK Dynamic Proxy (tạo object implement interface, dùng java.lang.reflect.Proxy) và CGLIB (sinh subclass bytecode của bean class). Spring Boot 2.0+ mặc định CGLIB. Pitfall quan trọng nhất: this.method() trong cùng class không qua proxy — mọi @Transactional, @PreAuthorize, @Async trên method đó mất tác dụng.
Bài Bean lifecycle đã map 9 giai đoạn, trong đó bước 8 — BPP after-init là nơi AOP proxy được tạo. Bài này soi vào một câu hỏi cụ thể: proxy đó được tạo bằng cơ chế nào, làm được gì và không làm được gì?
1. Vì sao proxy tồn tại — cross-cutting concern
Hãy hình dung bạn có 50 service method cần transaction, 30 method cần security check, 10 method cần log timing. Nếu không có proxy, bạn phải viết lặp:
// Khong co AOP — lap code cho moi method
public void placeOrder(Order order) {
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionDefinition());
try {
securityChecker.check("ROLE_USER");
long start = System.currentTimeMillis();
// ... business logic
log.info("placeOrder took {}ms", System.currentTimeMillis() - start);
txManager.commit(tx);
} catch (Exception e) {
txManager.rollback(tx);
throw e;
}
}
AOP (Aspect-Oriented Programming — lập trình hướng khía cạnh) giải quyết vấn đề này bằng cách tách cross-cutting concern ra khỏi business code. Cross-cutting concern là loại logic "cắt ngang" nhiều class, không thuộc về nghiệp vụ cụ thể nào: transaction, security, caching, logging, retry, tracing.
Spring AOP cài cross-cutting concern qua proxy pattern: bọc bean gốc trong một object trung gian có cùng interface/class. Mọi lời gọi từ bên ngoài đi qua proxy trước — proxy chèn phần logic concern, rồi mới chuyển tiếp xuống bean gốc.
Sơ đồ ở section 5 vẽ đúng đường đi này — và cả đường đi KHÔNG qua proxy, thứ sinh ra bug phổ biến nhất của Spring AOP.
Kết quả: business code sạch hoàn toàn — chỉ chứa nghiệp vụ, không một dòng transaction hay security.
2. Cơ chế bên dưới — BPP tạo proxy tại init phase
AnnotationAwareAspectJAutoProxyCreator là một BeanPostProcessor được Spring Boot tự đăng ký khi có spring-aop trên classpath (đã có sẵn qua spring-boot-starter). Nó hoạt động tại bước 8 — postProcessAfterInitialization:
// Simplified — org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 1. Kiem tra bean co match bat ky aspect/advisor nao khong
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(
bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
// 2. Neu co match -> wrap bean trong proxy
Object proxy = createProxy(bean.getClass(), beanName,
specificInterceptors, new SingletonTargetSource(bean));
return proxy; // tra proxy thay vi bean goc
}
return bean; // khong match -> tra nguyen bean
}
Quan trọng: method này trả về object thay thế. Khi trả về proxy, container đặt proxy (chứ không phải bean gốc) vào singletonObjects. Mọi @Autowired từ đó nhận proxy.
Xem lại diagram từ bài Bean lifecycle — proxy wrap xảy ra sau @PostConstruct, sau tất cả init callback. Bean gốc đã được khởi tạo đầy đủ trước khi bị wrap.

Điều này kết nối với BeanDefinition & BeanFactoryPostProcessor: BeanFactoryPostProcessor can thiệp metadata (trước khi bean tạo), còn BeanPostProcessor can thiệp instance (sau khi bean tạo) — proxy wrap là use case điển hình của BPP.
3. JDK Dynamic Proxy — proxy dựa trên interface
JDK Dynamic Proxy là cơ chế built-in của Java, không cần thư viện ngoài. Nó tạo một anonymous class implement interface của bean tại runtime, thông qua java.lang.reflect.Proxy.newProxyInstance().
// Truong hop JDK proxy: bean implement interface
public interface PaymentGateway {
void charge(Money amount);
void refund(String orderId);
}
@Service
public class StripePaymentGateway implements PaymentGateway {
@Transactional
public void charge(Money amount) { /* ... */ }
@Transactional
public void refund(String orderId) { /* ... */ }
}
Spring tạo proxy như sau (simplified):
// Proxy la instance cua anonymous class KHONG LIEN QUAN toi StripePaymentGateway
PaymentGateway proxy = (PaymentGateway) Proxy.newProxyInstance(
StripePaymentGateway.class.getClassLoader(),
new Class[]{PaymentGateway.class}, // implement cung interface
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
// Chay advice (transaction, security) truoc
return method.invoke(stripeInstance, args); // delegate xuong bean goc
}
}
);
Proxy class tên dạng $Proxy42 — anonymous, sinh tại runtime. Hệ quả quan trọng:
@Autowired
PaymentGateway gateway; // OK — proxy implement PaymentGateway
@Autowired
StripePaymentGateway stripe; // FAIL voi JDK proxy -- proxy KHONG phai subclass cua StripePaymentGateway
// Fix: inject theo interface PaymentGateway, hoac bat proxyTargetClass=true (CGLIB)
Vì proxy không extend bean class, proxy instanceof StripePaymentGateway trả về false. Nếu code có downcast về class gốc, sẽ ClassCastException trên JDK proxy mode.
4. CGLIB Proxy — proxy dựa trên subclass bytecode
CGLIB (Code Generation Library, đã đóng gói vào spring-core) tạo proxy bằng cách sinh subclass của bean class tại runtime, override các method để chèn advice.
// Bean KHONG can implement interface
@Service
public class OrderService {
@Transactional
public void place(Order order) { /* ... */ }
}
Spring dùng CGLIB sinh class (simplified):
// Class sinh ra tai runtime — extend bean class
public class OrderService$$SpringCGLIB$$0 extends OrderService {
@Override
public void place(Order order) {
// Chay advice (transaction begin)
super.place(order); // goi method goc
// Chay advice (commit/rollback)
}
}
Vì proxy là subclass, proxy instanceof OrderService trả về true. Đây là lý do CGLIB ít gây bất ngờ hơn khi code có downcast hay instanceof check.
Bảng so sánh hai cơ chế:
| Khía cạnh | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| Yêu cầu bean | Phải implement ít nhất 1 interface | Không cần interface |
| Cơ chế | java.lang.reflect.Proxy — implement interface | Bytecode generation — tạo subclass |
| Tên proxy class | $Proxy42 (anonymous) | OrderService$$SpringCGLIB$$0 |
instanceof BeanClass | false | true |
Method final | Không liên quan (interface không có final) | Không proxy được — subclass không override final |
Class final | Không liên quan | Không proxy được — không subclass được |
| Thư viện | Java SE built-in | CGLIB (đóng gói trong spring-core) |
Spring chọn proxy nào? Luật chọn theo thứ tự ưu tiên:
spring.aop.proxy-target-class=true(hoặc@EnableAspectJAutoProxy(proxyTargetClass=true)) → luôn CGLIB.- Spring Boot 2.0+ đặt
proxy-target-class=truemặc định → CGLIB cho tất cả. - Nếu config thủ công tắt CGLIB: bean implement interface → JDK proxy; bean không có interface → CGLIB.
Ý nghĩa thực tế: từ Spring Boot 2.0 trở đi, hầu như mọi proxy đều là CGLIB — nhất quán, ít bất ngờ instanceof hơn.
5. Vì sao self-call this.method() bypass proxy?
Đây là nguồn gốc của một trong những bug phổ biến nhất với Spring AOP.
Khi @Autowired OrderService service, biến service trỏ tới proxy, không phải bean gốc. Mọi call từ ngoài qua biến service đều đi qua proxy → advice chạy đúng.
Nhưng khi một method gọi method khác trong cùng class (this.otherMethod()), Java thực thi trực tiếp trên object gốc — không có proxy ở giữa:
@Service
public class OrderService {
// Duoc goi tu controller -> qua proxy -> @Transactional CHAY
@Transactional
public void placeOrder(Order order) {
validate(order); // this.validate(order) -- KHONG qua proxy!
}
// @Transactional KHONG co tac dung khi goi tu placeOrder()
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) {
// TX moi KHONG duoc bat -- van dung TX cua placeOrder()
}
}

Lý do kỹ thuật: proxy là object khác nằm bên ngoài bean gốc. Khi placeOrder chạy trên bean gốc, this trỏ đến bean gốc — không phải proxy. Call this.validate() là direct method call trên bean gốc, proxy không nhìn thấy call này.
Self-call bypass khó lần ra vì không có exception nào được ném: validate() vẫn chạy, test vẫn xanh — chỉ có transaction mới (REQUIRES_NEW) là không bao giờ được mở. validate() chạy chung transaction với placeOrder(): khi placeOrder() rollback, phần việc của validate() rollback theo, dù bạn thiết kế nó để commit độc lập (ví dụ ghi audit log). Bug chỉ lộ khi production xuất hiện data inconsistency — rất khó truy ngược về dòng this.validate(order). Fix: tách class hoặc self-inject (xem 3 cách bên dưới).
3 cách fix self-call bypass
Cách 1: Tách thành 2 class (khuyến nghị — clean nhất)
@Service
public class OrderService {
private final OrderValidator validator;
public OrderService(OrderValidator validator) {
this.validator = validator;
}
@Transactional
public void placeOrder(Order order) {
validator.validate(order); // qua proxy cua OrderValidator -> @Transactional chay
}
}
@Service
public class OrderValidator {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { /* ... */ }
}
Cách 2: Inject self bean (workaround — dùng khi refactor quá tốn công)
@Service
public class OrderService {
@Autowired
private OrderService self; // inject proxy cua chinh minh
@Transactional
public void placeOrder(Order order) {
self.validate(order); // qua proxy -> @Transactional chay
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { /* ... */ }
}
Lưu ý: self-injection tạo circular dependency. Từ Spring Boot 2.6 trở đi, circular reference bị cấm mặc định (spring.main.allow-circular-references=false) — phải khai báo @Lazy trên field self-inject (hoặc bật lại allow-circular-references=true) thì container mới start. Bản trước 2.6 cho phép circular reference âm thầm nên không cần.
Cách 3: ApplicationContext.getBean() để lấy proxy (chỉ dùng khi 2 cách trên không khả thi)
@Service
public class OrderService implements ApplicationContextAware {
private ApplicationContext ctx;
public void setApplicationContext(ApplicationContext ctx) {
this.ctx = ctx;
}
@Transactional
public void placeOrder(Order order) {
// Lay proxy tu context
ctx.getBean(OrderService.class).validate(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { /* ... */ }
}
Cách này dùng ApplicationContext như service locator — xem bài 01 — BeanFactory vs ApplicationContext về vì sao pattern này là anti-pattern trong code business. Chỉ dùng khi 2 cách trên thực sự không khả thi.
So sánh 3 fix:
| Cách | Độ sạch | Khi nào dùng |
|---|---|---|
| Tách 2 class | Tốt nhất — đúng nguyên tắc SRP | Mặc định |
| Self-inject | Chấp nhận được | Khi validate/helper logic quá nhỏ để tách class |
| ApplicationContext.getBean | Worst — service locator | Chỉ khi code legacy cứng không thể refactor |
6. Pitfall phụ — class/method final với CGLIB
CGLIB tạo proxy bằng subclass — nên class final hoặc method final không proxy được:
// SAI -- CGLIB khong the subclass final class
@Service
@Transactional
public final class OrderService { ... }
// Loi: Cannot subclass final class OrderService (khi startup)
// SAI -- CGLIB khong the override final method
@Service
public class OrderService {
@Transactional
public final void place(Order order) { ... }
// @Transactional khong co tac dung -- method khong bi proxy
// Spring 5.3+ log warning; Spring 6 throw exception
}
Quy tắc: bean class và method muốn AOP áp dụng phải không phải final. Kotlin mặc định final mọi class — dùng Spring Boot với Kotlin cần kotlin-allopen plugin (Spring Boot auto-config plugin này).
7. Bài toán ví dụ — vì sao một record lỗi lại rollback cả batch?
Đề bài: ImportService.importAll(rows) duyệt danh sách và gọi this.importOne(row) cho từng dòng. importOne được đánh @Transactional(propagation = REQUIRES_NEW) với chủ đích mỗi dòng commit độc lập — một dòng lỗi không kéo cả lô rollback. Nhưng production cho thấy chỉ một dòng lỗi là toàn bộ 10.000 dòng rollback. Vì sao, và sửa thế nào?
@Service
public class ImportService {
@Transactional
public void importAll(List<Row> rows) {
for (Row row : rows) {
this.importOne(row); // self-call -- bypass proxy!
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void importOne(Row row) {
// ghi 1 dong
}
}
Chẩn đoán: this.importOne(row) là self-call — đi thẳng tới bean gốc, không qua proxy. @Transactional(REQUIRES_NEW) chỉ có hiệu lực khi proxy intercept call; proxy không thấy call này nên importOne chạy chung transaction của importAll. Một dòng lỗi đánh dấu transaction rollback-only, nên khi importAll kết thúc, cả lô rollback.
Lời giải: tách importOne sang một bean riêng. Call từ ImportService sang rowImporter.importOne() đi qua proxy của RowImporter, nên REQUIRES_NEW mới có tác dụng — mỗi dòng commit độc lập.
@Service
public class ImportService {
private final RowImporter rowImporter;
public ImportService(RowImporter rowImporter) {
this.rowImporter = rowImporter;
}
@Transactional
public void importAll(List<Row> rows) {
for (Row row : rows) {
try {
rowImporter.importOne(row); // qua proxy -> REQUIRES_NEW co hieu luc
} catch (Exception e) {
// 1 dong loi khong dung vong lap; transaction rieng cua dong do da rollback
}
}
}
}
@Service
public class RowImporter {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void importOne(Row row) {
// ghi 1 dong
}
}
try/catch quanh mỗi call để một dòng lỗi không dừng vòng lặp — vì importOne chạy transaction riêng, rollback của nó không động tới các dòng đã commit.
8. Deep Dive
Spring Reference:
- Spring AOP Proxying Mechanisms — JDK vs CGLIB, understanding AOP proxies,
@EnableAspectJAutoProxy. - Spring AOP — Understanding AOP Proxies — self-invocation problem chính thức, Spring giải thích rõ.
- Choosing AOP Declaration Style — Spring AOP (proxy-based) vs full AspectJ và các declaration style, khi nào cần chuyển sang AspectJ.
Source để đọc:
AbstractAutoProxyCreator.postProcessAfterInitialization— nơi proxy được quyết định tạo hay không.DefaultAopProxyFactory.createAopProxy— logic chọn JDK vs CGLIB (chỉ ~30 dòng, rất dễ đọc).CglibAopProxyvàJdkDynamicAopProxy— implementation của 2 loại proxy.
Java SE:
java.lang.reflect.Proxy— JDK Dynamic Proxy API gốc của Java.
Liên hệ các bài khác
- Bean lifecycle phases — proxy wrap xảy ra ở bước 8 (BPP after-init), sau tất cả init callback. Đọc bài đó để thấy proxy fit vào đâu trong 9 bước toàn cảnh.
- BeanDefinition & BeanFactoryPostProcessor —
BeanPostProcessor(tạo proxy) vàBeanFactoryPostProcessor(chỉnh metadata) là 2 extension point khác nhau trong lifecycle container. Bài đó giải thích tại sao cần tách 2 loại. - BeanFactory vs ApplicationContext — proxy được đặt vào
singletonObjectsmap sau khi tạo. Bài đó giải thích map này là gì và tại sao lần saugetBeantrả ngay proxy cached. - Singleton & Prototype scope — proxy thay thế bean gốc trong container, nhưng bean gốc vẫn tồn tại. Scope quyết định lifecycle của proxy object cũng như bean gốc.
Tóm tắt
- Proxy được tạo bởi
AnnotationAwareAspectJAutoProxyCreator(BPP after-init, bước 8) và đặt vàosingletonObjectsthay bean gốc — mọi@Autowiredtừ đó nhận proxy. - Pitfall self-call:
this.method()không qua proxy — 3 fix: (1) tách class (clean nhất), (2) inject self bean (Spring Boot 2.6+ cần@Lazytránh circular), (3)ApplicationContext.getBean()(anti-pattern). - Class
final→ fail startup; methodfinal→ proxy tạo được nhưng advice im lặng không chèn — không throw exception, khó phát hiện (Kotlin class cầnkotlin-allopen).
Tự kiểm tra
- Q1Khi bạn gọi
service.placeOrder(order)từ controller, biếnservicethực ra trỏ tới gì? Giải thích luồng thực thi từ controller tới business logic. - Q2Spring chọn JDK Dynamic Proxy hay CGLIB trong từng trường hợp sau? (a) Bean implement interface, Spring Boot 2.3; (b) Bean không có interface; (c) Code gọi
@Autowired StripePaymentGateway stripenhưng bean có interfacePaymentGateway. - Q3Service sau có
@Transactionaltrên cả 2 method. Khi controller gọiplaceOrder(), transaction nào được áp dụng choauditLog()?REQUIRES_NEWcó tạo transaction mới không?@Transactional public void placeOrder(Order o) { auditLog(o); }@Transactional(propagation = REQUIRES_NEW) public void auditLog(Order o) { ... } - Q4Vì sao class bean
finalgây lỗi startup khi dùng với@Transactionaltrong Spring Boot? Methodfinalkhác với classfinalở hậu quả nào? - Q5Bạn có bean
ReportServicekhông implement interface. Methodgenerate()có@Transactional. Sau khi inject vào controller,controllerReportService.getClass().getName()in ra gì?controllerReportService instanceof ReportServicetrả về gì?
Bài tiếp theo: Singleton & Prototype scope
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên