Cơ Chế AOP Dynamic Proxy: So Sánh JDK Proxy Và CGLIB
Mổ xẻ cơ chế tạo Dynamic Proxy của AOP trong Spring: so sánh JDK Dynamic Proxy và CGLIB, cạm bẫy self-call bypass và 3 giải pháp xử lý.
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 can thiệp code nghiệp vụ. Có 2 cơ chế: JDK Dynamic Proxy (implement interface qua reflection) và CGLIB (sinh subclass bytecode). Spring Boot 2.0+ mặc định CGLIB (proxy-target-class=true). Cạm bẫy lớn nhất: this.method() trong cùng class không đi qua proxy, khiến @Transactional hay @Async trên phương thức đó hoàn toàn vô hiệu.
Bài Vòng đời bean đã chỉ ra rằng bước 8 — BPP after-init là nơi AOP proxy được sinh ra. Bài học này làm rõ nguyên lý hoạt động, sự khác biệt giữa hai cơ chế proxy và cách xử lý triệt để lỗi self-call.
1. Vì sao Spring AOP cần sử dụng cơ chế Dynamic Proxy?
Trong ứng dụng doanh nghiệp, các tác vụ như quản lý transaction, kiểm tra quyền hạn, ghi log hay đo thời gian thực thi xuất hiện lặp đi lặp lại ở hàng chục service:
// Logic lap lai khi khong co AOP
public void placeOrder(Order order) {
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionDefinition());
try {
securityChecker.check("ROLE_USER");
orderRepository.save(order);
txManager.commit(tx);
} catch (Exception e) {
txManager.rollback(tx);
throw e;
}
}
AOP (Aspect-Oriented Programming) giải quyết vấn đề này bằng cách tách các logic "cắt ngang" (cross-cutting concerns) ra khỏi mã nghiệp vụ. Để áp dụng các khía cạnh này mà không sửa đổi mã nguồn gốc của lập trình viên, Spring sử dụng Proxy Pattern.
Đối tượng Proxy đóng vai trò trung gian đứng trước bean thật. Khi một component bên ngoài gọi phương thức của bean, yêu cầu bắt buộc phải đi qua Proxy trước. Tại đây, Proxy thực thi các đoạn mã bổ trợ (Advice) rồi mới ủy quyền (delegate) lời gọi xuống bean nghiệp vụ bên dưới.
2. Dynamic Proxy được khởi tạo ở giai đoạn nào trong vòng đời Bean?
Bộ xử lý AnnotationAwareAspectJAutoProxyCreator là một BeanPostProcessor được Spring Boot tự động đăng ký khi có thư viện AOP trên classpath. Bộ xử lý này can thiệp trực tiếp tại bước 8 — postProcessAfterInitialization:
// AbstractAutoProxyCreator - giai doan tao proxy
public Object postProcessAfterInitialization(Object bean, String beanName) {
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(
bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
// Boc bean goc trong proxy object va tra ve
return createProxy(bean.getClass(), beanName,
specificInterceptors, new SingletonTargetSource(bean));
}
return bean; // Khong co aspect phu hop -> giu nguyen bean goc
}
Khi phương thức này trả về đối tượng Proxy, Spring Container sẽ lưu Proxy này vào cache singletonObjects thay cho bean gốc. Mọi vị trí khai báo @Autowired sau đó đều nhận được tham chiếu tới Proxy.

