这种方式是把耗时逻辑任务的执行与服务器的管理线程相分离,从而实现多线程的并行。因为HTTP
响应在异步处理结束之后才生成,因此从客户端看来与同步处理无异。
通过这种方式,可以在异步处理的任意时刻向客户端发送信息。显然,这种方式是基于HTTP/1.1
的分块传输编码(Chunked transfer encoding
),因此客户端必须支持分块传输编码。
本文将只说明第1种异步处理方式,第2
种方式将放在下篇讲述。
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);
}
}
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());
}
}
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()); %>
Processing is complete !
Accept timestamp is ${acceptedTime}
Complete timestamp is ${completedTime}
因为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
}
}
使用CURL
或者浏览器访问,将得到由complete.jsp
生成的HTML
代码。
$ curl -D - http://localhost:8080/standard?waitSec=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
Processing is complete !
Accept timestamp is 2016-10-05T00:22:46.929
Complete timestamp is 2016-10-05T00:22:47.933
服务器控制台的输出信息:
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
首先使用最普通的方法 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 get(@RequestParam(defaultValue = "0") long waitSec, Model model) {
Console.println("Start get.");
model.addAttribute("acceptedTime", LocalDateTime.now());
// 在Callable的call方法内实现异步处理逻辑
// 因为Callable是函数式接口,因此可以与Java8的lambda表达式隐式转换
Callable 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";
}
}
同时需要配置DispatcherServlet
和各种Filter
支持异步处理。
appServlet
org.springframework.web.servlet.DispatcherServlet
true
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
true
此时,服务器的控制台信息将变为如下:
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
从上面可以看出,异步处理部分不再由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;
}
}
此时服务器的控制台输出信息为:
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
异步处理线程变成了我们自己定义的mvcAsyncExecutor-1池化线程。
如果不是以线程函数的返回值作为最终处理的结果,或者想更灵活地返回处理结果,而不必与线程函数的返回值绑定在一起,使编程更方便,可以考虑使用DeferredResult
。同时DeferredResult
可以注册超时时间和对应的超时返回结果,十分方便。只需将控制器的代码修改为:
@Controller
@RequestMapping("/async")
public class AsyncController {
// ...
@RequestMapping(path = "deferred", method = RequestMethod.GET)
public DeferredResult getReferred(@RequestParam(defaultValue = "0") long waitSec, Model model) {
Console.println("Start get.");
model.addAttribute("acceptedTime", LocalDateTime.now());
// 超时时间为10s,超时返回"ERROR"。
DeferredResult deferredResult = new DeferredResult<>(10000, "ERROR");
//要把该deferredResult的引用传给对应的异步函数处理
asyncHelper.asyncProcessing(model, waitSec, deferredResult);
Console.println("End get.");
return deferredResult; // 注意:返回值是该DeferredResult
}
}
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 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();
}
}
}