![translation](https://cdn.durumis.com/common/trans.png)
This is an AI translated post.
[Spring] How to Use @Async
- Writing language: Korean
- •
-
Base country: All countries
- •
- Information Technology
Select Language
Summarized by durumis AI
- This document describes how to implement Java asynchronous processing using Spring @Async, its advantages, and precautions.
- Spring @Async allows you to easily implement asynchronous processing by simply adding the @EnableAsync annotation and attaching the @Async annotation to the methods you want to process asynchronously.
- Efficient thread management is possible through thread pool settings, and you can return asynchronous processing results using Future, ListenableFuture, and CompletableFuture, depending on the return type.
Java Asynchronous Processing
Before we look at Spring @Async, it is essential to understand the concepts of synchronous, asynchronous, and multi-threaded. Assuming you are familiar with these concepts, let's take a look at pure Java asynchronous processing methods in code. If you are familiar with Java threads, you can skip this chapter.
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 + "");
}
}
If you create a function that simply receives and prints a message synchronously, you can write code as shown above. If you change this to a multi-threading asynchronous method, you can write the source code as follows.
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 + "");
}
}
However, this method is very risky because you cannot manage the threads. For example, if 10,000 calls are made at the same time, you need to create 10,000 threads in a very short time. The cost of creating threads is not small, so it can negatively affect the performance of the program, and it can even cause an OOM error. Therefore, you need to implement a Thread Pool to manage the threads, and Java provides the ExecutorService class.
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 + "");
}
}
The total number of threads is limited to 10, and we are now able to correctly perform the asynchronous processing in the multi-threading method that we want. However, with this method, you have to apply the ExecutorService's submit() method to each method you want to process asynchronously, so you have to do repetitive modification work. In other words, if you want to change a method originally written in synchronous logic to asynchronous, you will inevitably have to change the logic of the method itself.
Spring @Async
Simple Method
@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 + "");
}
}
Just attach the @EnableAsync annotation to the Application class and attach the @Async annotation to the method of the synchronous logic you want to process asynchronously. However, this method has the problem of not managing threads. This is because the default setting of @Async is to use SimpleAsyncTaskExecutor, which is not a thread pool, but simply creates threads.
Using Thread Pool
First, remove @EnableAsync from the Application class. If the Application class has @EnableAutoConfiguration or @SpringBootApplication settings, it reads the threadPoolTaskExecutor bean information of the SpringAsyncConfig class (which will be created below) at runtime because the @Configuration is set.
@Configuration
@EnableAsync // Should be attached to Async configuration class, not Application.
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // Default thread count
taskExecutor.setMaxPoolSize(30); // Maximum thread count
taskExecutor.setQueueCapacity(100); // Queue size
taskExecutor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
You can set the core and max sizes. At this time, you might expect that it will operate with the initial core size and then increase the number of threads to the max size if it cannot handle more tasks, but that is not the case.
Internally, it creates a LinkedBlockingQueue of Integer.MAX_VALUE size and waits in the Queue if it cannot handle tasks with the core size of threads. When the Queue is full, it creates threads up to the max size to handle the tasks.
If you find it burdensome to set the Queue size to Integer.MAX_VALUE, you can set queueCapacity. If you set it as above, it will initially process with 3 threads, and if the processing speed slows down, it will put tasks into a 100-size Queue, and if more requests come in, it will create up to 30 threads to handle the tasks.
Once the thread pool is set up, you can attach the name of the bean to the method with the @Async annotation.
@Service
public class MessageService {
@Async("threadPoolTaskExecutor")
public void print(String message) {
System.out.println(message);
}
If you want to set up multiple thread pool types, create multiple bean creation methods like threadPoolTaskExecutor() and put the thread pool bean you want when setting @Async.
Return Types by Return Type
When there is no return value
This is the case where the method that needs to be processed asynchronously does not need to return a result. In this case, you can set the return type of the @Async annotation to void.
When there is a return value
You can use Future, ListenableFuture, CompletableFuture types as the return type. The return type of the asynchronous method can be wrapped in 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());
}
}
}
// Execution Result
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() acts as a blocking function that waits until the request result arrives. Therefore, it becomes an asynchronous blocking method and its performance is not good. Future is usually not used.
[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()));
}
}
}
// Execution Result
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
ListenableFuture allows you to process tasks in a non-blocking manner through callbacks. You can define the first parameter of the addCallback() method as the task completion callback method and the second parameter as the task failure callback method. By the way, since the core thread of the thread pool is set to 3, you can see that the "Task Start" message is printed 3 times at the beginning.
[CompletableFuture]
Even with ListenableFuture alone, you can implement non-blocking logic, but if callbacks are needed inside callbacks, it can lead to very complicated code called callback hell.
Of course, since we are not going to cover CompletableFuture in detail this time, if you are curious about the code that handles complex callbacks, please refer to the link in the source below.
@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;
});
}
}
It is more readable than defining callbacks for ListenableFuture, and it fully performs the non-blocking function. Therefore, it is recommended to use CompletableFuture when using @Async and a return value is required.
Advantages of @Async
Developers can simply attach the @Async annotation to the method if they want asynchronous behavior, even though they wrote the method synchronously. This allows you to create code that is well-maintained for synchronous and asynchronous operations.
Cautions of @Async
To use the @Async function, you need to declare the @EnableAsync annotation. If you don't make any separate settings, it works in proxy mode. That is, all asynchronous methods that work with the @Async annotation follow the restrictions of Spring AOP. For more details, please refer to this post.
- Even if you attach @Async to a private method, AOP will not work.
- AOP does not work when methods within the same object call each other.