前言
现在已经在2019年,这个时候再来谈Spring MVC的异步模式,好像有点老掉牙了。毕竟现在都Spring5的时代了,甚至将来肯定是webflux的天下了。
而Spring MVC的异步请求模式是Spring3.2就推出了,它是基于基Servlet3.0规范实现的,而此规范是2011年推出的,距现在已经有近10的历史了,可谓是非常非常成熟的一种技术规范了。
但是震惊的是,我前端时间一连问了公司的3位同事(工作5年以上),对Spring MVC的异步模式三缄其口,说不出个所以然,更有连Servlet3.0规范都没听过,有什么新特性都没有了解的。着实让我大吃了一惊~
需要说明的一点:我问的这几位同事,做业务方便绝对是杠杠的没问题的,也有很长的Spring MVC使用经验
我想了一下出现这现象的原因:
1、Spring MVC足够优秀,封装得我们现在处理业务请求只需要面向JavaBean去编程即可,没必要再去了解Servlet底层的细节
2、Servlet源生的API在Spring MVC的环境下,使用场景已经非常的少了。甚至给我们一种错觉:servlet技术已经淡化了大众的视野,是不是都不更新了呢?(显然不是的嘛~毕竟4.0的规范都快出来了)
3、Spring MVC的异步模式多多少少都会增加使用的复杂度,从而增加犯错的概率。而它的同步模式可以说是能够满足现在绝大部分的使用场景(大不了觉得性能不够了,就加机器嘛,很少会从代码的本身去考虑和优化性能),所以确实没使用过也是在清理之中的~
Spring 5
这里小插曲不得不简单说一下Spring5。2016 年7月28日重磅发布Spring5.0版本。
Spring Framework 5.0的**最大特点之一是响应式编程(Reactive Programming)。**它使用了如下规范:
- Servlet 3.1
- JMS 2.0
- JPA 2.1
- JAX-RS 2.0
- Bean Validation 1.1
这里必须再提一次它最重要的新特性:响应式编程(Reactive Programming)。为了将下来更好的去学习和深入理解响应式编程的核心内容,我觉得先铺垫此篇文章的讲解尤为重要。
Spring5.0以后,它对servlet不再强依赖,而是变为了可选依赖。另外一个选择还可以是:Reactive编程
Spring MVC的同步模式
要知道什么是异步模式,就先要知道什么是同步模式。
浏览器发起请求,Web服务器开一个线程处理(请求处理线程),处理完把处理结果返回浏览器。这就是同步模式。,绝大多数Web服务器都如此般处理。这里面有几个关键的点:简单示例图如下
此处需要明晰一个概念:比如tomcat,它既是一个web服务器,同时它也是个servlet后端容器(调ava后端服务),所以要区分清楚这两个概念。请求处理线程是有限的,宝贵的资源~(注意它和处理线程的区别)
1.请求发起者发起一个request,然后会一直等待一个response,这期间它是阻塞的
2.请求处理线程会在Call了之后等待Return,自身处于阻塞状态(这个很关键)
3.然后都等待return,知道处理线程全部完事后返回了,然后把response反给调用者就算全部结束了
问题在哪里?
绝大部分情况下,这样是没有问题的。因为
第一:高并发、高流量的场景放眼中国的公司,占比也是非常少的。
第二:长时间处理服务这种情况也是少之又少的
所以两者结合起来,场景就更加稀少了。相信这就是为什么好多做开发N年了的,却还不知道Servlet和Spring MVC的异步模式的原因吧。
正所谓,别人不懂的地方,咱们才有机会嘛。因此好好学习本文的内容,能让你升值哦~
Tomcat等应用服务器的连接线程池实际上是有限制的;每一个连接请求都会耗掉线程池的一个连接数;如果某些耗时很长的操作,如对大量数据的查询操作、调用外部系统提供的服务以及一些IO密集型操作等,会占用连接很长时间,这个时候这个连接就无法被释放而被其它请求重用。如果连接占用过多,服务器就很可能无法及时响应每个请求;极端情况下如果将线程池中的所有连接耗尽,服务器将长时间无法向外提供服务!
Spring MVC异步模式Demo Show
Spring MVC3.2之后支持异步请求,能够在controller中返回一个Callable或者DeferredResult。由于Spring MVC的良好封装,异步功能使用起来出奇的简单。
Callable案例:
@Controller @RequestMapping("/async/controller") public class AsyncHelloController { @ResponseBody @GetMapping("/hello") public Callable<String> helloGet() throws Exception { System.out.println(Thread.currentThread().getName() + " 主线程start"); Callable<String> callable = () -> { System.out.println(Thread.currentThread().getName() + " 子子子线程start"); TimeUnit.SECONDS.sleep(5); //模拟处理业务逻辑,话费了5秒钟 System.out.println(Thread.currentThread().getName() + " 子子子线程end"); // 这里稍微小细节一下:最终返回的不是Callable对象,而是它里面的内容 return "hello world"; }; System.out.println(Thread.currentThread().getName() + " 主线程end"); return callable; } }
输出:
http-apr-8080-exec-3 主线程start http-apr-8080-exec-3 主线程end MvcAsync1 子子子线程start MvcAsync1 子子子线程end
先明细两个概念:
- 请求处理线程:处理线程 属于 web 服务器线程,负责 处理用户请求,采用 线程池 管理。
- 异步线程:异步线程 属于 用户自定义的线程,可采用 线程池管理。
前端页面等待5秒出现结果。
注意:异步模式对前端来说,是无感知的,这是后端的一种技术。所以这个和我们自己开启一个线程处理,立马返回给前端是有非常大的不同的,需要注意~
由此我们可以看出,主线程早早就结束了(需要注意,此时还并没有把response返回的,此处一定要注意),真正干事的是子线程(交给TaskExecutor去处理的,后续分析过程中可以看到),它的大致的一个处理流程图可以如下:
这里能够很直接的看出:我们很大程度上提高了我们请求处理线程的利用率,从而肯定就提高了我们系统的吞吐量。
异步模式处理步骤概述如下:
1.当Controller返回值是Callable的时候
2.Spring就会将Callable交给TaskExecutor去处理(一个隔离的线程池)
3.与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态
4.Callable线程处理完成后,Spring MVC讲请求重新派发给容器**(注意这里的重新派发,和后面讲的拦截器密切相关)**
5.根据Callabel返回结果,继续处理(比如参数绑定、视图解析等等就和之前一样了)~~~~
Spring官方解释如下截图:
WebAsyncTask
案例:
官方有这么一句话,截图给你:
如果我们需要超时处理的回调或者错误处理的回调,我们可以使用WebAsyncTask
代替Callable
实际使用中,我并不建议直接使用Callable ,而是使用Spring提供的
WebAsyncTask
代替,它包装了Callable,功能更强大些
@Controller @RequestMapping("/async/controller") public class AsyncHelloController { @ResponseBody @GetMapping("/hello") public WebAsyncTask<String> helloGet() throws Exception { System.out.println(Thread.currentThread().getName() + " 主线程start"); Callable<String> callable = () -> { System.out.println(Thread.currentThread().getName() + " 子子子线程start"); TimeUnit.SECONDS.sleep(5); //模拟处理业务逻辑,话费了5秒钟 System.out.println(Thread.currentThread().getName() + " 子子子线程end"); return "hello world"; }; // 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称 WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, callable); // 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的 webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调")); // 这两个返回的内容,最终都会放进response里面去=========== webAsyncTask.onTimeout(() -> "程序[超时]的回调"); // 备注:这个是Spring5新增的 webAsyncTask.onError(() -> "程序[出现异常]的回调"); System.out.println(Thread.currentThread().getName() + " 主线程end"); return webAsyncTask; } }
如上,由于我们设置了超时时间为3000ms,而业务处理是5s,所以会执行onTimeout这个回调函数。因此页面是会显示“程序[超时]的回调”这几个字。其执行的过程同Callback。