УРОК 14 / 30 0%

@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 — как работает
  1. Spring AOP создаёт прокси вокруг бина
  2. Когда вызывается @Async метод — прокси перехватывает
  3. Задача отправляется в TaskExecutor (пул потоков)
  4. Вызывающий поток немедленно возвращается

⚠️ Важно: вызов @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. 1
    config/AsyncConfig.java
    
    package 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. 2
    service/NotificationService.java
    
    package 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.CompletableFuture sendTaskCreatedWithResult(Task task) {
            sendTaskCreatedNotification(task);
            return java.util.concurrent.CompletableFuture.completedFuture("sent");
        }
    }
    
  3. 3
    Использование в TaskService
    
    public 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. 4
    Возврат CompletableFuture из Controller
    
    @GetMapping("/{id}/notify")
    public java.util.concurrent.CompletableFuture notify(@PathVariable Long id) {
        Task task = repo.findById(id).orElseThrow();
        return notificationService.sendTaskCreatedWithResult(task);
    }
    
  5. 5
    Логирование (видно имя потока)
    
    # Синхронно
    http-nio-8080-exec-1    Sending notification for task id=42
    # Асинхронно
    taskflow-async-1        Sending notification for task id=42
    

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

UX: уведомления, отправка email, логирование — всё что НЕ критично для ответа — асинхронно.
Устойчивость: если email-сервис упал — наш API всё равно работает (задача ушла в очередь, отправка ретраится).
Bulkhead: @Async изолирует медленные операции от критического path.
Thread name в логахtaskflow-async-1 легко грепать, видно где задача выполняется.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Почему @Async не срабатывает при вызове this.method()?

Разбор внутри: AOP-прокси и вызов метода через Spring bean.

2
Что происходит с исключениями внутри @Async void метода?

Разбор внутри: потерянные ошибки, AsyncUncaughtExceptionHandler и Future/CompletableFuture.

3
ThreadPoolTaskExecutor vs ForkJoinPool — что выбрать?

Разбор внутри: I/O-задачи, CPU-задачи и настройка пула.

4
AbortPolicy vs CallerRunsPolicy — как вести себя при переполнении очереди?

Разбор внутри: отказ, backpressure и защита приложения от перегруза.

5
Зачем разные executors для разных @Async методов?

Разбор внутри: bulkhead pattern и изоляция медленных задач.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 15

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 13
🎉
Новый тир
Новый тир достигнут!