Executing background tasks using @Async annotation in Spring Boot
When developing an application we will encounter scenarios where some operations require more time to finish.
For example, consider the scenario where an application provides an option for its users to download all their data. When a download request is received, the application will start to collect all the data of the user. Then make a compressed file of this data and send it to the user. This will take a minimum of 5 minutes to complete.
If this operation is executing in the main thread of the request, then we will be blocked until the task is finished. So, How can we solve this? This long-running task needs to be moved to another thread.
The Spring boot framework provides a feature to execute tasks in a background thread using the @Async annotation. Let's see how to execute background tasks in Spring Boot using the @Async annotation.
I've created a sample project named asyncDemo using the spring initializer with the following structure
GitHub Link: spring-boot-async-demo
The REST Endpoints
/execute:- This will simulate a time-consuming task by introducing a 3-minute delay and writing a random number to a test database. Returns a taskId
/fetch/{taskId}:- retrieves the generated random number
Steps to make a method asynchronous
Create a thread pool executor configuration
Enable asynchronous method execution capability using @EnableAsync annotation
Annotate the required method with @Async
Add the following items to the project
Rest controller class
Entity class
Service class
Repository class
AsyncController.java (Rest API Controller)
package com.gintophilip.asyncDemo.controller;
import com.gintophilip.asyncDemo.entities.RandomUUID;
import com.gintophilip.asyncDemo.service.RandomUUIDService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import java.util.Random;
@RestController
@RequestMapping("/api/")
public class AsyncController {
@Autowired
RandomUUIDService userService;
@PostMapping("/execute")
ResponseEntity<Long> timeConsumingTask(){
Long taskId = new Random().nextLong(900) + 100;
userService.timeConsumingTask(taskId);
return ResponseEntity.ok(taskId);
}
@GetMapping("/fetch/{taskId}")
ResponseEntity<Optional<RandomUUID>> fetchResult(@PathVariable Long taskId){
return ResponseEntity.ok(userService.fetchResult(taskId));
}
}
RandomUUIDService.java (Service)
package com.gintophilip.asyncDemo.service;
import com.gintophilip.asyncDemo.entities.RandomUUID;
import com.gintophilip.asyncDemo.repositories.RandomUUIDRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
@Service
public class RandomUUIDService {
@Autowired
RandomUUIDRepository randomUUIDRepository;
Logger logger = LoggerFactory.getLogger(RandomUUIDService.class);
public void timeConsumingTask(Long taskId) {
try {
String id = String.valueOf(UUID.randomUUID());
Thread.sleep(1000);
RandomUUID uuid = new RandomUUID();
uuid.setId(taskId);
uuid.setUUID(id);
randomUUIDRepository.save(uuid);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public Optional<RandomUUID> fetchResult(Long taskId) {
return randomUUIDRepository.findById(taskId);
}
}
RandomUUID.java (Entity)
package com.gintophilip.asyncDemo.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class RandomUUID {
@Id
private Long id;
private String UUID;
public RandomUUID() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUUID() {
return UUID;
}
public void setUUID(String UUID) {
this.UUID = UUID;
}
}
RandomUUIDRepository.java (Repository)
package com.gintophilip.asyncDemo.repositories;
import com.gintophilip.asyncDemo.entities.RandomUUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RandomUUIDRepository extends JpaRepository<RandomUUID,Long> {
}
Now run the application and call the /execute endpoint. You will see that to receive the response it took 3 minutes.
Let's solve this problem by using the @Async
annotation
Adding asynchronous method execution capability
Add @EnableAsync
at AsyncDemoApplication class
@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncDemoApplication.class, args);
}
}
Create the thread pool executor configuration
ThreadPoolConfig.java
Let's give an initial thread pool size of 5 and the name for the thread as async_thread
package com.gintophilip.asyncDemo.configurations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class ThreadPoolConfig {
@Bean("asyncExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(5);
executor.setThreadNamePrefix("async_thread");
executor.initialize();
return executor;
}
}
Configure the method to run as an asynchronous task
Annotate the method timeConsumingTask inside the RandomUUIDService.java class with @Async("asyncExecutor")
We can specify which task executor to use by including the bean name if we have multiple task executor configurations. Here we use the "asyncExecutor" bean.
@Async("asyncExecutor")
public void timeConsumingTask(Long taskId) {
logger.info("Thread name :{}",Thread.currentThread().getName());
try {
String id = String.valueOf(UUID.randomUUID());
//simulate task by sleeping for 3 minute
Thread.sleep(180000);
RandomUUID uuid = new RandomUUID();
uuid.setId(taskId);
uuid.setUUID(id);
randomUUIDRepository.save(uuid);
logger.info("task completed");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
We also added a log message to show the thread name of the running async task
The async method only supports Future and void as return type
Now run the application and call the /execute endpoint. You will notice that it returned a task ID as a response.
After returning the response the timeConsumingTask method will execute on the thread named async_thread
Check the log. You will see the thread name we logged from the timeConsumingTask method
[ async_thread1] c.g.asyncDemo.service.RandomUUIDService : Thread name :async_thread1
[ async_thread1] c.g.asyncDemo.service.RandomUUIDService : task completed
After the completion of the task, we can get the result by calling /fetch/{taskId}