在Servlet3.0就引入了异步请求的支持,但是在实际的业务开发中,可能用过这个特性的童鞋并不多?
本篇博文作为异步请求的扫盲和使用教程,将包含以下知识点
异步对于我们而言,应该属于经常可以听到的词汇了,在实际的开发中多多少少都会用到,那么什么是异步请求呢
一个正常调用,吭哧吭哧执行完毕之后直接返回,这个叫同步;
接收到调用,自己不干,新开一个线程来做,主线程自己则去干其他的事情,等后台线程吭哧吭哧的跑完之后,主线程再返回结果,这个就叫异步
我们这里讲到的异步请求,主要是针对web请求而言,后端响应请求的一种手段,同步/异步对于前端而言是无感知、无区别的
同步请求,后端接收到请求之后,直接在处理请求线程中,执行业务逻辑,并返回
异步请求,后端接收到请求之后,新开一个线程,来执行业务逻辑,释放请求线程,避免请求线程被大量耗时的请求沾满,导致服务不可用
通过上面两张图,可以知道异步请求的最主要特点
从特点出发,也可以很容易看出异步请求,更适用于耗时的请求,快速的释放请求处理线程,避免web容器的请求线程被打满,导致服务不可用
举一个稍微极端一点的例子,比如我以前做过的一个多媒体服务,提供图片、音视频的编辑,这些服务接口有同步返回结果的也有异步返回结果的;同步返回结果的接口有快有慢,大部分耗时可能 <10ms
,而有部分接口耗时则在几十甚至上百
这种场景下,耗时的接口就可以考虑用异步请求的方式来支持了,避免占用过多的请求处理线程,影响其他的服务
接下来介绍四种异步请求的使用姿势,原理一致,只是使用的场景稍有不同
在Servlet3.0+之后就支持了异步请求,第一种方式比较原始,相当于直接借助Servlet的规范来实现,当然下面的case并不是直接创建一个servlet,而是借助 AsyncContext
来实现
@RestController @RequestMapping(path = "servlet") public class ServletRest { @GetMapping(path = "get") public void get(HttpServletRequest request) { AsyncContext asyncContext = request.startAsync(); asyncContext.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("操作完成:" + Thread.currentThread().getName()); } @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { System.out.println("超时返回!!!"); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("超时了!!!!"); } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("出现了m某些异常"); asyncEvent.getThrowable().printStackTrace(); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("出现了某些异常哦!!!!"); } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("开始执行"); } }); asyncContext.setTimeout(3000L); asyncContext.start(new Runnable() { @Override public void run() { try { Thread.sleep(Long.parseLong(request.getParameter("sleep"))); System.out.println("内部线程:" + Thread.currentThread().getName()); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("异步返回!"); asyncContext.getResponse().getWriter().flush(); // 异步完成,释放 asyncContext.complete(); } catch (Exception e) { e.printStackTrace(); } } }); System.out.println("主线程over!!! " + Thread.currentThread().getName()); } }
完整的实现如上,简单的来看一下一般步骤
javax.servlet.ServletRequest#startAsync()
获取 AsyncContext
asyncContext.addListener(AsyncListener)
(这个是可选的)
asyncContext.setTimeout(3000L)
(可选) asyncContext.start(Runnable)
相比较于上面的复杂的示例,SpringMVC可以非常easy的实现,直接返回一个 Callable
即可
@RestController @RequestMapping(path = "call") public class CallableRest { @GetMapping(path = "get") public Callable<String> get() { Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { System.out.println("do some thing"); Thread.sleep(1000); System.out.println("执行完毕,返回!!!"); return "over!"; } }; return callable; } @GetMapping(path = "exception") public Callable<String> exception() { Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { System.out.println("do some thing"); Thread.sleep(1000); System.out.println("出现异常,返回!!!"); throw new RuntimeException("some error!"); } }; return callable; } }
请注意上面的两种case,一个正常返回,一个业务执行过程中,抛出来异常
分别请求,输出如下
# http://localhost:8080/call/get do some thing 执行完毕,返回!!!
异常请求: http://localhost:8080/call/exception
do some thing 出现异常,返回!!! 2020-03-29 16:12:06.014 ERROR 24084 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception java.lang.RuntimeException: some error! at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:40) ~[classes/:na] at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:34) ~[classes/:na] at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:328) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_171] at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) ~[na:1.8.0_171] at java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:1.8.0_171] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_171] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_171] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]
callable的方式,非常直观简单,但是我们经常关注的超时+异常的处理却不太好,这个时候我们可以用 WebAsyncTask
,实现姿势也很简单,包装一下 callable
,然后设置各种回调事件即可
@RestController @RequestMapping(path = "task") public class WebAysncTaskRest { @GetMapping(path = "get") public WebAsyncTask<String> get(long sleep, boolean error) { Callable<String> callable = () -> { System.out.println("do some thing"); Thread.sleep(sleep); if (error) { System.out.println("出现异常,返回!!!"); throw new RuntimeException("异常了!!!"); } return "hello world"; }; // 指定3s的超时 WebAsyncTask<String> webTask = new WebAsyncTask<>(3000, callable); webTask.onCompletion(() -> System.out.println("over!!!")); webTask.onTimeout(() -> { System.out.println("超时了"); return "超时返回!!!"; }); webTask.onError(() -> { System.out.println("出现异常了!!!"); return "异常返回"; }); return webTask; } }
DeferredResult
与 WebAsyncTask
最大的区别就是前者不确定什么时候会返回结果,
DeferredResult
的这个特点,可以用来做实现很多有意思的东西,如后面将介绍的 SseEmitter
就用到了它
下面给出一个实例
@RestController @RequestMapping(path = "defer") public class DeferredResultRest { private Map<String, DeferredResult> cache = new ConcurrentHashMap<>(); @GetMapping(path = "get") public DeferredResult<String> get(String id) { DeferredResult<String> res = new DeferredResult<>(); cache.put(id, res); res.onCompletion(new Runnable() { @Override public void run() { System.out.println("over!"); } }); return res; } @GetMapping(path = "pub") public String publish(String id, String content) { DeferredResult<String> res = cache.get(id); if (res == null) { return "no consumer!"; } res.setResult(content); return "over!"; } }
在上面的实例中,用户如果先访问 http://localhost:8080/defer/get?id=yihuihui
,不会立马有结果,直到用户再次访问 http://localhost:8080/defer/pub?id=yihuihui&content=哈哈
时,前面的请求才会有结果返回
那么这个可以设置超时么,如果一直把前端挂住,貌似也不太合适吧
new DeferredResult<>(3000L)
@Configuration @EnableWebMvc public class WebConf implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // 超时时间设置为60s configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(10)); } }
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
打赏 如果觉得我的文章对您有帮助,请随意打赏。