理解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);
    }
}
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}

Go to Top

因为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

Go to Top

服务器控制台的输出信息:

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

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 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和@Async注解

如果不是以线程函数的返回值作为最终处理的结果,或者想更灵活地返回处理结果,而不必与线程函数的返回值绑定在一起,使编程更方便,可以考虑使用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();
        }
    }
}

你可能感兴趣的:(JAVA)