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 theAsyncServicebean.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 usesAwaitilityto 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.