@Async в Spring + custom TaskExecutor
Цель урока
Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.
Теория · для собеса
1 Синхронно vs асинхронно ▸
Синхронно (default):
public TaskResponse create(CreateTaskRequest req) {
Task saved = repo.save(task);
notificationService.sendEmail(saved); // ← ждём 2 сек
return mapper.toResponse(saved); // пользователь ждёт 2 сек
}
Асинхронно:
public TaskResponse create(CreateTaskRequest req) {
Task saved = repo.save(task);
notificationService.sendEmailAsync(saved); // ← запустили и забыли
return mapper.toResponse(saved); // пользователь не ждёт
}
2
@Async — как работает
▸
- Spring AOP создаёт прокси вокруг бина
- Когда вызывается
@Asyncметод — прокси перехватывает - Задача отправляется в
TaskExecutor(пул потоков) - Вызывающий поток немедленно возвращается
⚠️ Важно: вызов @Async метода из того же класса НЕ работает (прокси не активируется). Это та же проблема, что с @Transactional.
3 ThreadPoolTaskExecutor — пул потоков ▸
Если @Async без настройки — Spring использует SimpleAsyncTaskExecutor (создаёт новый поток на каждый вызов). В проде это плохо.
Настройка своего пула:
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(5);
ex.setMaxPoolSize(20);
ex.setQueueCapacity(100);
ex.setThreadNamePrefix("taskflow-");
ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
ex.initialize();
return ex;
}
CallerRunsPolicy: если очередь полна — задача выполняется в вызывающем потоке (деградация, но не отказ).
---
Практика: улучшаем CRUD
-
1
config/AsyncConfig.javapackage com.taskflow.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfig { @Bean(name = "taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(5); ex.setMaxPoolSize(20); ex.setQueueCapacity(100); ex.setThreadNamePrefix("taskflow-async-"); ex.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()); ex.setWaitForTasksToCompleteOnShutdown(true); ex.setAwaitTerminationSeconds(30); ex.initialize(); return ex; } } -
2
service/NotificationService.javapackage com.taskflow.service; import com.taskflow.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class NotificationService { private static final Logger log = LoggerFactory.getLogger(NotificationService.class); @Async("taskExecutor") public void sendTaskCreatedNotification(Task task) { log.info("Sending 'task created' notification for task id={}, owner={}", task.getId(), task.getOwner().getUsername()); // Имитация отправки email (2 сек) try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("Notification sent for task id={}", task.getId()); } @Async("taskExecutor") public java.util.concurrent.CompletableFuturesendTaskCreatedWithResult(Task task) { sendTaskCreatedNotification(task); return java.util.concurrent.CompletableFuture.completedFuture("sent"); } } -
3Использование в
TaskServicepublic TaskResponse create(CreateTaskRequest req) { User me = currentUser.get(); Task task = mapper.toEntity(req); task.setOwner(me); Task saved = repo.save(task); // Запускаем асинхронно — НЕ блокируем ответ notificationService.sendTaskCreatedNotification(saved); return mapper.toResponse(saved); } -
4Возврат
CompletableFutureиз Controller@GetMapping("/{id}/notify") public java.util.concurrent.CompletableFuturenotify(@PathVariable Long id) { Task task = repo.findById(id).orElseThrow(); return notificationService.sendTaskCreatedWithResult(task); } -
5Логирование (видно имя потока)
# Синхронно http-nio-8080-exec-1 Sending notification for task id=42 # Асинхронно taskflow-async-1 Sending notification for task id=42---
Зачем это на собесе
После урока ты должен уметь ответить на:
@Async изолирует медленные операции от критического path.taskflow-async-1 легко грепать, видно где задача выполняется.5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: AOP-прокси и вызов метода через Spring bean.
Разбор внутри: потерянные ошибки, AsyncUncaughtExceptionHandler и Future/CompletableFuture.
Разбор внутри: I/O-задачи, CPU-задачи и настройка пула.
Разбор внутри: отказ, backpressure и защита приложения от перегруза.
Разбор внутри: bulkhead pattern и изоляция медленных задач.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 15Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 13