Vì quá trình bọc diễn ra ở sau giai đoạn khởi tạo, bean nghiệp vụ đã được inject dependencies và chạy xong @PostConstruct trước khi trở thành mục tiêu (target) của Proxy.
3. JDK Dynamic Proxy hoạt động ra sao và có những hạn chế nào?
JDK Dynamic Proxy là tính năng có sẵn của Java SE (java.lang.reflect.Proxy), cho phép tạo một anonymous class tại runtime cài đặt cùng interface với bean:
public interface PaymentGateway {
void charge(Money amount);
}
@Service
public class StripePaymentGateway implements PaymentGateway {
@Transactional
public void charge(Money amount) { /* xu ly nghiep vu */ }
}
Khi khởi tạo, Spring tạo ra một Proxy cài đặt interface PaymentGateway:
// Proxy implement interface, khong he ke thua StripePaymentGateway
PaymentGateway proxy = (PaymentGateway) Proxy.newProxyInstance(
StripePaymentGateway.class.getClassLoader(),
new Class<?>[]{PaymentGateway.class},
(proxyObj, method, args) -> {
// 1. Chay Advice (vi du: bat dau transaction)
Object result = method.invoke(stripeTarget, args);
// 2. Chay Advice sau (commit transaction)
return result;
}
);
Hạn chế lớn nhất của JDK Dynamic Proxy là bắt buộc bean phải implement ít nhất một interface. Hơn nữa, vì class proxy (thường mang tên dạng $Proxy42) chỉ implement interface mà không kế thừa class gốc:
@Autowired
PaymentGateway gateway; // Hop le: proxy implement interface nay
@Autowired
StripePaymentGateway stripe; // Nem loi NoSuchBeanDefinitionException / BeanNotOfRequiredTypeException
Biểu thức proxy instanceof StripePaymentGateway sẽ trả về false. Nếu ứng dụng cố tình ép kiểu về class triển khai cụ thể, chương trình sẽ lập tức ném ClassCastException.
4. CGLIB Proxy can thiệp bytecode thế nào và tại sao Spring Boot ưu tiên?
CGLIB (Code Generation Library) giải quyết nhược điểm của JDK Proxy bằng cách sinh ra một subclass của bean tại runtime thông qua thao tác trực tiếp trên bytecode:
// Class duoc CGLIB tao dong tai runtime
public class OrderService$$SpringCGLIB$$0 extends OrderService {
@Override
public void placeOrder(Order order) {
// 1. Interceptor mo transaction
super.placeOrder(order); // Goi phuong thuc goc tren lop cha
// 2. Interceptor commit transaction
}
}
Nhờ cơ chế kế thừa, biểu thức proxy instanceof OrderService luôn trả về true. Điều này giúp việc tiêm phụ thuộc theo kiểu class diễn ra hoàn toàn bình thường mà không gây lỗi ép kiểu.
| Khía cạnh so sánh | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| Yêu cầu Interface | Bắt buộc phải có interface | Không cần interface |
| Cơ chế kỹ thuật | java.lang.reflect.Proxy | Sinh subclass bằng Bytecode manipulation |
| Tên class runtime | $ProxyNN | TargetBean$$SpringCGLIB$$NN |
instanceof TargetClass | false | true |
| Giới hạn từ khóa final | Không bị ảnh hưởng | Không proxy được class hoặc method final |
Từ phiên bản Spring Boot 2.0 trở đi, thiết lập spring.aop.proxy-target-class=true được bật theo mặc định. Spring Boot sử dụng CGLIB cho toàn bộ bean để đảm bảo tính nhất quán và loại bỏ rủi ro ép kiểu runtime.
5. Vì sao cơ chế Self-Call this.method() bypass hoàn toàn Proxy?
Khi một component bên ngoài gọi phương thức của bean, nó gọi thông qua đối tượng Proxy nằm ở lớp vỏ ngoài. Tuy nhiên, khi một phương thức nội bộ gọi một phương thức khác trong cùng một class, từ khóa this đại diện cho chính đối tượng bean thật:
@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
// Loi: this.validate() goi truc tiep tren bean goc, khong di qua proxy
validate(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) {
// Transaction moi se KHONG bao gio duoc mo
}
}

