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» в реальном сервисе.

Sources

Related topics