Configuring asynchronous processing in a Spring Boot REST application allows you to handle requests without blocking the main thread, improving performance and responsiveness. Here’s a step-by-step guide to enable and configure asynchronous processing in your Spring Boot REST application:
1. Enable Async Support
First, enable asynchronous processing by adding the @EnableAsync
annotation to your Spring Boot application class or any other configuration class.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
2. Configure an Async Executor
Define a ThreadPoolTaskExecutor
bean to handle async tasks. This executor can be configured to manage the number of threads, queue capacity, and other parameters.
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 executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
3. Use @Async Annotation
Use the @Async
annotation on methods that you want to execute asynchronously. The method should return a Future
, CompletableFuture
, or ListenableFuture
.
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
@Service
public class AsyncService {
@Async("taskExecutor")
public CompletableFuture<String> asyncMethod() {
// Simulate a long-running task
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.completedFuture("Async Result");
}
@Async("taskExecutor")
public Future<String> asyncMethodWithFuture() {
// Simulate a long-running task
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AsyncResult<>("Async Result with Future");
}
}
4. Call Async Methods
Call the async methods from your controller or service. Use the CompletableFuture
or Future
to get the result when needed.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@RestController
public class AsyncController {
@Autowired
private AsyncService asyncService;
@GetMapping("/async")
public String asyncEndpoint() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = asyncService.asyncMethod();
// Do some other processing if needed
return future.get(); // Blocking call, waits for the async method to complete
}
@GetMapping("/asyncWithFuture")
public String asyncWithFutureEndpoint() throws ExecutionException, InterruptedException {
Future<String> future = asyncService.asyncMethodWithFuture();
// Do some other processing if needed
return future.get(); // Blocking call, waits for the async method to complete
}
}
5. Exception Handling in Async Methods
Handle exceptions in async methods by using @Async
with exception handling capabilities.
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import java.util.concurrent.Executor;
@Configuration
public class AsyncExceptionConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return new ThreadPoolTaskExecutor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
// Handle exceptions thrown by async methods
System.out.println("Exception message: " + throwable.getMessage());
System.out.println("Method name: " + method.getName());
for (Object param : objects) {
System.out.println("Parameter value: " + param);
}
};
}
}
By following these steps, you can configure asynchronous processing in your Spring Boot REST application, improving its performance and responsiveness.
6. Write Test Class for AsyncController
Create a test class for AsyncController
. In this class, use MockMvc
to perform requests and validate responses. Additionally, you can use CompletableFuture
and Awaitility
to wait for the asynchronous processing to complete.
import org.awaitility.Awaitility;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.concurrent.TimeUnit;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(AsyncController.class)
public class AsyncControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AsyncService asyncService;
@BeforeEach
public void setUp() {
// Setup your mock behavior here
given(asyncService.asyncMethod()).willReturn(CompletableFuture.completedFuture("Async Result"));
given(asyncService.asyncMethodWithFuture()).willReturn(new AsyncResult<>("Async Result with Future"));
}
@Test
public void testAsyncEndpoint() throws Exception {
mockMvc.perform(get("/async"))
.andExpect(status().isOk())
.andExpect(content().string("Async Result"));
verify(asyncService).asyncMethod();
}
@Test
public void testAsyncWithFutureEndpoint() throws Exception {
mockMvc.perform(get("/asyncWithFuture"))
.andExpect(status().isOk())
.andExpect(content().string("Async Result with Future"));
verify(asyncService).asyncMethodWithFuture();
}
@Test
public void testAsyncMethodDelay() throws Exception {
// Simulate a delay in async method
given(asyncService.asyncMethod()).willReturn(CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Delayed Async Result";
}));
mockMvc.perform(get("/async"))
.andExpect(status().isOk())
.andExpect(content().string("Delayed Async Result"));
// Use Awaitility to wait for async processing to complete
Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> {
verify(asyncService).asyncMethod();
return true;
});
}
}
3. Add Awaitility Dependency
Add the Awaitility dependency to handle waiting for async operations.
For Maven:
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.0.3</version>
<scope>test</scope>
</dependency>
Explanation
@WebMvcTest(AsyncController.class)
: This annotation is used to test the web layer. It disables full auto-configuration and instead applies only configuration relevant to MVC tests.@MockBean
: This annotation is used to add mocks to the Spring ApplicationContext. Here, it mocks theAsyncService
bean.setUp()
: This method sets up the mock behavior before each test.testAsyncEndpoint()
andtestAsyncWithFutureEndpoint()
: These tests perform GET requests to the async endpoints and verify the responses.testAsyncMethodDelay()
: This test simulates a delay in the async method and usesAwaitility
to wait for the async processing to complete.
By following these steps, you can write tests for your async controllers in a Spring Boot application. This ensures that the asynchronous processing is correctly handled and verified in your application.