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

59 篇文章 131 订阅

写在前面

在上一讲中,我们讲了一下Spring MVC异步请求处理的第一种使用方式,即将方法的返回值写成Callable。然而在实际的开发过程中,异步请求处理是绝不可能这么简单的,就拿下面这个实际的场景来说,如下图所示。

在这里插入图片描述

我们以创建订单为例,创建订单的请求一进来,应用1就要启动一个线程,来帮我们处理这个请求。如果假设应用1并不能创建订单,创建订单需要应用2来完成,那么此时应该怎么办呢?应用1可以把创建订单的消息存放在消息中间件中,比如RabbitMQ、Kafka等等,而应用2就来负责监听这些消息中间件里面的消息,一旦它发现有创建订单这个消息,那么它就进行相应处理,然后将处理完成后的结果,比如订单的订单号等等,再次存放在消息中间件中,接着应用1再启动一个线程,例如线程2,来监听消息中间件中的返回结果,只要订单创建完毕,它就会拿到返回的结果(即订单的订单号),最后将其响应给客户端。

不用说,你肯定会在实际开发中遇到类似的场景。

Spring MVC中的异步请求处理

当然,我们不可能来完全编写出以上应用2以及那些消息中间件,因此在这儿我们就做一个大概的演示。

如果真遇到了以上那种实际场景,那么该怎么办呢?其实,Spring MVC也有结合消息中间件的使用场景,即利用DeferredResult。说得更具体一点就是,当客户端发送一个请求过来以后,即使该请求不能及时得到处理,那也没有关系,你可以先new一个DeferredResult对象,然后把该对象返回出去,并把该对象保存在其他的地方,此时请求依旧在等待处理的过程中,那什么时候这个请求能被处理完成呢?这得等到另外一个线程在其他的地方获取到DeferredResult对象,并调用其setResult方法设置了结果之后,此时请求才会被处理完成,并响应给客户端。

于是,接下来,我们就编写一个简单的案例来模拟一下这个简单的场景。

首先,我们在AsyncController里面编写一个方法,例如createOrder,该方法的名字看起来好像是专门用于创建订单的,但是它并不能真正地来处理创建订单的请求,要想真正地来处理创建订单的请求,那还需要另外一个线程。注意,createOrder方法的返回值类型要写成DeferredResult哟😊

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;
import org.springframework.web.context.request.async.DeferredResult;

@Controller
public class AsyncController {
	
	@ResponseBody
	@RequestMapping("/createOrder")
	public DeferredResult<Object> createOrder() {
	    /*
	     * 在创建DeferredResult对象时,可以像下面这样传入一些参数哟!
	     * 
	     * 第一个参数(timeout): 超时时间。限定(请求?)必须在该时间内执行完,如果超出时间限制,那么就会返回一段错误的提示信息(timeoutResult)
	     * 第二个参数(timeoutResult):超出时间限制之后,返回的错误提示信息
	     */
		DeferredResult<Object> deferredResult = new DeferredResult<>((long)3000, "create fail...");
		return deferredResult;
	}

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

从上可以看到,为了能够把消息直接写出去,我们使用的是@ResponseBody注解,这样就可以阻止跳转页面了。其实,我们还能看到,现在还没有另外一个线程来调用DeferredResult对象中的setResult方法为其设置结果。

然后,我们来启动项目进行测试。项目启动成功之后,来访问createOrder请求,发现大概等了3秒钟之后,快速地响应给浏览器create fail...这样一串字符串,如下图所示。

在这里插入图片描述

很明显这是超出时间限制了,应该是相当于创建订单失败了吧!那接下来,我们应该怎么办呢?真令人头疼😭

OK,我们不妨来模拟一个队列。首先新建一个类,例如DeferredResultQueue,如下所示。

package com.meimeixia.service;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.springframework.web.context.request.async.DeferredResult;

public class DeferredResultQueue {
	
	// DeferredResult对象临时保存的地方
	private static Queue<DeferredResult<Object>> queue = new ConcurrentLinkedQueue<DeferredResult<Object>>();

