Spring Core & Boot
17/41
Bài 17 / 41~12 phútBean Lifecycle & ScopesLộ trình · chặng 13/21Miễn phí… lượt xem

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:

Java
// 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:

Java
// 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.

Bean gốc sẵn sàng sau bước 1 đến 7, tới bước 8 BeanPostProcessor hỏi có aspect nào khớp không: khớp thì bọc gốc trong proxy và trả về proxy, không khớp thì trả về chính bean gốc; singletonObjects giữ đúng thứ được trả về

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:

Java
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:

Java
// 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:

Java
@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:

Java
// 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ánhJDK Dynamic ProxyCGLIB Proxy
Yêu cầu InterfaceBắt buộc phải có interfaceKhông cần interface
Cơ chế kỹ thuậtjava.lang.reflect.ProxySinh subclass bằng Bytecode manipulation
Tên class runtime$ProxyNNTargetBean$$SpringCGLIB$$NN
instanceof TargetClassfalsetrue
Giới hạn từ khóa finalKhông bị ảnh hưởngKhô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:

Java
@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
    }
}

Hai đường gọi tới cùng một object: đường dọc từ controller đi xuyên qua hộp OrderService PROXY nên transaction được mở, còn đường ngang từ chính bean gốc sang method validate cùng class thì không đi qua proxy nên transaction REQUIRES_NEW không bao giờ mở

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.

Lỗi vô hình nguy hiểm trong production

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:

Java
@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:

Java
@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:

Java
@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:

Java
// 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.
  • CglibAopProxy và 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 @Transactional hay @Async bị vô hiệu hóa; xử lý bằng tách bean độc lập hoặc dùng @Lazy self-injection.
  • Tránh đặt từ khóa final trê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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Dynamic Proxy của Spring AOP được tạo ra và thay thế bean gốc trong Container ở giai đoạn nào?
  2. Q2
    Hạn chế lớn nhất của JDK Dynamic Proxy so với CGLIB là gì?
  3. Q3
    Tạ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?
  4. Q4
    Khi á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?
  5. 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.

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

Bài tiếp theo

Singleton Và Prototype Scope: Cạm Bẫy Scope Mismatch