Asynchronous processing is supported since Servlet 3.0. Spring MVC makes asynchronous processing even simpler. Here is a hello world level demo on how to do that using Spring MVC.
Basically there are 2 things need to do:
- configure web.xml to turn on async support
- return Callable instance instead of what you mostly return, String/Model/ModelAndView etc, from a controller’s method.
Let’s suppose you know how to setup a maven managed Spring MVC project in Eclipse. If you don’t , here is the tutorial
0. What you need
JDK 1.7+
Maven 3.2.1
1. Maven pom.xml
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shengwang.demo</groupId>
<artifactId>spring-mvc-asynchronous-basic</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>spring-mvc-asynchronous-basic Maven Webapp</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-framework.version>4.1.6.RELEASE</spring-framework.version>
<!-- Logging -->
<logback.version>1.1.2</logback.version>
<slf4j.version>1.7.7</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>spring-mvc-asynchronous-basic</finalName>
<!-- Use Java 1.7 -->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
In maven pom.xml, dependencies like Spring MVC and logging has been added. Also JDK version is set to 1.7
2. configure web.xml to support async
Use <async-supported>true</async-supported> to turn on async support for servlets and filters. Use <dispatcher>ASYNC</dispatcher> for filter-mapping if any.
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Demo for mvc hello world config</display-name>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<!-- turn on async support for servlet -->
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
3. Define java class
First is the service class, just sleep 3 seconds to mimic a slow task.
package com.shengwang.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
Logger logger = LoggerFactory.getLogger(HelloService.class);
public String doSlowWork() {
logger.info("Start slow work");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("finish slow work");
// return "forward:/another"; // forward to another url
return "index"; // return view's name
}
}
The hello service does nothing except sleep a few seconds then return view's name.
The controller look like below.
package com.shengwang.demo;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
Logger logger = LoggerFactory.getLogger(HelloController.class);
@Autowired
HelloService helloService;
@RequestMapping("/helloAsync")
public Callable<String> sayHelloAsync() {
logger.info("Entering controller");
Callable<String> asyncTask = new Callable<String>() {
@Override
public String call() throws Exception {
return helloService.doSlowWork();
}
};
logger.info("Leaving controller");
return asyncTask;
}
}
The method sayHelloAsync return a Callable instance.
4. Configure Spring MVC
Compare to the basic hello world Spring mvc configuration in tutorial on “How to setup Spring MVC project with maven in Eclipse”, the difference is the task executor setup for async tasks.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.shengwang.demo" />
<!-- ================================== -->
<!-- 0. Set up task executor for async -->
<!-- ================================== -->
<mvc:annotation-driven>
<mvc:async-support default-timeout="30000" task-executor="taskExecutor"/>
</mvc:annotation-driven>
<!-- modify the parameters of thread pool -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="50"/>
<property name="queueCapacity" value="10"/>
<property name="keepAliveSeconds" value="120"/>
</bean>
<!-- ================================== -->
<!-- 1. mapping static resources -->
<!-- ================================== -->
<mvc:resources location="/static-resources/css/" mapping="/css/**" cache-period="3600"/>
<mvc:resources location="/static-resources/img/" mapping="/img/**" cache-period="3600"/>
<mvc:resources location="/static-resources/js/" mapping="/js/**" cache-period="3600"/>
<!-- ================================== -->
<!-- 2. view resolver for JSP -->
<!-- ================================== -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
In real project, the task executor must be explicitly setup instead of using the default SimpleAsyncTaskExecutor, which can not be used in production. As Spring document says:
It is highly recommended to configure this property since by default Spring MVC uses SimpleAsyncTaskExecutor.
5. Run the async web app
Run the project and access http://localhost:8080/spring-mvc-asynchronous-basic/helloAsync, the log from console looks like below
The controller thread(http-bio-8080-exec-7) exits immediately, but another MVC mangaged thread(taskExecutor-1) takes 3 seconds to finish the slow work. Although the controller thread finishes very fast, the connection keeps open, browser doesn’t get response until 3 seconds later, all threads finish.
Hi Thanks for this demo.... but after reaching the controller it's giving me directly the index page without performing the doslowwork() method part. Please help
ReplyDeleteHi there, I can't point out exactly but sounds like async doesn't work. First make sure you have correctly reach the right method in your controller, then maybe you should check if you have turn async support on in web.xml and make sure you do reply a callable for your controller method.
DeleteI haven't use maven in the application. the async support turns on. controller is working but async part is not. If possible could i share the application with you.
DeleteHi,
ReplyDeleteVery much appreciated of your detailed explanation.
I would like to use spring mvc async support as described by you, but i would like to have event notification service from the controller response, which keeps sending out events. Can you please let me know how can this be accomplished, as the async support discussed above will return a single response asynchronously but it does not send multiple outputs in intervals.
Thanks
Naresh
The controller can only response to the user(browser) ONCE. Async Servlet is designed to improve server's performance by reduce thread number in large concurrent environment. For your requirement, sending out multi-times from controller, use websocket or long-poll instead.
DeleteHi Sheng, How we can download your source code (this project)?
ReplyDeleteThanks,
Sayali
Sheng, Thanks a lot for your article. I followed your instruction. That's how I started getting our application to switch to Spring MVC async using Callable. The following articles are also useful.
ReplyDeletehttps://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-async (Which give an overall picture and If you use java filter and Spring MVC interceptor), http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/AsyncHandlerInterceptor.html (Useful for MVC interceptors too).
Our async application is deployed in Tomcat. That works fine. However, I just couldn't get one test passed. Our tests use embedded Jetty 8 server. I traced and found.out that Spring MVC will return 404 Not Found "No mapping request found for HTTP request URI....". if the async web request execution time is longer than 100ms, Very similar requests with execution time < 100ms works fine in embedded Jetty 8. Similar requests with async web request taking longer than 1 second (due to remote Mongodb) work fine in Tomcat. I configured default-timeout in mvc:async-support. It does not look like it has something to do with this issue.
According to timeout section in the article
https://spring.io/blog/2012/05/10/spring-mvc-3-2-preview-making-a-controller-method-asynchronous/,
"You can configure the timeout value (of async web request) through the MVC Java config and the MVC namespace, or directly on the RequestMappingHandlerAdapter. If not configured, the timeout value will depend on the underlying Servlet container. On Tomcat it is 10 seconds and it starts after the initial Servlet container thread exits all the way out."
That seems to match what I experienced. It did not elaborate how to do that. Let me know if you know any solution. Currently, I am research on
spring.mvc.async.request-timeout (preferred) and also if I can override embedded Jetty server servlet (async) request timeout
Hi Sheng. Could you give me an example using this project with MDC Logger? I'm trying to use Spring MVC Rest Async but when I try to use MDC but I've lost the reference and the MDC logger doesn't work. Could you help me, please?
ReplyDeleteThanks!
Of course, MDC is just for Request Context (In other word, ThreadLocal of Request)
DeleteWhen you call MDC in other thread -> Difference from ThreadLocal.
So, you must create other map to store you MDC and then pass it down the pipeline.
Ex:
Map ctx = MDC. getCopyOfContextMap();
Callable x = new Callable(){
public Object call(){
String myId = ctx.get("your-key");
log.("{}", myId);
return ;
}
};
browser doesn’t get response until 3 seconds later, all threads finish. can browser get
ReplyDeleteresponse when main thread finish,if some thread is time-consuming,the custormer will wait for a long time
Spring sync mvc is not design for this actually, it is for optimization on server side. For browser<->server interaction, the big picture is still the same(i.e blocking).
Deletethis is not working for me
ReplyDelete