	// 临时保存DeferredResult对象的方法
	public static void save(DeferredResult<Object> deferredResult) {
		queue.add(deferredResult);
	}
	
	// 获取DeferredResult对象的方法
	public static DeferredResult<Object> get() {
		/*
		 * poll():检索并且移除,移除的是队列头部的元素
		 */
		return queue.poll();
	}
	
}

然后,修改一下AsyncController中的createOrder方法。上面我也已经说过了,该方法并不能真正地来处理创建订单的请求。即使如此,那也没关系,因为我们可以在该方法中先把new出来的DeferredResult对象临时保存起来。

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;
import org.springframework.web.context.request.async.DeferredResult;

import com.meimeixia.service.DeferredResultQueue;

@Controller
public class AsyncController {
	
	@ResponseBody
	@RequestMapping("/createOrder")
	public DeferredResult<Object> createOrder() {
		/*
		* 在创建DeferredResult对象时,可以像下面这样传入一些参数哟!
		* 
		* 第一个参数(timeout): 超时时间。限定(请求?)必须在该时间内执行完,如果超出时间限制,那么就会返回一段错误的提示信息(timeoutResult)
		* 第二个参数(timeoutResult):超出时间限制之后,返回的错误提示信息
		*/
		DeferredResult<Object> deferredResult = new DeferredResult<>((long)3000, "create fail...");
		DeferredResultQueue.save(deferredResult);
		return deferredResult;
	}

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

当然了,实际开发中应该有一个别的线程来专门监听这个事,啥事?我猜应该是在其他的地方临时保存DeferredResult对象这件事吧!不过,我们在这儿并不会这样做,而是再在AsyncController中编写一个方法,例如create,该方法才是真正来处理创建订单的请求的。

@ResponseBody
@RequestMapping("/create")
public String create() {
    // 在这模拟创建订单
    String order = UUID.randomUUID().toString();
    /*
     * 如果我们想在上一个请求(即createOrder)中使用订单,那么该怎么办呢?从临时保存DeferredResult对象的地方获取
     * 到刚才保存的DeferredResult对象,然后调用其setResult方法设置结果,例如设置订单的订单号
     */
    DeferredResult<Object> deferredResult = DeferredResultQueue.get();
    deferredResult.setResult(order);
    // 这儿给客户端直接响应"success===>订单号"这样的字符串,不要再跳转页面了
    return "success===>" + order;
}

至此,可以看到,只要客户端发送一个createOrder请求进来创建订单,那么服务端就会先将new出来的DeferredResult对象临时保存起来。等到create方法触发并把订单号设置进去之后,在createOrder方法中就会立即得到返回结果,即订单号。

最后,我们来重启项目进行测试。重启成功之后,先来访问createOrder请求,以便来创建订单,但是订单必须得在3秒内创建完,所以一旦访问了createOrder请求后,你必须立即访问create请求来真正创建订单,而且至少得在3秒内完成。

这时,你会看到什么结果呢?可以看到访问create请求之后,直接给浏览器页面响应了一个success===>4e7e3e4c-27d2-4989-87c1-00d545e05feb这样的字符串,其中4e7e3e4c-27d2-4989-87c1-00d545e05feb就是所创建订单的订单号,如下图所示。

在这里插入图片描述

切到访问createOrder请求的浏览器窗口之后,你也可以在浏览器页面中看到所创建订单的订单号,即4e7e3e4c-27d2-4989-87c1-00d545e05feb,如下图所示。

在这里插入图片描述

可以看到订单号都是一样的,测试完全通过!

至此,我们是不是可以得出这样的结论:另外一个线程拿到临时保存的DeferredResult对象之后,只要将最终处理的结果给该对象设置进去,那么另一边的线程就能立即得到返回结果了

以上就是Spring MVC异步请求处理的第二种使用方式,即将方法的返回值写成DeferredResult。

至此,Spring注解驱动开发系列教程我就更新完了,真的是太不容易了😂!我是李阿昀,如果我写得不错,那么请记得关注、评论、点赞!拜拜各位,我要开始新的征程了,江湖再见!!!

  • 4
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:白松林 返回首页

打赏

李阿昀

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值