Spring Core & Boot/AOP Proxy — JDK Dynamic Proxy vs CGLIB và pitfall self-call
17/41
Bài 17 / 41~12 phútBean Lifecycle & ScopesMiễn phí lượt xem

AOP Proxy — JDK Dynamic Proxy vs CGLIB và pitfall self-call

Spring không sửa source code của bạn để thêm @Transactional hay @PreAuthorize — thay vào đó nó wrap bean trong một proxy object tại init phase. Bài này bóc tách cơ chế BeanPostProcessor tạo proxy, hai kiểu proxy JDK Dynamic Proxy (interface-based) và CGLIB (subclass bytecode), khi nào Spring chọn cái nào, và vì sao self-call this.method() âm thầm bypass toàn bộ AOP annotation.

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. Câu hỏi phóng to của bài này: proxy đó được tạo ra bằng cơ chế nào, nó 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 đặt cross-cutting concern thông qua proxy pattern — wrap bean gốc trong một object trung gian có cùng interface/class. Mọi call từ ngoài đi qua proxy trước, proxy chèn concern logic, rồi delegate xuống bean gốc.

flowchart LR
    Caller["Controller<br/>goi service.placeOrder()"]
    Proxy["OrderService PROXY<br/>1. bat dau TX<br/>2. check security<br/>3. delegate xuong goc<br/>4. commit/rollback TX"]
    Real["OrderService<br/>(bean goc)<br/>chi co business logic"]

    Caller --> Proxy --> Real
    Real -.->|"ket qua"| Proxy
    Proxy -.->|"ket qua"| Caller

    style Proxy fill:#fef3c7
    style Real fill:#d1fae5

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.

flowchart TB
    A["1-7: Instantiate, Populate,<br/>PostConstruct, init-method<br/>(bean goc san sang)"]
    B["8: BPP.postProcessAfterInitialization<br/>AnnotationAwareAspectJAutoProxyCreator<br/>kiem tra: co aspect match?"]
    C{"Match?"}
    D["Wrap bean goc trong proxy<br/>return proxy object"]
    E["Khong wrap<br/>return bean goc"]
    F["Container dat vao singletonObjects"]

    A --> B --> C
    C -->|"CO @Transactional<br/>@Async @PreAuthorize"| D
    C -->|"Khong"| E
    D --> F
    E --> F

    style D fill:#fef3c7

Đ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
                               // Spring tu dong fall back sang 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ạnhJDK Dynamic ProxyCGLIB Proxy
Yêu cầu beanPhải implement ít nhất 1 interfaceKhông cần interface
Cơ chếjava.lang.reflect.Proxy — implement interfaceBytecode generation — tạo subclass
Tên proxy class$Proxy42 (anonymous)OrderService$$SpringCGLIB$$0
instanceof BeanClassfalsetrue
Method finalKhông liên quan (interface không có final)Không proxy được — subclass không override final
Class finalKhông liên quanKhông proxy được — không subclass được
Thư việnJava SE built-inCGLIB (đóng gói trong spring-core)

Spring chọn proxy nào? Luật chọn theo thứ tự ưu tiên:

  1. spring.aop.proxy-target-class=true (hoặc @EnableAspectJAutoProxy(proxyTargetClass=true)) → luôn CGLIB.
  2. Spring Boot 2.0+ đặt proxy-target-class=true mặc định → CGLIB cho tất cả.
  3. 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. Pitfall tối quan trọng — self-call 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()
    }
}
flowchart LR
    C["Controller"]
    P["OrderService PROXY<br/>(bat TX cho placeOrder)"]
    B["OrderService BEAN GOC"]

    C -->|"service.placeOrder()"| P
    P -->|"delegate"| B
    B -->|"this.validate(order)<br/>direct call -- bypass proxy"| B

    style P fill:#fef3c7
    style B fill:#d1fae5

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.

Lỗi vô hình — không throw, chỉ silently sai

Self-call bypass nguy hiểm 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 — Spring giải quyết được nhưng cần khai báo @Lazy trên field nếu Spring Boot phiên bản cũ hơn 2.6 throw error.

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ạchKhi nào dùng
Tách 2 classTốt nhất — đúng nguyên tắc SRPMặc định
Self-injectChấp nhận đượcKhi validate/helper logic quá nhỏ để tách class
ApplicationContext.getBeanWorst — service locatorChỉ 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. Deep Dive

Tài liệu và source để đọc sâu hơn

Spring Reference:

Source để đọc:

