理解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();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值