Это сообщение переведено AI.
Выбрать язык
Текст, резюмированный ИИ durumis
- В этой статье рассматривается, как реализовать асинхронную обработку Java с помощью Spring @Async, а также ее преимущества и нюансы.
- Используя Spring @Async, вы можете легко реализовать асинхронную обработку, добавив аннотацию @EnableAsync и добавив аннотацию @Async к методам, которые вы хотите сделать асинхронными.
- Настройка пула потоков позволяет эффективно управлять потоками, а в зависимости от типа возвращаемого значения можно использовать Future, ListenableFuture, CompletableFuture и т. д. для возврата результатов асинхронной обработки.
Обработка Java в асинхронном режиме
Прежде чем изучать Spring @Async, необходимо понять концепции синхронности, асинхронности и многопоточности. Предполагая, что вы знакомы с этими концепциями, давайте рассмотрим, как выполнить асинхронную обработку в чистом Java. Если вы знакомы с потоками Java, вы можете пропустить эту главу.
public class MessageService {
public void print(String message) {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Если вы хотите синхронно реализовать функцию, которая получает сообщение и просто выводит его, вы можете написать код, как показано выше. Изменив этот код на многопоточный асинхронный вариант, вы получите следующий код:
public class MessageService {
public void print(String message) {
new Thread(() -> System.out.println(message))
.start();
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Однако этот метод очень опасен, так как вы не можете управлять потоками. Например, если одновременно выполняется 10 000 вызовов, вам нужно создать 10 000 потоков за очень короткое время. Стоимость создания потока не маленькая, поэтому это негативно сказывается на производительности программы, и даже может привести к ошибке OOM. Поэтому, для управления потоками необходимо реализовать пул потоков, и Java предоставляет для этого класс ExecutorService.
public class MessageService {
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
public void print(String message) {
executorService.submit(() -> System.out.println(message));
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Мы ограничили общее количество потоков 10 и правильно реализовали многопоточную асинхронную обработку. Однако, в этом методе, вам нужно использовать метод submit() ExecutorService для каждого метода, который вы хотите обработать асинхронно, что означает, что вам нужно выполнить повторяющуюся корректировку. То есть, если вы хотите изменить метод, написанный изначально синхронно, на асинхронный, вам неизбежно придется изменять сам код метода.
Spring @Async
Простой метод
@EnableAsync
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@Service
public class MessageService {
@Async
public void print(String message) {
System.out.println(message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() {
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Просто добавьте аннотацию @EnableAsync в класс Application и аннотацию @Async в метод, который вы хотите обработать асинхронно. Однако, этот метод не позволяет управлять потоками. Поскольку @Async по умолчанию использует SimpleAsyncTaskExecutor, который не является пулом потоков, а просто создает потоки.
Использование пула потоков
Сначала удалите @EnableAsync из класса Application. Если в классе Application есть настройки @EnableAutoConfiguration или @SpringBootApplication, то во время выполнения будет считываться информация о bean threadPoolTaskExecutor (которую мы создадим позже) из класса SpringAsyncConfig, в котором настроена @Configuration.
@Configuration
@EnableAsync // Должно быть в классе Async-конфигурации, а не в Application.
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // Количество основных потоков
taskExecutor.setMaxPoolSize(30); // Максимальное количество потоков
taskExecutor.setQueueCapacity(100); // Размер очереди
taskExecutor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
Вы можете настроить размер ядра и максимальный размер. В этом случае, вы можете предположить, что он работает с размером ядра, а затем, если он не может обрабатывать больше работы, размер потока увеличится до максимального размера, но это не так.
Внутренне он создает LinkedBlockingQueue размером Integer.MAX_VALUE, и если задача не может быть обработана 3 потоками ядра, то она будет ждать в очереди. Когда очередь заполнится, то для обработки задач будут создаваться потоки до максимального размера.
Если вы не хотите, чтобы размер очереди был Integer.MAX_VALUE, вы можете настроить queueCapacity. Настроив его как показано выше, сначала будет обрабатываться 3 потока ядра, если скорость обработки будет снижаться, задачи будут помещаться в очередь размером 100, и если поступает больше запросов, будет создаваться до 30 потоков для обработки задач.
Настройка пула потоков завершена, теперь вам нужно просто добавить имя bean к методу с аннотацией @Async.
@Service
public class MessageService {
@Async("threadPoolTaskExecutor")
public void print(String message) {
System.out.println(message);
}
Если вы хотите настроить несколько типов пулов потоков, вы можете создать несколько методов создания bean, таких как threadPoolTaskExecutor(), и указать нужный bean в настройках @Async.
Типы возвращаемых значений
Без возвращаемого значения
Это случай, когда метод, который нужно обработать асинхронно, не должен возвращать никаких результатов. В этом случае, в качестве типа возвращаемого значения @Async, вы должны использовать void.
С возвращаемым значением
В качестве типа возвращаемого значения можно использовать типы Future, ListenableFuture, CompletableFuture. Чтобы вернуть результат асинхронного метода, нужно обернуть его в new AsyncResult().
[Future]
@Service
public class MessageService {
@Async
public Future print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws ExecutionException, InterruptedException {
for (int i = 1; i <= 5; i++) {
Future future = messageService.print(i + "");
System.out.println(future.get());
}
}
}
// Результат выполнения
Task Start - 1
jayon-1
Task Start - 2
jayon-2
Task Start - 3
jayon-3
Task Start - 4
jayon-4
Task Start - 5
future.get() блокирует и ждет, пока не придет результат запроса. Поэтому это асинхронный блокирующий метод, который не дает хорошей производительности. Обычно Future не используется.
[ListenableFuture]
@Service
public class MessageService {
@Async
public ListenableFuture print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws InterruptedException {
for (int i = 1; i <= 5; i++) {
ListenableFuture listenableFuture = messageService.print(i + "");
listenableFuture.addCallback(System.out::println, error -> System.out.println(error.getMessage()));
}
}
}
// Результат выполнения
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
ListenableFuture позволяет обрабатывать задачи в неблокирующем режиме с помощью обратного вызова. Первый параметр метода addCallback() — это метод обратного вызова при успешном завершении задачи, второй параметр — метод обратного вызова при ошибке. Обратите внимание, что поскольку размер потока ядра установлен в 3, вы можете видеть, что сообщение "Task Start" выводится 3 раза в начале.
[CompletableFuture]
ListenableFuture позволяет реализовать неблокирующую логику, но если вам нужно встроить обратный вызов в обратный вызов, то это может привести к очень сложному коду, который называется "ад обратных вызовов".
Конечно, в этот раз мы не будем подробно рассматривать CompletableFuture, поэтому, если вам интересно, как справиться со сложными обратными вызовами, обратитесь к ссылке, указанной ниже.
@Service
public class MessageService {
@Async
public CompletableFuture print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message).completable();
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws InterruptedException {
for (int i = 1; i <= 5; i++) {
CompletableFuture completableFuture = messageService.print(i + "");
completableFuture
.thenAccept(System.out::println)
.exceptionally(error -> {
System.out.println(error.getMessage());
return null;
});
}
}
Читаемость лучше, чем определение обратного вызова для ListenableFuture, и он полностью реализует функции неблокирования. Поэтому, если вам нужно вернуть значение при использовании @Async, рекомендуется использовать CompletableFuture.
Преимущества @Async
Разработчику нужно просто добавить аннотацию @Async в метод, если он хочет использовать асинхронный метод, написанный синхронно. Это позволяет создавать код с хорошим обслуживанием синхронного и асинхронного режима.
Что нужно помнить при использовании @Async
Для использования функции @Async необходимо объявить аннотацию @EnableAsync, но если не использовать никаких дополнительных настроек, она будет работать в режиме прокси. Это означает, что все асинхронные методы, которые используют аннотацию @Async, должны соблюдать все ограничения Spring AOP. Подробнее см. в этом сообщении.
- Если добавить @Async к частному методу, то AOP работать не будет.
- AOP не будет работать при вызове методов из одного и того же объекта.