理解Spring MVC中的异步处理请求(上)
理解Spring MVC中的异步处理请求(上)
运行环境声明
- Java SE 8
- Tomcat 8.5.5(Servlet 3.1)
- Spring Framework 4.3.3.RELEASE
Spring MVC的两种异步处理方式
1.异步处理结束后才开始生成HTTP响应
这种方式是把耗时逻辑任务的执行与服务器的管理线程相分离,从而实现多线程的并行。因为HTTP响应在异步处理结束之后才生成,因此从客户端看来与同步处理无异。
2.在异步处理时已经开始生成HTTP响应
通过这种方式,可以在异步处理的任意时刻向客户端发送信息。显然,这种方式是基于HTTP/1.1的分块传输编码(Chunked transfer encoding),因此客户端必须支持分块传输编码。
本文将只说明第1种异步处理方式,第2种方式将放在下篇讲述。
Spring MVC的同步处理代码示例
package com.example.component;
import java.time.LocalDateTime;
public class Console {
public static void println(Object target) {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread() + ": " + target);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
package com.example.component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Controller
@RequestMapping("/standard")
public class StandardController {
@RequestMapping(method = RequestMethod.GET)
public String get(@RequestParam(defaultValue = "0") long waitSec, Model model) {
Console.println("Start get.");
model.addAttribute("acceptedTime", LocalDateTime.now());
heavyProcessing(waitSec, model);
Console.println("End get.");
return "complete";
}
private void heavyProcessing(long waitSec, Model model) {
if (waitSec == 999) {
throw new IllegalStateException("Special parameter for confirm error.");
}
try {
TimeUnit.SECONDS.sleep(waitSec);
} catch (InterruptedException e) {
Thread.interrupted();
}
model.addAttribute("completedTime", LocalDateTime.now());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
complete.jsp文件放在Spring MVC默认的位置——/WEB-INF中。
<% //src/main/webapp/WEB-INF/complete.jsp %>
<%@ page import="com.example.component.Console" %>
<% Console.println("Called complete.jsp"); %>
<% Console.println(request.getDispatcherType()); %>
<html>
<body>
<h2>Processing is complete !</h2>
<p>Accept timestamp is ${acceptedTime}</p>
<p>Complete timestamp is ${completedTime}</p>
<p><a href="${pageContext.request.contextPath}/">Go to Top</a></p>
</body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
因为Handler只返回一个逻辑视图名称,需要ViewResolver把该逻辑视图名称解析为真正的视图View对象。
package com.example.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@EnableWebMvc // 如果使用Spring Boot,不能加上这句,否则将导致AutoConfigure失效
@ComponentScan("com.example.component")
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp(); // 启用ViewResolver
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
使用CURL或者浏览器访问,将得到由complete.jsp生成的HTML代码。
$ curl -D - http://localhost:8080/standard?waitSec=1
- 1
HTTP/1.1 200
Set-Cookie: JSESSIONID=469B8E011EAE404434D889F2E20B1CFA;path=/;HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Language: ja-JP
Content-Length: 204
Date: Tue, 04 Oct 2016 15:22:48 GMT
<html>
<body>
<h2>Processing is complete !</h2>
<p>Accept timestamp is 2016-10-05T00:22:46.929</p>
<p>Complete timestamp is 2016-10-05T00:22:47.933</p>
<p><a href="/">Go to Top</a></p>
</body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
服务器控制台的输出信息:
2016-10-05T00:22:46.929 Thread[http-nio-8080-exec-1,5,main]: Start get.
2016-10-05T00:22:47.933 Thread[http-nio-8080-exec-1,5,main]: End get.
2016-10-05T00:22:48.579 Thread[http-nio-8080-exec-1,5,main]: Called complete.jsp
2016-10-05T00:22:48.579 Thread[http-nio-8080-exec-1,5,main]: FORWARD
- 1
- 2
- 3
- 4
Spring MVC的异步处理代码示例
首先使用最普通的方法 java.util.concurrent.Callable
实现多线程。
package com.example.component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.time.LocalDateTime;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@Controller
@RequestMapping("/async")
public class AsyncController {
@RequestMapping(method = RequestMethod.GET)
public Callable<String> get(@RequestParam(defaultValue = "0") long waitSec, Model model) {
Console.println("Start get.");
model.addAttribute("acceptedTime", LocalDateTime.now());
// 在Callable的call方法内实现异步处理逻辑
// 因为Callable是函数式接口,因此可以与Java8的lambda表达式隐式转换
Callable<String> asyncProcessing = () -> {
Console.println("Start Async processing.");
heavyProcessing(waitSec, model);
Console.println("End Async processing.");
return "complete";
};
Console.println("End get.");
return asyncProcessing;
}
private void heavyProcessing(long waitSec, Model model) {
if (waitSec == 999) {
throw new IllegalStateException("Special parameter for confirm error.");
}
try {
TimeUnit.SECONDS.sleep(waitSec);
} catch (InterruptedException e) {
Thread.interrupted();
}
model.addAttribute("completedTime", LocalDateTime.now());
}
@ExceptionHandler(Exception.class)
public String handleException() {
return "error";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
同时需要配置DispatcherServlet和各种Filter支持异步处理。
<!-- src/main/webapp/WEB-INF/web.xml -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- ... -->
<async-supported>true</async-supported> <!-- 支持异步处理 -->
</servlet>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<!-- ... -->
</filter>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
此时,服务器的控制台信息将变为如下:
2016-10-05T00:28:24.161 Thread[http-nio-8080-exec-1,5,main]: Start get.
2016-10-05T00:28:24.163 Thread[http-nio-8080-exec-1,5,main]: End get.
2016-10-05T00:28:24.168 Thread[MvcAsync1,5,main]: Start Async processing.
2016-10-05T00:28:25.172 Thread[MvcAsync1,5,main]: End Async processing.
2016-10-05T00:28:25.663 Thread[http-nio-8080-exec-2,5,main]: Called complete.jsp
2016-10-05T00:28:25.663 Thread[http-nio-8080-exec-2,5,main]: FORWARD
- 1
- 2
- 3
- 4
- 5
- 6
从上面可以看出,异步处理部分不再由Tomcat的管理线程(http-nio-8080-exec-xx)执行,而交给Spring MVC专门生成的另一个新线程(MvcAsync1)。
使用线程池
上面的例子中,因为没有使用线程池,因此每次响应一个请求都要新创建一个线程来执行它,显得十分低效。
@EnableWebMvc //如果使用Spring Boot,不能加上这句,否则将导致AutoConfigure失效
@ComponentScan("com.example.component")
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
// ...
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setTaskExecutor(mvcAsyncExecutor()); // 自定义线程池
}
// 让Spring的DI容器来管理ThreadPoolTaskExecutor的生命周期
@Bean
public AsyncTaskExecutor mvcAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(10);
return executor;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
此时服务器的控制台输出信息为:
2016-10-05T00:35:20.574 Thread[http-nio-8080-exec-1,5,main]: Start get.
2016-10-05T00:35:20.576 Thread[http-nio-8080-exec-1,5,main]: End get.
2016-10-05T00:35:20.580 Thread[mvcAsyncExecutor-1,5,main]: Start Async processing.
2016-10-05T00:35:21.583 Thread[mvcAsyncExecutor-1,5,main]: End Async processing.
2016-10-05T00:35:22.065 Thread[http-nio-8080-exec-2,5,main]: Called complete.jsp
2016-10-05T00:35:22.065 Thread[http-nio-8080-exec-2,5,main]: FORWARD
- 1
- 2
- 3
- 4
- 5
- 6
异步处理线程变成了我们自己定义的mvcAsyncExecutor-1池化线程。
使用DeferredResult和@Async注解
如果不是以线程函数的返回值作为最终处理的结果,或者想更灵活地返回处理结果,而不必与线程函数的返回值绑定在一起,使编程更方便,可以考虑使用DeferredResult。同时DeferredResult可以注册超时时间和对应的超时返回结果,十分方便。只需将控制器的代码修改为:
@Controller
@RequestMapping("/async")
public class AsyncController {
// ...
@RequestMapping(path = "deferred", method = RequestMethod.GET)
public DeferredResult<String> getReferred(@RequestParam(defaultValue = "0") long waitSec, Model model) {
Console.println("Start get.");
model.addAttribute("acceptedTime", LocalDateTime.now());
// 超时时间为10s,超时返回"ERROR"。
DeferredResult<String> deferredResult = new DeferredResult<>(10000, "ERROR");
//要把该deferredResult的引用传给对应的异步函数处理
asyncHelper.asyncProcessing(model, waitSec, deferredResult);
Console.println("End get.");
return deferredResult; // 注意:返回值是该DeferredResult
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
package com.example.component;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.context.request.async.DeferredResult;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Component
public class AsyncHelper {
@Async // 该注解十分方便,能使其变成异步函数(相当于一个线程的run函数)
public void asyncProcessing(Model model, long waitSec, DeferredResult<String> deferredResult) {
Console.println("Start Async processing.");
sleep(waitSec);
model.addAttribute("completedTime", LocalDateTime.now());
deferredResult.setResult("complete"); // 此时就通知MVC异步处理已经完成,可以生成HTTP响应。因此后面的代码不会造成HTTP响应的延迟
Console.println("End Async processing.");
}
private void sleep(long timeout) {
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}