Spring注解驱动开发第57讲——体验一把Spring MVC中的异步请求处理(返回Callable)

本文详细讲解了Spring MVC中基于Servlet 3.0的异步请求处理,特别是返回Callable的方法。通过示例展示了如何在Controller中创建异步方法,重点解析了Spring MVC异步处理的五个步骤,包括Callable的执行流程和TaskExecutor的角色。同时,讨论了异步拦截器的概念和实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

在上一讲,我讲了一下Servlet 3.0里面的异步请求处理。而在这一讲中,我会基于Servlet 3.0中的异步请求处理机制来讲述一下Spring MVC里面的异步请求处理。

其实,我们完全可以参考Spring MVC的官方文档来掌握它里面的异步请求处理。打开Spring MVC的官方文档,找到1.6. Asynchronous Requests这一章节并打开,稍微浏览一下这一章节中的内容,如下图所示。

在这里插入图片描述

我们应该知道,Spring MVC中的异步请求处理是基于Servlet 3.0中的异步请求处理机制的,相当于做了一个简单的封装。也就是说,即使某人不知道Servlet里面的API,他也可以使用Spring MVC中的异步请求处理机制。

咋个来使用呢?继续往下看1.6. Asynchronous Requests这一章节中的内容,发现使用方式有两种,一种是将方法的返回值写成Callable,如下图所示。

在这里插入图片描述

另一种是将方法的返回值写成DeferredResult,如下图所示。

在这里插入图片描述

以上两种方式都是可行的,不过在这一讲中,我们先来讲解第一种使用方式,即将方法的返回值写成Callable。

Spring MVC中的异步请求处理

我们来到springmvc-annotation-liayun这一项目中,在该项目中新建一个Controller,例如AsyncController,然后在该AsyncController中编写一个如下的async01方法来处理异步请求。

package com.meimeixia.controller;

import java.util.concurrent.Callable;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AsyncController {

	@ResponseBody
	@RequestMapping("/async01")
	public Callable<String> async01() {
		Callable<String> callable = new Callable<String>() {

			@Override
			public String call() throws Exception {
				// 响应给客户端一串字符串,即"Callable<String> async01()"
				return "Callable<String> async01()";
			}
			
		};
		return callable;
	}
	
}

可以看到与我们写的常规方法不同,以上async01方法的返回值是Callable<String>,其中泛型String就是要响应给客户端的数据的数据类型。

为了让大家看到究竟是哪些线程在工作,我们先在async01方法中打印一下主线程,看主线程究竟是谁,并且还打印一下主线程开始与结束的时间,如下所示。

package com.meimeixia.controller;

