SPRING SERIES - LỖI IM LẶNG

@Async Không Async Và Lỗi Mất Tích

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 - annotation bị lờ đi

@Async chạy như lời gọi Java bình thường trên thread của caller

Cùng một biên proxyChưa đăng ký async BPP thì không có advice. Có advice rồi, lỗi được định tuyến theo kiểu trả về thay vì ném đồng bộ về caller.

Triệu chứng B - caller không nhận lỗi

void đưa lỗi vào handler; Future giữ failure để consumer quan sát

Mức 1 - từ enable tới proxy

Đăng Ký Async Advisor

Đường mặc định AdviceMode.PROXY

  1. @EnableAsyncImport AsyncConfigurationSelector vào local application context
  2. selectImports(PROXY)Chọn ProxyAsyncConfiguration
  3. ProxyAsyncConfiguration.asyncAdvisor()Khai bean infrastructure cho async annotation
  4. new AsyncAnnotationBeanPostProcessor()Tạo BPP cùng AsyncAnnotationAdvisor
  5. registerBeanPostProcessors()Đưa async BPP vào chuỗi xử lý bean
  6. postProcessAfterInitialization()Thêm advisor vào proxy sẵn có hoặc tạo proxy cho bean phù hợp

Bất đối xứng của Boot 3.2.5: TransactionAutoConfiguration bật transaction management khi có bean TransactionManager. TaskExecutionAutoConfiguration có thể cấp taskExecutor, nhưng không import @EnableAsync.

Mức 2 - zoom async advice

AsyncExecutionInterceptor.invoke()

Submit task và trả quyền điều khiển

  1. proxy.sendWelcomeEmail()Lời gọi từ ngoài đi vào async advisor
  2. determineAsyncExecutor(userMethod)Chọn qualifier, unique TaskExecutor/taskExecutor hoặc local fallback
  3. Tạo CallableWorker sẽ gọi invocation.proceed() trên target
  4. Worker bắt ThrowableChuyển lỗi vào handleError()
  5. doSubmit(task, executor, returnType)Submit task theo kiểu trả về đã khai báo
  6. Proxy trả về callernull cho void; actual async handle cho Future

Kiểm chứng: breakpoint tại invoke(), doSubmit() và log tên thread ở caller/target. Khác thread chứng minh invocation đó đã qua async boundary; cùng thread chưa chỉ ra duy nhất một nguyên nhân.

Mức 3 - zoom handleError

Hai Kênh Nhận Exception

Caller không nhận lại exception theo stack đồng bộ

Method trả void

  1. handleError()Return type void không thuộc họ Future
  2. AsyncUncaughtExceptionHandlerNhận exception, method và arguments
  3. SimpleAsyncUncaughtExceptionHandlerMặc định log ERROR; caller vẫn không có error channel

Method trả Future

  1. handleError() rethrow trong taskNhánh Future.isAssignableFrom(returnType)
  2. Executor capture failureProxy đã trả actual Future hoặc CompletableFuture
  3. Consumer phải quan sátget(), join(), whenComplete() hoặc exceptionally()

Ưu tiên sửa

  • Bật và kiểm chứng async infrastructureThêm @EnableAsync, gọi qua proxy và xác nhận advice thực sự được chạm.
  • Kết thúc mọi kênh lỗiCustom handler cho void; luôn consume hoặc compose Future.

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

  • Cùng thread nghĩa là chắc chắn thiếu @EnableAsyncCòn có self-invocation, executor đồng bộ hoặc rejection policy kiểu caller-runs.
  • void exception mặc định không tạo logSai: default handler log ERROR, nhưng không truyền lỗi về caller.
  • Có executor là @Async đã bậtExecutor chỉ là nơi chạy task; async BPP/advisor vẫn phải được đăng ký.