Khi lời gọi diễn ra nội bộ qua this, Proxy hoàn toàn không hay biết. Do đó, toàn bộ các chỉ dẫn AOP như @Transactional(propagation = REQUIRES_NEW), @Async hay @Cacheable trên phương thức được gọi đều bị vô hiệu hóa trong âm thầm.
Self-call bypass không hề ném ra bất kỳ Exception nào lúc chạy. Phương thức validate() vẫn chạy bình thường nhưng chia sẻ chung transaction với placeOrder(). Nếu logic này được thiết kế để lưu log thanh tra độc lập, toàn bộ log sẽ bị rollback theo đơn hàng nếu đơn hàng gặp lỗi.
6. Làm thế nào để khắc phục triệt để cạm bẫy Self-Call Bypass?
Có 3 phương án xử lý lỗi self-call, trong đó tách lớp là giải pháp được khuyến nghị cao nhất:
Cách 1: Tách phương thức sang bean riêng biệt (Khuyến nghị chuẩn SRP)
Giải pháp sạch nhất theo nguyên lý Single Responsibility là chuyển logic cần aspect sang một Spring bean độc lập:
@Service
public class OrderService {
private final OrderValidator validator;
public OrderService(OrderValidator validator) {
this.validator = validator;
}
@Transactional
public void placeOrder(Order order) {
// Di qua proxy cua OrderValidator -> REQUIRES_NEW hoat dong chinh xac
validator.validate(order);
}
}
@Service
public class OrderValidator {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { /* ghi log hoac kiem tra doc lap */ }
}
Cách 2: Tiêm chính mình thông qua Self-Injection và @Lazy
Nếu logic quá nhỏ và việc tách file gây phân mảnh mã nguồn, bạn có thể tiêm proxy của chính class vào bên trong:
@Service
public class OrderService {
@Autowired
@Lazy // Bat buoc tren Spring Boot 2.6+ de tranh loi Circular Dependency
private OrderService self;
@Transactional
public void placeOrder(Order order) {
self.validate(order); // Di qua proxy -> Aspect duoc kich hoat
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { /* xu ly */ }
}
Lưu ý: Từ Spring Boot 2.6, tham chiếu vòng bị chặn mặc định. Bạn bắt buộc phải thêm @Lazy để Spring hoãn việc khởi tạo proxy vòng lặp.
Cách 3: Lấy Proxy từ ApplicationContext
Trong các hệ thống kế thừa cũ không thể tái cấu trúc, ta có thể nhờ ApplicationContext lấy lại bean proxy tại thời điểm chạy:
@Service
public class OrderService implements ApplicationContextAware {
private ApplicationContext context;
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
@Transactional
public void placeOrder(Order order) {
// Lay proxy truc tiep tu Container
context.getBean(OrderService.class).validate(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validate(Order order) { /* xu ly */ }
}
7. Tại sao từ khóa final làm hỏng cơ chế Proxy của CGLIB?
Vì CGLIB sử dụng cơ chế kế thừa để tạo proxy subclass, từ khóa final trong Java sẽ gây ra các vấn đề nghiêm trọng:
// 1. Class final: Container nem exception ngay khi khoi dong
@Service
@Transactional
public final class OrderService { /* Loi: Cannot subclass final class */ }
// 2. Method final: Proxy tao duoc nhung AOP advice bi bo qua am tham
@Service
public class OrderService {
@Transactional
public final void placeOrder(Order order) {
// Subclass khong the override method nay -> Aspect khong the can thiep!
}
}
Khi method mang từ khóa final, CGLIB không thể override method đó trong subclass. Kết quả là lời gọi sẽ thực thi thẳng mã của lớp cha mà không qua bất kỳ khâu xử lý AOP nào, nhưng ứng dụng không hề báo lỗi startup.
8. Đọc sâu mã nguồn AOP Proxy ở những lớp nào trong Spring?
Để hiểu sâu hơn về cơ chế nội tại của Spring AOP, bạn nên đọc trực tiếp các lớp sau trong thư viện:
AbstractAutoProxyCreator.postProcessAfterInitialization(): Nơi quyết định bọc bean trong proxy hoặc trả về bean nguyên bản.DefaultAopProxyFactory.createAopProxy(): Lớp đưa ra quyết định chọn JDK Dynamic Proxy hay CGLIB dựa trên cấu hình và interface.CglibAopProxyvàJdkDynamicAopProxy: Hai lớp hiện thực cốt lõi của hai cơ chế Dynamic Proxy trong Spring.
Tóm tắt
- Dynamic Proxy được chèn ở bước 8 — BeanPostProcessor after-init của vòng đời bean để đảm nhận các cross-cutting concerns.
- JDK Dynamic Proxy phụ thuộc vào interface của Java SE; CGLIB can thiệp bytecode để tạo subclass kế thừa bean (Spring Boot 2.0+ mặc định CGLIB).
- Lời gọi nội bộ
this.method()bypass qua proxy khiến@Transactionalhay@Asyncbị vô hiệu hóa; xử lý bằng tách bean độc lập hoặc dùng@Lazyself-injection. - Tránh đặt từ khóa
finaltrên class hoặc method có AOP vì Java cấm kế thừa và override, khiến CGLIB không thể chèn advice.
Tự kiểm tra
- Q1Dynamic Proxy của Spring AOP được tạo ra và thay thế bean gốc trong Container ở giai đoạn nào?
- Q2Hạn chế lớn nhất của JDK Dynamic Proxy so với CGLIB là gì?
- Q3Tại sao phương thức validate() được gắn @Transactional(propagation = REQUIRES_NEW) lại không mở transaction mới khi được gọi từ placeOrder() trong cùng class?
- Q4Khi áp dụng kỹ thuật Self-Injection trên Spring Boot 2.6+, tại sao bắt buộc phải thêm annotation @Lazy?
- Q5Điều gì xảy ra khi bạn đánh dấu @Transactional trên một phương thức mang từ khóa final trong bean dùng CGLIB Proxy?
Bài tiếp theo: Singleton & Prototype Scope: Vòng Đời Và Cạm Bẫy Scope Mismatch
Bài này có giúp bạn hiểu bản chất không?
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.
Vòng Đời Của Bean: 9 Giai Đoạn Từ Khởi Tạo Đến Hủy Bỏ
Bài tiếpSingleton Và Prototype Scope: Cạm Bẫy Scope Mismatch
Lộ trình Junior Java Backend Developer
Chặng 13/21 · Spring IoC, DI & Bean — bài 9/17 của chặ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.
Thảo luận & góp ý
Hỏi đáp về bài này