关于
web开发
的相关知识点,后续有补充时再开续写了。好比webService
服务、发邮件
等,这些通常上以为不彻底属于web开发
方面的,并且目前webService
做为一个接口来提供服务的机会应该比较小了吧。因此本章节开始,开始讲解关于异步开发过程当中会使用到的一些知识点。本章节就来说解下异步请求相关知识点。html
在Servlet 3.0
以前,Servlet
采用Thread-Per-Request
的方式处理请求,即每一次Http
请求都由某一个线程从头至尾负责处理。若是一个请求须要进行IO操做,好比访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待IO操做完成, 而IO操做是很是慢的,因此此时的线程并不能及时地释放回线程池以供后续使用,在并发量愈来愈大的状况下,这将带来严重的性能问题。其请求流程大体为:前端
而在Servlet3.0
发布后,提供了一个新特性:异步处理请求。能够先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,能够在耗时处理完成(例如长时间的运算)时再对客户端进行响应。其请求流程为:java
在Servlet 3.0
后,咱们能够从HttpServletRequest
对象中得到一个**AsyncContext
**对象,该对象构成了异步处理的上下文,Request
和Response
对象均可从中获取。AsyncContext
能够从当前线程传给另外的线程,并在新的线程中完成对请求的处理并返回结果给客户端,初始线程即可以还回给容器线程池以处理更多的请求。如此,经过将请求从一个线程传给另外一个线程处理的过程便构成了Servlet 3.0
中的异步处理。git
多说几句:github
随着Spring5
发布,提供了一个响应式Web框架:Spring WebFlux
。以后可能就不须要Servlet
容器的支持了。如下是其前后对比图:web
左侧是传统的基于Servlet
的Spring Web MVC
框架,右侧是5.0版本新引入的基于Reactive Streams
的Spring WebFlux
框架,从上到下依次是Router Functions,WebFlux,Reactive Streams三个新组件。spring
对于其发展前景仍是拭目以待吧。有时间也该去了解下Spring5
了。数据库
在编写实际代码以前,咱们来了解下一些关于异步请求的api的调用说明。api
HttpServletRequest
对象获取。AsyncContext asyncContext = request.startAsync();
其监听器的接口代码:springboot
public interface AsyncListener extends EventListener { void onComplete(AsyncEvent event) throws IOException; void onTimeout(AsyncEvent event) throws IOException; void onError(AsyncEvent event) throws IOException; void onStartAsync(AsyncEvent event) throws IOException; }
说明:
通常上,咱们在超时或者异常时,会返回给前端相应的提示,好比说超时了,请再次请求等等,根据各业务进行自定义返回。同时,在异步调用完成时,通常须要执行一些清理工做或者其余相关操做。
须要注意的是只有在调用request.startAsync
前将监听器添加到AsyncContext
,监听器的onStartAsync
方法才会起做用,而调用startAsync
前AsyncContext
还不存在,因此第一次调用startAsync
是不会被监听器中的onStartAsync
方法捕获的,只有在超时后又从新开始的状况下onStartAsync
方法才会起做用。
setTimeout
方法设置,单位:毫秒。必定要设置超时时间,不能无限等待下去,否则和正常的请求就同样了。。
前面已经提到,可经过HttpServletRequest
对象中得到一个**AsyncContext
**对象,该对象构成了异步处理的上下文。因此,咱们来实际操做下。
0.编写一个简单控制层
/** * 使用servlet方式进行异步请求 * @author oKong * */ @Slf4j @RestController public class ServletController { @RequestMapping("/servlet/orig") public void todo(HttpServletRequest request, HttpServletResponse response) throws Exception { //这里来个休眠 Thread.sleep(100); response.getWriter().println("这是【正常】的请求返回"); } @RequestMapping("/servlet/async") public void todoAsync(HttpServletRequest request, HttpServletResponse response) { AsyncContext asyncContext = request.startAsync(); asyncContext.addListener(new AsyncListener() { @Override public void onTimeout(AsyncEvent event) throws IOException { log.info("超时了:"); //作一些超时后的相关操做 } @Override public void onStartAsync(AsyncEvent event) throws IOException { // TODO Auto-generated method stub log.info("线程开始"); } @Override public void onError(AsyncEvent event) throws IOException { log.info("发生错误:",event.getThrowable()); } @Override public void onComplete(AsyncEvent event) throws IOException { log.info("执行完成"); //这里能够作一些清理资源的操做 } }); //设置超时时间 asyncContext.setTimeout(200); //也能够不使用start 进行异步调用 // new Thread(new Runnable() { // // @Override // public void run() { // 编写业务逻辑 // // } // }).start(); asyncContext.start(new Runnable() { @Override public void run() { try { Thread.sleep(100); log.info("内部线程:" + Thread.currentThread().getName()); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("这是【异步】的请求返回"); } catch (Exception e) { log.error("异常:",e); } //异步请求完成通知 //此时整个请求才完成 //其实能够利用此特性 进行多条消息的推送 把链接挂起。。 asyncContext.complete(); } }); //此时之类 request的线程链接已经释放了 log.info("线程:" + Thread.currentThread().getName()); } }
注意:异步请求时,能够利用ThreadPoolExecutor
自定义个线程池。
1.启动下应用,查看控制台输出就能够获悉是否在同一个线程里面了。同时,可设置下等待时间,以后就会调用超时回调方法了。你们可本身试试。
2018-08-15 23:03:04.082 INFO 6732 --- [nio-8080-exec-1] c.l.l.s.controller.ServletController : 线程:http-nio-8080-exec-1 2018-08-15 23:03:04.183 INFO 6732 --- [nio-8080-exec-2] c.l.l.s.controller.ServletController : 内部线程:http-nio-8080-exec-2 2018-08-15 23:03:04.190 INFO 6732 --- [nio-8080-exec-3] c.l.l.s.controller.ServletController : 执行完成
使用过滤器时,须要加入asyncSupported
为true
配置,开启异步请求支持。
@WebServlet(urlPatterns = "/okong", asyncSupported = true ) public class AsyncServlet extends HttpServlet ...
**题外话:**其实咱们能够利用在未执行asyncContext.complete()
方法时请求未结束这特性,能够作个简单的文件上传进度条之类的功能。但注意请求是会超时的,须要设置超时的时间下。
在
Spring
中,有多种方式实现异步请求,好比callable
、DeferredResult
或者WebAsyncTask
。每一个的用法略有不一样,可根据不一样的业务场景选择不一样的方式。如下主要介绍一些经常使用的用法
使用很简单,直接返回的参数包裹一层
callable
便可。
@RequestMapping("/callable") public Callable<String> callable() { log.info("外部线程:" + Thread.currentThread().getName()); return new Callable<String>() { @Override public String call() throws Exception { log.info("内部线程:" + Thread.currentThread().getName()); return "callable!"; } }; }
控制台输出:
2018-08-15 23:32:22.317 INFO 15740 --- [nio-8080-exec-2] c.l.l.s.controller.SpringController : 外部线程:http-nio-8080-exec-2 2018-08-15 23:32:22.323 INFO 15740 --- [ MvcAsync1] c.l.l.s.controller.SpringController : 内部线程:MvcAsync1
从控制台能够看见,异步响应的线程使用的是名为:MvcAsync1
的线程。第一次再访问时,就是MvcAsync2
了。若采用默认设置,会无限的建立新线程去处理异步请求,因此正常都须要配置一个线程池及超时时间。
编写一个配置类:CustomAsyncPool.java
@Configuration public class CustomAsyncPool extends WebMvcConfigurerAdapter{ /** * 配置线程池 * @return */ @Bean(name = "asyncPoolTaskExecutor") public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("callable-"); // 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } @Override public void configureAsyncSupport(final AsyncSupportConfigurer configurer) { //处理 callable超时 configurer.setDefaultTimeout(60*1000); configurer.registerCallableInterceptors(timeoutInterceptor()); configurer.setTaskExecutor(getAsyncThreadPoolTaskExecutor()); } @Bean public TimeoutCallableProcessor timeoutInterceptor() { return new TimeoutCallableProcessor(); } }
自定义一个超时异常处理类:CustomAsyncRequestTimeoutException.java
/** * 自定义超时异常类 * @author oKong * */ public class CustomAsyncRequestTimeoutException extends RuntimeException { /** * */ private static final long serialVersionUID = 8754629185999484614L; public CustomAsyncRequestTimeoutException(String uri){ super(uri); } }
同时,在统一异常处理加入对CustomAsyncRequestTimeoutException
类的处理便可,这样就有个统一的配置了。
以后,再运行就能够看见使用了自定义的线程池了,超时的能够自行模拟下:
2018-08-15 23:48:29.022 INFO 16060 --- [nio-8080-exec-1] c.l.l.s.controller.SpringController : 外部线程:http-nio-8080-exec-1 2018-08-15 23:48:29.032 INFO 16060 --- [ oKong-1] c.l.l.s.controller.SpringController : 内部线程:oKong-1
相比于
callable
,DeferredResult
能够处理一些相对复杂一些的业务逻辑,最主要仍是能够在另外一个线程里面进行业务处理及返回,便可在两个彻底不相干的线程间的通讯。
/** * 线程池 */ public static ExecutorService FIXED_THREAD_POOL = Executors.newFixedThreadPool(30); @RequestMapping("/deferredresult") public DeferredResult<String> deferredResult(){ log.info("外部线程:" + Thread.currentThread().getName()); //设置超时时间 DeferredResult<String> result = new DeferredResult<String>(60*1000L); //处理超时事件 采用委托机制 result.onTimeout(new Runnable() { @Override public void run() { log.error("DeferredResult超时"); result.setResult("超时了!"); } }); result.onCompletion(new Runnable() { @Override public void run() { //完成后 log.info("调用完成"); } }); FIXED_THREAD_POOL.execute(new Runnable() { @Override public void run() { //处理业务逻辑 log.info("内部线程:" + Thread.currentThread().getName()); //返回结果 result.setResult("DeferredResult!!"); } }); return result; }
控制台输出:
2018-08-15 23:52:27.841 INFO 12984 --- [nio-8080-exec-2] c.l.l.s.controller.SpringController : 外部线程:http-nio-8080-exec-2 2018-08-15 23:52:27.843 INFO 12984 --- [pool-1-thread-1] c.l.l.s.controller.SpringController : 内部线程:pool-1-thread-1 2018-08-15 23:52:27.872 INFO 12984 --- [nio-8080-exec-2] c.l.l.s.controller.SpringController : 调用完成
注意:返回结果时记得调用下setResult
方法。
题外话:利用DeferredResult
可实现一些长链接的功能,好比当某个操做是异步时,咱们能够保存这个DeferredResult
对象,当异步通知回来时,咱们在找回这个DeferredResult
对象,以后在setResult
会结果便可。提升性能。
使用方法都相似,只是
WebAsyncTask
是直接返回了。以为就是写法不一样而已,更多细节但愿大神解答!
@RequestMapping("/webAsyncTask") public WebAsyncTask<String> webAsyncTask() { log.info("外部线程:" + Thread.currentThread().getName()); WebAsyncTask<String> result = new WebAsyncTask<String>(60*1000L, new Callable<String>() { @Override public String call() throws Exception { log.info("内部线程:" + Thread.currentThread().getName()); return "WebAsyncTask!!!"; } }); result.onTimeout(new Callable<String>() { @Override public String call() throws Exception { // TODO Auto-generated method stub return "WebAsyncTask超时!!!"; } }); result.onCompletion(new Runnable() { @Override public void run() { //超时后 也会执行此方法 log.info("WebAsyncTask执行结束"); } }); return result; }
控制台输出:
2018-08-15 23:55:02.568 INFO 2864 --- [nio-8080-exec-1] c.l.l.s.controller.SpringController : 外部线程:http-nio-8080-exec-1 2018-08-15 23:55:02.587 INFO 2864 --- [ oKong-1] c.l.l.s.controller.SpringController : 内部线程:oKong-1 2018-08-15 23:55:02.615 INFO 2864 --- [nio-8080-exec-2] c.l.l.s.controller.SpringController : WebAsyncTask执行结束
本章节主要是讲解了
异步请求
的使用及相关配置,如超时,异常等处理。设置异步请求时,记得不要忘记设置超时时间。异步请求
只是提升了服务的吞吐量,提升单位时间内处理的请求数,并不会加快处理效率的,这点须要注意。。下一章节,讲讲使用@Async
进行异步调用相关知识。
目前互联网上不少大佬都有
SpringBoot
系列教程,若有雷同,请多多包涵了。本文是做者在电脑前一字一句敲的,每一步都是本身实践的。若文中有所错误之处,还望提出,谢谢。
499452441
lqdevOps
我的博客:http://blog.lqdev.cn
完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-20
原文地址:http://blog.lqdev.cn/2018/08/16/springboot/chapter-twenty/