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