Spring boot async rest controller with Callable interface

Learn to write spring boot async rest controller which support async request processing and returning the response using Callable interface.

1. spring boot async controller

Writing a controller and having it handle the request asynchronously is as simple as changing the return type of the controller’s handler method.

Given controller is simplest example and return the Callable<String> instead of normal string value "Hello World !!" after 5 seconds delay while the actual logic inside controller is accomplished.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;

@RestController
public class HelloWorldCallableController 
{
      @GetMapping(value = "/testCallable")
      public Callable<String> echoHelloWorld() 
      {
            return () -> 
            {
                  Thread.sleep(ThreadLocalRandom.current().nextInt(5000));
                  
                  return "Hello World !!";
            };
      }
}

2. How to test async rest controller

To test the above controller, I am using mockito shipped with spring boot distribution.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
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.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;

import com.howtodoinjava.springasyncexample.web.controller.HelloWorldCallableController;

@RunWith(SpringRunner.class)
@WebMvcTest(HelloWorldCallableController.class)
public class HelloWorldCallableControllerTest {

      @Autowired
      private MockMvc mockMvc;

      @Test
      public void testHelloWorldController() throws Exception 
      {
            MvcResult mvcResult = mockMvc.perform(get("/testCallable"))
                                                .andExpect(request().asyncStarted())
                                                .andDo(MockMvcResultHandlers.log())
                                                .andReturn();
            
            mockMvc.perform(asyncDispatch(mvcResult)).andExpect(status().isOk())
                              .andExpect(content().contentTypeCompatibleWith("text/plain"))
                              .andExpect(content().string("Hello World !!"));
      }
}

Program output.

2019-02-06 12:13:19.356 DEBUG 15384 --- [main] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'helloWorldCallableController'
2019-02-06 12:13:19.357 DEBUG 15384 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Last-Modified value for [/testCallable] is: -1
2019-02-06 12:13:19.409 DEBUG 15384 --- [main] o.s.w.c.request.async.WebAsyncManager    : Concurrent handling starting for GET [/testCallable]
2019-02-06 12:13:19.416 DEBUG 15384 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Leaving response open for concurrent processing
2019-02-06 12:13:19.417 DEBUG 15384 --- [main] o.s.b.w.s.f.OrderedRequestContextFilter  : Cleared thread-bound request context: org.springframework.mock.web.MockHttpServletRequest@3a627c80
2019-02-06 12:13:19.463 DEBUG 15384 --- [main] o.s.test.web.servlet.result              : MvcResult details:
...
2019-02-06 12:13:24.836 DEBUG 15384 --- [mvc-task-1] o.s.w.c.request.async.WebAsyncManager    : Concurrent result value [Hello World !!] - dispatching request to resume processing
2019-02-06 12:13:24.929 DEBUG 15384 --- [main] o.s.b.w.s.f.OrderedRequestContextFilter  : Bound request context to thread: org.springframework.mock.web.MockHttpServletRequest@3a627c80
2019-02-06 12:13:24.929 DEBUG 15384 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : DispatcherServlet with name '' resumed processing GET request for [/testCallable]
2019-02-06 12:13:24.930 DEBUG 15384 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Looking up handler method for path /testCallable
2019-02-06 12:13:24.930 DEBUG 15384 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Returning handler method [public java.util.concurrent.Callable<java.lang.String> com.howtodoinjava.springasyncexample.web.controller.HelloWorldCallableController.echoHelloWorld()]
2019-02-06 12:13:24.930 DEBUG 15384 --- [main] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'helloWorldCallableController'
2019-02-06 12:13:24.930 DEBUG 15384 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Last-Modified value for [/testCallable] is: -1
2019-02-06 12:13:24.931 DEBUG 15384 --- [main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Found concurrent result value [Hello World !!]
2019-02-06 12:13:24.955 DEBUG 15384 --- [main] m.m.a.RequestResponseBodyMethodProcessor : Written [Hello World !!] as "text/plain" using [org.springframework.http.converter.StringHttpMessageConverter@2bffa76d]
2019-02-06 12:13:24.956 DEBUG 15384 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Null ModelAndView returned to DispatcherServlet with name '': assuming HandlerAdapter completed request handling
2019-02-06 12:13:24.956 DEBUG 15384 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Successfully completed request
  • Notice that request handling is started on a certain thread (main).
  • Another thread is doing the processing and returning the result (mvc-task-1).
  • Finally, the request is dispatched to spring again to handle the result and shutting down the application (main and Thread-2).

3. Async configuration options

To override the default async behavior such as thread pool and timeout, you can implement the WebMvcConfigurer interface and override it’s configureAsyncSupport() method.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class SpringAsyncExampleApplication implements WebMvcConfigurer {

      public static void main(String[] args) {
            SpringApplication.run(SpringAsyncExampleApplication.class, args);
      }

      @Override
      public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            configurer.setTaskExecutor(mvcTaskExecutor());
            configurer.setDefaultTimeout(30_000);
      }

      @Bean
      public ThreadPoolTaskExecutor mvcTaskExecutor() {
            ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
            taskExecutor.setThreadNamePrefix("mvc-task-");
            return taskExecutor;
      }
}

4. Sourcecode files

4.1. application.properties

Enable debug logging here to understand the behavior of the application.

logging.level.org.springframework=DEBUG
logging.level.com.howtodoinjava=DEBUG

4.2. pom.xml

The used pom.xml is:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      
      <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.2.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
      </parent>
      
      <groupId>com.howtodoinjava.demo</groupId>
      <artifactId>spring-async-demo</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <name>spring-async-demo</name>
      <description>Demo project for Spring Boot</description>

      <properties>
            <java.version>1.8</java.version>
      </properties>

      <dependencies>
      
            <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            
            <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-webflux</artifactId>
            </dependency>

            <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
            </dependency>
            <dependency>
                  <groupId>io.projectreactor</groupId>
                  <artifactId>reactor-test</artifactId>
                  <scope>test</scope>
            </dependency>
      </dependencies>

      <build>
            <plugins>
                  <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                  </plugin>
            </plugins>
      </build>
      
      <repositories>
        <repository>
            <id>repository.spring.release</id>
            <name>Spring GA Repository</name>
            <url>http://repo.spring.io/release</url>
        </repository>
    </repositories>

</project>

Let me know if you face any error while executing this asynchronous rest service example.

Happy Learning !!

Was this post helpful?

Join 7000+ Fellow Programmers

Subscribe to get new post notifications, industry updates, best practices, and much more. Directly into your inbox, for free.

Leave a Comment

HowToDoInJava

A blog about Java and its related technologies, the best practices, algorithms, interview questions, scripting languages, and Python.