import java.util.concurrent.Callable;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AsyncController {

	@ResponseBody
	@RequestMapping("/async01")
	public Callable<String> async01() {
		System.out.println("主线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		Callable<String> callable = new Callable<String>() {

			@Override
			public String call() throws Exception {
				// 响应给客户端一串字符串,即"Callable<String> async01()"
				return "Callable<String> async01()";
			}
			
		};
		System.out.println("主线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		return callable;
	}
	
}

这里有一点需要我们注意,那就是我们的业务逻辑是要写在Callable对象中的call方法里面的,一般而言业务逻辑是非常复杂的,执行可能得花费一段时间,为了模拟这一点,我们在这儿不妨让线程睡上2秒,如下所示。

package com.meimeixia.controller;

import java.util.concurrent.Callable;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AsyncController {

	@ResponseBody
	@RequestMapping("/async01")
	public Callable<String> async01() {
		System.out.println("主线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		Callable<String> callable = new Callable<String>() {

			@Override
			public String call() throws Exception {
				Thread.sleep(2000); // 我们来睡上2秒 
				// 响应给客户端一串字符串,即"Callable<String> async01()"
				return "Callable<String> async01()";
			}
			
		};
		System.out.println("主线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		return callable;
	}
	
}

同样地,我们再在以上call方法中打印一下副线程,看副线程究竟是谁,并且还打印一下副线程开始与结束的时间,如下所示。

package com.meimeixia.controller;

import java.util.concurrent.Callable;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AsyncController {

	@ResponseBody
	@RequestMapping("/async01")
	public Callable<String> async01() {
		System.out.println("主线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		Callable<String> callable = new Callable<String>() {

			@Override
			public String call() throws Exception {
				System.out.println("副线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
				Thread.sleep(2000); // 我们来睡上2秒 
				System.out.println("副线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
				// 响应给客户端一串字符串,即"Callable<String> async01()"
				return "Callable<String> async01()";
			}
			
		};
		System.out.println("主线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		return callable;
	}
	
}

OK,编写完以上AsyncController之后,我们便来启动项目进行测试。项目启动成功之后,我们来访问async01请求,大概等上2秒之后,我们就能在浏览器页面中看到Callable async01()这样的字符串了。而且,Eclipse控制台还打印了如下内容。

在这里插入图片描述

从上图中可以看到,主线程从开始到结束,很快,间隔时间几乎不可计,而且用的都是http-nio-8080-exec-6这一个线程;而副线程从开始到结束,真的就差不多耗了2秒钟,而且用的都是MvcAsync1这一个线程,也就是说,以上call方法里面写的代码都是在另外一个线程(即MvcAsync1)里面执行的。

那为什么会打印成这个样子啊,其原理又是怎样的呢?不急,下面我会向大家解释。

其实,Spring MVC的官方文档就已经做了解释了,我们不妨看一下。打开Spring MVC的官方文档,找到1.6.3. Processing这一小节并打开,细心点看,发现若Controller中方法的返回值写成了Callable,则其处理流程就应该是下面这个样子的。

在这里插入图片描述

以上的每一步,我们不妨来逐步逐步来分析。

第一步,控制器返回Callable。其实,这里要说的是控制器中方法的返回值要写成Callable了,而再也不能是以前普通的字符串对象了。

第二步,控制器返回Callable以后,Spring MVC就会异步地启动一个处理方法(即Spring MVC异步处理),也即将Callable提交到TaskExecutor(任务执行器)里面,并使用一个隔离的线程进行处理。

注意,TaskExecutor(任务执行器)是JUC包中Executor旗下的,我们不妨点进去TaskExecutor源码里面看一看,如下图所示。

在这里插入图片描述

可以清楚地看到,TaskExecutor(任务执行器)是我们Spring MVC自己来定义的,只不过它继承了JUC包里面的Executor接口,它是来执行Runnable对象中的run方法里面的任务的。

第三步,与此同时,DispatcherServlet和所有的Filter将会退出Servlet容器的线程(即主线程),但是response仍然保持打开的状态。既然response依旧保持打开状态,那就表明还没有给浏览器以响应,因此我们还能给response里面写数据。

第四步,最终,Callable返回一个结果,并且Spring MVC会将请求重新派发给Servlet容器,恢复之前的处理。也就是说,之前的response依旧还保存着打开的状态,仍然还可以往其里面写数据。

第五步,如果还是把上一次的请求再发过来,假设上一次的请求是async01,那么DispatcherServlet依旧还是能接收到该请求,收到以后,DispatcherServlet便会再次执行,来恢复之前的处理。

那么它是如何来处理的呢?根据Callable返回的结果,该怎么处理还是怎么处理,即Spring MVC会继续进入视图渲染等等流程,也就是说,Spring MVC会从头开始再次执行其流程(收请求→视图渲染→给响应),这是因为请求会被再次发给Spring MVC。

上面我对Spring MVC的异步模式处理步骤说得够明白了吧!要是脑袋还是昏昏涨涨的,那我也没法了。

我们不妨再次看一下Eclipse控制台中打印的如下内容,发现咱们自己编写的拦截器(即MyFirstInterceptor)也有相应内容打印。

在这里插入图片描述

而且它还是这样打印的:首先在目标方法运行之前打印preHandle...,然后主线程和副线程相继开始与结束,接着再次在目标方法运行之前打印preHandle...,紧接着在目标方法运行正确以后打印postHandle...,最后在页面响应以后打印afterCompletion...

唉😡,不知你有没有发现preHandle...打印了两次哟~,这正是因为Callable线程处理完成后,Spring MVC会将请求重新派发给Servlet容器,并且由于我们配置的DispatcherServlet是拦截所有请求(即/),所以Spring MVC依然会接收到重新派发过来的请求,继而就能继续进行之前的处理了。

为了验证这一点,我们不妨在目标方法运行之前打印一下拦截器到底拦截的是哪一个请求,如下所示。

package com.meimeixia.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class MyFirstInterceptor implements HandlerInterceptor {

	// 在页面响应以后执行
	@Override
	public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
			throws Exception {
		// TODO Auto-generated method stub
		System.out.println("afterCompletion...");
	}

	// 在目标方法运行正确以后执行
	@Override
	public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
			throws Exception {
		// TODO Auto-generated method stub
		System.out.println("postHandle...");
	}

	// 在目标方法运行之前执行
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse arg1, Object arg2) throws Exception {
		// TODO Auto-generated method stub
		System.out.println("preHandle..." + request.getRequestURI());
		return true; // 返回true,表示放行(目标方法)
	}

}

然后,我们便来再次启动项目进行测试。项目启动成功之后,我们依旧来访问async01请求,大概等上2秒之后,我们依然能在浏览器页面中看到Callable async01()这样的字符串,只不过,Eclipse控制台此时打印出了如下内容。

在这里插入图片描述

以上打印内容我们应该分为三段来看,第一段内容如下:

preHandle.../springmvc-annotation-liayun/async01
主线程开始...Thread[http-nio-8080-exec-3,5,main]==>1616292788290
主线程结束...Thread[http-nio-8080-exec-3,5,main]==>1616292788291

这一段除了打印主线程的开始与结束之外,还在目标方法运行之前打印了preHandle...。至此,DispatcherServlet以及所有的Filter都退出了主线程。

你应该知道的一点是,其实主线程中运行的目标方法就是AsyncController中的async01方法,如下所示,可以很明显地看到该方法的返回值是Callable,一旦该方法返回了Callable,主线程便结束了,相应地,DispatcherServlet也会退出该主线程。

package com.meimeixia.controller;

import java.util.concurrent.Callable;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AsyncController {

	@ResponseBody
	@RequestMapping("/async01")
	public Callable<String> async01() {
		System.out.println("主线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		Callable<String> callable = new Callable<String>() {

			@Override
			public String call() throws Exception {
				System.out.println("副线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
				Thread.sleep(2000); // 我们来睡上2秒 
				System.out.println("副线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
				// 响应给客户端一串字符串,即"Callable<String> async01()"
				return "Callable<String> async01()";
			}
			
		};
		System.out.println("主线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis());
		return callable;
	}
	
}

分析完第一段的内容之后,我们再来看第二段内容,如下所示:

副线程开始...Thread[MvcAsync1,5,main]==>1616292788299
副线程结束...Thread[MvcAsync1,5,main]==>1616292790301

这儿是将Callable提交到TaskExecutor(任务执行器)里面,并使用一个隔离的线程来进行处理,所谓的处理应该就是执行Callable对象中的call方法里面的任务。不知我这样理解对不对😂

最后,我们来看看第三段内容,如下所示:

preHandle.../springmvc-annotation-liayun/async01
postHandle...
afterCompletion...

当Callable对象中的任务执行完成以后,Spring MVC会将请求重新派发给Servlet容器,以恢复之前的处理。此时,Spring MVC依旧会接收到重新派发过来的请求,即async01请求,这可以从以上打印结果中看出。

现在你该明白了吧!之前Spring MVC接收到的请求是async01,现在依然还是async01,只不过,此时收到请求之后,目标方法并不用被执行了,之前返回的Callable就是目标方法的返回值。接下来,postHandle...afterCompletion...自然就要被依次打印了。

以上就是Spring MVC的异步处理过程。当然,我们要知道的一点是,在异步处理的情况下,Spring MVC并不能拦截到真正的业务逻辑的整个处理流程,而想要做到这一点,那就得使用异步的拦截器了,而且异步的拦截器有两种,它们分别是:

  • 原生Servlet里面的AsyncListener。我们不妨看一下它的源码,如下图所示,发现它是一个接口。

    在这里插入图片描述

  • Spring MVC给提供的异步拦截器,即AsyncHandlerInterceptor。我们不妨看一下Spring MVC的官方文档,打开它,然后查看1.6.3. Processing这一小节下的Interception这一部分的内容,你会发现要想成为一个异步拦截器,那么它必须得实现AsyncHandlerInterceptor接口。

    在这里插入图片描述

    也就是说,如果在使用Spring MVC的情况下,那么你只须实现AsyncHandlerInterceptor接口即能编写一个异步拦截器了。

终于终于我们讲完了Spring MVC异步请求处理的第一种使用方式,即将方法的返回值写成Callable。如果使用这种方式,那么就会启动一个新的线程(我觉得应该是Callable线程,不知道我理解的对不对😭),而且Spring MVC还帮我们维护了一个异步处理的线程池,就像下面这张图所表示的那样,这张图我们在上一讲中就看过了。

在这里插入图片描述

从上图中我们可以看到,主线程会先进来执行,对于Spring MVC而言,如果这是一个异步任务,那么对应地就会有异步处理线程池中的线程来进行异步处理,这时主线程会立马进行释放,然后等待迎接下一个请求。而且,在异步处理完了以后,之前一直保持打开状态的response会将响应数据写出去。

唉,不知道我这样理解对不对😭,各位大佬有什么建议可以提出来吗?

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李阿昀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值