Spring BootMiddleCoding

Как реализовать асинхронную обработку в Spring Boot с помощью @Async?

@Async работает через AOP-прокси: Spring перехватывает вызов и отправляет его в TaskExecutor. Требуется @EnableAsync и вызов метода через Spring-бин (не this.method()), иначе прокси не срабатывает.

Как работает @Async

Spring создаёт CGLIB-прокси вокруг бина с @Async-методами. При вызове метода прокси перехватывает его, оборачивает в Callable и передаёт в TaskExecutor. Оригинальный поток возвращает управление немедленно; метод исполняется в пуле потоков.

Метод может возвращать void, Future<T> или CompletableFuture<T>.

Минимальная настройка

@SpringBootApplication
@EnableAsync   // активирует async-инфраструктуру
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Настройка пула потоков

@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
        exec.setCorePoolSize(4);
        exec.setMaxPoolSize(16);
        exec.setQueueCapacity(200);
        exec.setThreadNamePrefix("async-");
        // CallerRunsPolicy: если очередь заполнена — выполняет в вызывающем потоке
        exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        exec.initialize();
        return exec;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
            LoggerFactory.getLogger(getClass())
                .error("Async error in {}: {}", method.getName(), ex.getMessage(), ex);
    }
}

Сервис с @Async

@Service
public class ReportService {

    // void — fire-and-forget
    @Async
    public void generateReport(Long userId) {
        // долгая операция: PDF, email...
        log.info("Generating for user {} in thread {}",
            userId, Thread.currentThread().getName());
    }

    // CompletableFuture — можно дождаться результата
    @Async
    public CompletableFuture<String> fetchExternalData(String url) {
        String result = restClient.get().uri(url).retrieve().body(String.class);
        return CompletableFuture.completedFuture(result);
    }
}

Использование в контроллере

@RestController
@RequiredArgsConstructor
public class ReportController {

    private final ReportService reportService;

    @PostMapping("/reports")
    public ResponseEntity<Void> trigger(@RequestParam Long userId) {
        reportService.generateReport(userId);  // возвращается сразу
        return ResponseEntity.accepted().build(); // 202
    }
}

Тест асинхронного метода

@SpringBootTest
class ReportServiceTest {

    @Autowired
    private ReportService service;

    @Test
    void fetchExternalData_returnsResult() throws Exception {
        CompletableFuture<String> future = service.fetchExternalData("https://example.com");
        String result = future.get(5, TimeUnit.SECONDS);
        assertThat(result).isNotBlank();
    }
}

Подводные камни

  • Вызов через this — прокси не срабатывает: если внутри того же класса вызвать this.generateReport(), AOP-перехват не происходит и метод выполняется синхронно. Решение: выделить async-метод в отдельный бин.
  • private-методы игнорируются: CGLIB не может переопределить приватный метод. @Async на private-методе молча не работает.
  • void + исключение = тихая смерть: если метод возвращает void и бросает исключение, оно не дойдёт до вызывающего. Обязательно настройте AsyncUncaughtExceptionHandler.
  • Неограниченная очередь = OutOfMemoryError: дефолтный SimpleAsyncTaskExecutor создаёт новый поток на каждый вызов без лимита. В продакшне всегда используйте ThreadPoolTaskExecutor с ограниченной очередью.
  • SecurityContext не передаётся автоматически: в async-потоке SecurityContextHolder пуст. Нужно явно настроить DelegatingSecurityContextAsyncTaskExecutor.
  • @Transactional + @Async = разные транзакции: async-метод выполняется в новом потоке без транзакции вызывающего. Транзакцию нужно открывать внутри async-метода.
  • Readiness probe в Kubernetes: долгий async-метод, запущенный при старте через ApplicationRunner, может задержать сигнал готовности. Всегда разграничивайте инициализацию и обработку запросов.
  • Мониторинг пула: без метрик пул незаметно насыщается. Подключите Actuator (management.endpoints.web.exposure.include=metrics) и наблюдайте executor.active, executor.queued.

Common mistakes

  • Путать термин «boot async» с соседним механизмом Spring Boot.
  • Не называть границу lifecycle, transaction, thread или request для «boot async».
  • Игнорировать production-эффекты «boot async»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «boot async» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «boot async».
  • Уточнить, какие настройки или API меняют «boot async» в реальном сервисе.

Sources

Related topics