Java SE:

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 & BeanFactoryPostProcessorBeanPostProcessor (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 singletonObjects map sau khi tạo. Bài đó giải thích map này là gì và tại sao lần sau getBean trả 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

  • Spring AOP không sửa source code — nó wrap bean trong proxy object để chèn cross-cutting concern (transaction, security, async) minh bạch.
  • Proxy được tạo bởi BeanPostProcessor.postProcessAfterInitialization — cụ thể là AnnotationAwareAspectJAutoProxyCreator — ngay sau init phase.
  • JDK Dynamic Proxy: tạo anonymous class implement interface bean, dùng java.lang.reflect.Proxy. instanceof BeanClass trả false. Yêu cầu bean có interface.
  • CGLIB Proxy: sinh subclass bytecode của bean class. instanceof BeanClass trả true. Không cần interface nhưng không hoạt động với class/method final.
  • Spring Boot 2.0+ mặc định CGLIB cho mọi bean (proxy-target-class=true) — nhất quán hơn.
  • Pitfall self-call: this.method() trong cùng bean không qua proxy@Transactional, @Async, @PreAuthorize trên method đó mất tác dụng. 3 fix: tách class (tốt nhất), inject self bean, hoặc ApplicationContext.getBean() (tệ nhất).
  • Class/method final không thể CGLIB proxy — tránh final trên bean class.

Tự kiểm tra

Tự kiểm tra
Q1
Khi bạn gọi service.placeOrder(order) từ controller, biến service thực ra trỏ tới gì? Giải thích luồng thực thi từ controller tới business logic.

Biến service trỏ tới proxy object, không phải bean gốc. Proxy được Spring tạo ra tại bước 8 của lifecycle (BPP after-init) và đặt vào singletonObjects thay cho bean gốc.

Luồng thực thi: Controller gọi service.placeOrder(), call rơi vào proxy. Proxy chạy advice trước (ví dụ bắt đầu transaction, kiểm tra security), rồi delegate xuống bean gốc bằng cách gọi method trên target object. Bean gốc thực thi business logic, kết quả quay về proxy; proxy chạy advice sau (commit/rollback transaction) rồi mới trả kết quả về controller.

Bean gốc vẫn tồn tại trong heap, proxy giữ tham chiếu đến nó (qua TargetSource). Bean gốc không biết mình bị wrap — nó chỉ thực thi khi được proxy delegate tới.

Q2
Spring 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 stripe nhưng bean có interface PaymentGateway.

(a) Bean implement interface, Spring Boot 2.3: CGLIB. Spring Boot 2.0+ đặt spring.aop.proxy-target-class=true mặc định — luôn CGLIB bất kể bean có interface hay không. JDK proxy chỉ được dùng nếu config thủ công tắt flag này.

(b) Bean không có interface: CGLIB. Không có interface để JDK proxy implement — CGLIB là lựa chọn duy nhất dù ở bất kỳ Spring version nào.

(c) @Autowired StripePaymentGateway stripe: Nếu đang ở chế độ JDK proxy (Spring Framework default không có Spring Boot), inject theo class gốc sẽ fail vì proxy là anonymous class không extend StripePaymentGateway. Spring tự fallback sang CGLIB để thỏa mãn injection. Với Spring Boot 2.0+ (CGLIB mặc định), không có vấn đề — proxy là subclass của bean class, inject được.

Bài học: code nên depend vào interface khi inject, tránh inject theo class gốc để không bị mode-dependent.

Q3
Service sau có @Transactional trên cả 2 method. Khi controller gọi placeOrder(), transaction nào được áp dụng cho auditLog()? REQUIRES_NEW có tạo transaction mới không?

@Transactional public void placeOrder(Order o) { auditLog(o); }
@Transactional(propagation = REQUIRES_NEW) public void auditLog(Order o) { ... }

Không — REQUIRES_NEW không có hiệu lực. auditLog() được gọi bằng this.auditLog(o) (self-call) — call này đi trực tiếp đến bean gốc, không qua proxy. Annotation @Transactional(propagation = REQUIRES_NEW) chỉ được xử lý khi proxy intercept call — mà proxy không thấy call này.

Hệ quả: auditLog() chạy trong cùng transaction với placeOrder(). Nếu placeOrder() rollback, auditLog() cũng rollback — ngược với ý định của REQUIRES_NEW.

Cách fix tốt nhất: tách auditLog() sang class AuditService riêng, inject vào OrderService. Call từ OrderService tới auditService.auditLog() sẽ qua proxy của AuditServiceREQUIRES_NEW có tác dụng, transaction mới được tạo độc lập.

Q4
Vì sao class bean final gây lỗi startup khi dùng với @Transactional trong Spring Boot? Method final khác với class final ở hậu quả nào?

Class final: CGLIB tạo proxy bằng cách sinh subclass — nhưng Java không cho phép subclass của class final. Spring phát hiện điều này khi tạo proxy, ném IllegalArgumentException: Cannot subclass final class ngay lúc startup (container fail to start). Rõ ràng, fail fast.

Method final: Class vẫn có thể subclass được — proxy được tạo thành công. Nhưng CGLIB không thể override method final để chèn advice. Hậu quả: proxy tồn tại, bean inject vào controller được, nhưng khi gọi method đó annotation không có hiệu lực — không có transaction, không có security check. Lỗi này khó phát hiện hơn vì không throw exception lúc startup, chỉ lộ ra khi chạy (behavior sai).

Kotlin mặc định mọi class và method là final — Spring Boot tự động kích hoạt kotlin-allopen plugin để mở open cho các class có Spring annotation. Nếu tắt plugin đó hoặc dùng Kotlin không qua Spring Boot autoconfigure, sẽ gặp đúng 2 vấn đề trên.

Q5
Bạn có bean ReportService không implement interface. Method generate()@Transactional. Sau khi inject vào controller, controllerReportService.getClass().getName() in ra gì? controllerReportService instanceof ReportService trả về gì?

getClass().getName() in ra tên dạng ReportService$$SpringCGLIB$$0 (hoặc tương tự với EnhancerByCGLIB tùy Spring version). Đây là class được CGLIB sinh ra tại runtime, extend ReportService.

instanceof ReportService trả về true. Vì proxy là subclass của ReportService, nó thỏa mãn instanceof check của class gốc.

Đây là điểm CGLIB vượt trội JDK proxy trong trường hợp code có instanceof check hoặc downcast về class gốc. JDK proxy chỉ implement interface — instanceof BeanClass sẽ false, downcast sẽ ClassCastException.

Verify nhanh khi debug: print getClass().getName() — thấy $$SpringCGLIB là biết bean đang bị wrap CGLIB proxy, thấy $Proxy là JDK proxy.

Bài tiếp theo: Singleton & Prototype scope

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt 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