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

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ề

Đ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ạ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. 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()
    }
}

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ở

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 ném exception, chỉ âm thầm sai

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ạ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. 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

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

  • Proxy được tạo bởi AnnotationAwareAspectJAutoProxyCreator (BPP after-init, bước 8) và đặt vào singletonObjects thay bean gốc — mọi @Autowired từ đó 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 @Lazy tránh circular), (3) ApplicationContext.getBean() (anti-pattern).
  • Class final → fail startup; method final → 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ần kotlin-allopen).

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. 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.
  2. 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.
  3. 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) { ... }
  4. 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?
  5. 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ì?

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

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

Bài tiếp theo

Singleton vs Prototype scope — cache và scope-mismatch trap