Spring FrameworkSeniorTechnical
Как Spring поддерживает асинхронное выполнение методов с помощью @Async и @EnableAsync?
@EnableAsync активирует AsyncAnnotationBeanPostProcessor, который создаёт прокси вокруг бинов с @Async. При вызове метода прокси отправляет задачу в TaskExecutor (по умолчанию SimpleAsyncTaskExecutor) и немедленно возвращает Future/CompletableFuture или void.
Как работает @Async под капотом
@EnableAsync регистрирует AsyncAnnotationBeanPostProcessor (или ProxyAsyncConfiguration при mode=PROXY). Постпроцессор обёртывает каждый бин, содержащий методы с @Async, в CGLIB/JDK-прокси. При вызове такого метода прокси перехватывает вызов и отправляет Runnable/Callable в TaskExecutor, а вызывающему потоку возвращает CompletableFuture (или void).
Настройка Executor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async error in {}: {}", method.getName(), ex.getMessage(), ex);
}
}
Рабочий пример
@Service
public class NotificationService {
// Возвращаем CompletableFuture — можно ждать результат
@Async
public CompletableFuture<String> sendEmail(String address) {
// выполняется в потоке пула, не блокирует HTTP-поток
emailClient.send(address);
return CompletableFuture.completedFuture("sent to " + address);
}
// void — fire-and-forget
@Async("reportExecutor") // явно указываем именованный executor
public void generateReport(Long reportId) {
reportBuilder.build(reportId);
}
}
@RestController
@RequiredArgsConstructor
public class OrderController {
private final NotificationService notificationService;
@PostMapping("/orders")
public ResponseEntity<Order> placeOrder(@RequestBody OrderRequest req) {
Order order = orderService.create(req);
// HTTP-поток не блокируется — письмо уходит асинхронно
notificationService.sendEmail(req.getEmail());
return ResponseEntity.ok(order);
}
}
Ожидание нескольких задач
@Async
public CompletableFuture<ReportData> fetchSales() { /* ... */ }
@Async
public CompletableFuture<ReportData> fetchReturns() { /* ... */ }
// В вызывающем методе (НЕ @Async!):
public DashboardData buildDashboard() throws Exception {
CompletableFuture<ReportData> sales = reportService.fetchSales();
CompletableFuture<ReportData> returns = reportService.fetchReturns();
CompletableFuture.allOf(sales, returns).join();
return new DashboardData(sales.get(), returns.get());
}
Подводные камни
- Self-invocation не работает: вызов
this.asyncMethod()внутри того же класса обходит прокси — задача выполнится синхронно без предупреждения. - @Async на private-методе игнорируется: CGLIB не может переопределить private-методы; Spring не бросает ошибку, просто выполняет метод синхронно.
- SimpleAsyncTaskExecutor по умолчанию не переиспользует потоки: создаёт новый поток на каждый вызов; при высокой нагрузке это убьёт сервер. Всегда настраивайте
ThreadPoolTaskExecutor. - Исключения в void-методах теряются: прокси перехватывает их и логирует только если настроен
AsyncUncaughtExceptionHandler; без этого ошибка исчезает бесследно. - @Transactional + @Async — разные потоки, разные транзакции: транзакция вызывающего метода не распространяется в асинхронный метод; каждый поток открывает свою транзакцию.
- SecurityContext не передаётся автоматически: по умолчанию Spring Security не копирует
SecurityContextв дочерний поток; нужно настроитьSecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL)илиDelegatingSecurityContextExecutor. - Слушатели ApplicationEvent + @Async:
@EventListener @Asyncвыглядит удобно, но если обработчик бросает исключение и никто его не ждёт, событие обрабатывается «наполовину» без видимой ошибки в логах. - Завершение контекста может обрывать задачи: при shutdown
ThreadPoolTaskExecutorждёт завершения задач только если настроенsetWaitForTasksToCompleteOnShutdown(true); иначе пул прерывается немедленно.
Common mistakes
- Путать термин «async enableasync» с соседним механизмом Spring Framework.
- Не называть границу lifecycle, transaction, thread или request для «async enableasync».
- Игнорировать production-эффекты «async enableasync»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «async enableasync» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «async enableasync».
- Уточнить, какие настройки или API меняют «async enableasync» в реальном сервисе.