MVC之Callable异步请求处理方式

MVC异步请求支持

Servlet 3.0开始支持异步请求的处理,SpringMVC 3.2开始支持Servlet的这种特性.

  1. 项目中修改web.xml,申明web-app_3_0.xsd
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    
  2. 在Servlet配置中启用异步请求支持,本Demo中使用的Spring版本为4.3.9
    <servlet>
        <servlet-name>SpringMVCServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                classpath*:app_config.xml
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <!-- 这里开启异步支持 -->
        <async-supported>true</async-supported>
    </servlet>
    
    配置Spring的线程池,作为MVC的异步支持
    <mvc:async-support task-executor="asyncRunBusinessThreadPoolTaskExecutor" default-timeout="30000"/>
    <bean id="asyncRunBusinessThreadPoolTaskExecutor"
          class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 核心线程数 -->
        <property name="corePoolSize" value="32"/>
        <!-- 最大线程数 -->
        <property name="maxPoolSize" value="1000"/>
        <!-- 创建阻塞队列,线程池满了直接在调用线程上执行 -->
        <property name="queueCapacity" value="0"/>
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name="keepAliveSeconds" value="300"/>
        <property name="threadNamePrefix" value="async-run-business-"/>
        <property name="waitForTasksToCompleteOnShutdown" value="true"/>
        <!-- 线程池对拒绝任务(无线程可用)的处理策略 ThreadPoolExecutor.CallerRunsPolicy ,直接在调用线程执行任务(保证业务逻辑正确). -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
        </property>
    </bean>
    
    ps: Spring线程池及相关阻塞队列的内容详见:

Callable处理异步请求

  • 什么是Callable?
    Callable接口是java.util.concurrent下的一个接口,与Runnable接口类似,同样可以用来创建线程.Callable接口中也只有一个方法:
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

与Runnable接口不同的是,Callable接口中的call()方法是一个带泛型的有返回值,抛出异常的方法.

  • 请求流程:
    • DispatcherServlet返回Callable
    • Spring进行异步处理,将Callable提交到配置好的TaskExecutor中,使用一个隔离线程进行处理
    • Servlet和所有的Filter退出web容器线程,但是保持响应开启状态
    • Callable返回处理结果,Servlet再次被调用并恢复之前的处理
    • MVC进行试图渲染

来一张MVC执行异步请求处理的时序图:
MVC之Callable异步请求处理方式

  • MVC异步请求的优点:
    由时序图可见,MVC的异步请求处理,可以将IO的操作交由独立的隔离线程去处理.空闲出web容器的线程,使web容器可以接收更多请求,可以支持更大的并发量.对于IO密集型项目,不失为一个好的优化方法.
  • Demo
@RestController
public class AsyncDemoController {

    @GetMapping("/demo")
    public Callable<String> asyncTest() {
        System.out::println("主线程开始执行===>" + Thread.currentThread().getName() + "------" + System.currentTimeMillis());
        Callable<String> call = this::showThread;
        System.out::println("主线程执行完毕===>" + Thread.currentThread().getName() + "------" + System.currentTimeMillis());
        return call;
    }

    private String showThread() throws InterruptedException {
        System.out::println("副线程开始执行===>" + Thread.currentThread().getName() + "------" + System.currentTimeMillis());
        Thread.sleep(1000);
        System.out::println("副线程执行完毕===>" + Thread.currentThread().getName() + "------" + System.currentTimeMillis());
        return "SUCCESS";
    }
}
  • 运行结果
主线程开始执行===>qtp1451546120-129------1549016869807
主线程执行完毕===>qtp1451546120-129------1549016869807
副线程开始执行===>async-run-business-1------1549016869814
副线程执行完毕===>async-run-business-1------1549016870815

从运行结果可以清楚看到,Servlet返回Callable,然后结束web容器线程.具体的业务执行交由隔离线程去执行.空闲出web容器线程去接收更多的请求,增强了服务器的并发能力.

文章参考:
SpringMVC之-Callable和DeferredResult异步请求
spring mvc对异步请求的处理
SpringMVC异步处理(Callable和DeferredResult)