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

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

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





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

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

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<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";
    }

}

同时需要配置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>

此时,服务器的控制台信息将变为如下:

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<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
    }

}
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();
        }
    }
}
Spring Boot,配置线程池可以通过在application.properties或application.yml文件设置相关属性来实现。以下是一些常用的属性: 1. 设置核心线程数: ``` spring.task.execution.pool.core-size=10 ``` 2. 设置最大线程数: ``` spring.task.execution.pool.max-size=20 ``` 3. 设置线程池队列容量: ``` spring.task.execution.pool.queue-capacity=100 ``` 4. 设置线程池线程的存活时间(单位为秒): ``` spring.task.execution.pool.keep-alive=30 ``` 5. 设置线程池线程的名称前缀: ``` spring.task.execution.pool.thread-name-prefix=task-executor- ``` 6. 设置拒绝策略: ``` spring.task.execution.pool.rejection-policy=CALLER_RUNS ``` 以上属性spring.task.execution.pool前缀指定了线程池的名称,后面的属性指定了线程池线程的相关参数。 另外,还可以通过在@Configuration类实现TaskExecutorConfigurer接口来自定义线程池配置。例如: ``` @Configuration @EnableAsync public class MyConfig implements TaskExecutorConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { configurer.setTaskExecutor(myTaskExecutor()); } @Bean public TaskExecutor myTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("my-executor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` 在这个例子,我们通过实现TaskExecutorConfigurer接口来配置自定义的线程池,并将其设置为默认的异步执行器。通过@Bean注解,我们创建了一个ThreadPoolTaskExecutor对象,并设置了与上述属性类似的参数。最后,我们将其返回,并将其设置为AsyncSupportConfigurer的任务执行器。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值