SPRING SERIES - LỖI IM LẶNG

Vỏ Bọc Proxy Và Lỗi Bốc Hơi Transaction

PROXY - XANH = TẦNG PHÓNG TO Đọc từ trái sang phải. Màu đậm dần nghĩa là gần cơ chế hơn. Dấu * đánh dấu điểm bài viết khai thác.

Triệu chứng A - Tên class lạ

getClass() in ra tên có $$SpringCGLIB$$ thay vì class tự viết

Cùng một gốcProxy là một lớp vỏ bọc quanh bean thật, sinh ra tự động lúc runtime.

Triệu chứng B - Advice bốc hơi

@Transactional trên method final không mở transaction

Mức 1 - đường tạo proxy

AbstractAutoProxyCreator

Từ BPP cuối lifecycle tới implementation thật

  1. postProcessAfterInitialization()Trạm chốt chặn cuối của dây chuyền tạo bean
  2. wrapIfNecessary()Tìm advice; trả bean gốc nếu không cần proxy
  3. createProxy() -> buildProxy()Dựng ProxyFactory, advisors và TargetSource
  4. ProxyFactory.getProxy()Yêu cầu factory tạo proxy theo cấu hình đã chốt
  5. ProxyCreatorSupport.createAopProxy()Chuyển cấu hình sang AopProxyFactory
  6. DefaultAopProxyFactory.createAopProxy()Chọn implementation JDK hay CGLIB
  7. AopProxy.getProxy()Implementation sinh object proxy và trả về context

Hai loại vỏ, hai kiểu "giống bạn": JDK proxy chỉ đeo cùng biển - nó implement cùng interface, nên chỉ có những method nằm trên interface. Method public nào bạn viết thêm ở class mà quên khai lên interface thì proxy không có, và autowire bằng class cụ thể sẽ nổ. CGLIB proxy là con ruột - kế thừa chính class của bạn nên có đủ method. Đó là lý do Boot lật mặc định sang CGLIB thay vì giữ luật JDK-first của Spring Framework.

Mức 2 - zoom bước 6

DefaultAopProxyFactory.createAopProxy()

Đọc flag đã có, không đọc file cấu hình

  1. isOptimize() || isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)Nếu đúng, kiểm tra tiếp loại target class
  2. Target null, interface, JDK proxy hoặc lambdaTrả JdkDynamicAopProxy, kể cả khi flag class proxy đang bật
  3. Target là class concreteTrả ObjenesisCglibAopProxy
  4. Flag false và có user-supplied interfaceĐi thẳng sang JdkDynamicAopProxy

Flag được đặt từ trước: AopAutoConfiguration của Boot ánh xạ spring.aop.proxy-target-class vào auto-proxy creator; mặc định là true. Method trên chỉ đọc trạng thái đã có.

Kiểm chứng trong 30 giây

AopUtils.isAopProxy(), isCglibProxy(), isJdkDynamicProxy()getTargetClass() trả lời bốn câu khác nhau.

Đặt spring.aop.proxy-target-class=false bằng một property source của Boot, restart rồi gọi bean qua interface để quan sát nhánh JDK.

Mức 3 - zoom nơi đúc CGLIB

Vỏ ObjenesisCglibAopProxy

Cơ chế kế thừa và các giới hạn

  • Sinh subclass (lớp con) lúc runtimeKế thừa trực tiếp từ Target class thật
  • Tên class $$SpringCGLIB$$0Dấu hiệu nhận biết lớp vỏ CGLIB
  • Override method có thể chặnCallback đưa lời gọi qua Advice rồi sang target
  • Method finalĐược kế thừa nhưng không override; lời gọi không được route sang target và có thể chạm field rỗng trên proxy
  • Method privateKhông được kế thừa; subclass không nhìn thấy để tạo điểm chặn

Lối ra an toàn

  • Hỏi đúng câuDùng getClass() để xem lớp ngoài; dùng AopUtils.getTargetClass(bean) để hỏi target class Spring xác định được.
  • Giữ đường vào có thể chặnĐặt advice trên method public, non-final và gọi method đó qua proxy.

Khi KHÔNG được kết luận

  • Mất transaction là do method finalChưa đủ. Lời gọi nội bộ (self-invocation) cũng làm transaction bốc hơi dù method hợp lệ.
  • Tắt bằng @EnableAspectJAutoProxy(proxyTargetClass = false)Không hạ được flag true đã hợp nhất trong context Boot; dùng property spring.aop.proxy-target-class=false.