【SpringBoot WEB系列】异步请求知识点与使用姿势小结

摘要:
在Servlet 3.0中引入了异步请求知识点和姿势总结,以支持异步请求。然而,在实际的业务发展中,使用过这一功能的童鞋可能不多?“);asyncContext.getResponse().setCharactEncoding;asyncContext.getResponse().setContentType;asyncContext.getResresponse().getWriter().println(“超时!!!”);}@OverridepublicvotionStartAsynchrowsIOException{System.out.println;}});asyncContext。setTimeout;AsyncContext。start(newRunnable(){@Overridepublicvoidrun()){try{Thread.sleep(Long.parseLong(request.getParameter(“sleep”)));System.out.println(“内部线程:”+Thread.currentThread().getName());asyncContext.getResponse().setCharacterEncoding(“utf-8”);async Context.getResponse().setContentType(“异步返回!

【SpringBoot WEB系列】异步请求知识点与使用姿势小结第1张

【SpringBoot WEB系列】异步请求知识点与使用姿势小结

在 Servlet3.0 就引入了异步请求的支持,但是在实际的业务开发中,可能用过这个特性的童鞋并不多?

本篇博文作为异步请求的扫盲和使用教程,将包含以下知识点

  • 什么是异步请求,有什么特点,适用场景
  • 四种使用姿势:
    • AsyncContext 方式
    • Callable
    • WebAsyncTask
    • DeferredResult

I. 异步请求

异步对于我们而言,应该属于经常可以听到的词汇了,在实际的开发中多多少少都会用到,那么什么是异步请求呢

1. 异步请求描述

先介绍一下同步与异步:

一个正常调用,吭哧吭哧执行完毕之后直接返回,这个叫同步;

接收到调用,自己不干,新开一个线程来做,主线程自己则去干其他的事情,等后台线程吭哧吭哧的跑完之后,主线程再返回结果,这个就叫异步

异步请求:

我们这里讲到的异步请求,主要是针对 web 请求而言,后端响应请求的一种手段,同步/异步对于前端而言是无感知、无区别的

同步请求,后端接收到请求之后,直接在处理请求线程中,执行业务逻辑,并返回

来源于网络

异步请求,后端接收到请求之后,新开一个线程,来执行业务逻辑,释放请求线程,避免请求线程被大量耗时的请求沾满,导致服务不可用

来源于网络

2. 特点

通过上面两张图,可以知道异步请求的最主要特点

  • 业务线程,处理请求逻辑
  • 请求处理线程立即释放,通过回调处理线程返回结果

3. 场景分析

从特点出发,也可以很容易看出异步请求,更适用于耗时的请求,快速的释放请求处理线程,避免 web 容器的请求线程被打满,导致服务不可用

举一个稍微极端一点的例子,比如我以前做过的一个多媒体服务,提供图片、音视频的编辑,这些服务接口有同步返回结果的也有异步返回结果的;同步返回结果的接口有快有慢,大部分耗时可能<10ms,而有部分接口耗时则在几十甚至上百

这种场景下,耗时的接口就可以考虑用异步请求的方式来支持了,避免占用过多的请求处理线程,影响其他的服务

II. 使用姿势

接下来介绍四种异步请求的使用姿势,原理一致,只是使用的场景稍有不同

1. AsyncContext

在 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)

2. Callable

相比较于上面的复杂的示例,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

【SpringBoot WEB系列】异步请求知识点与使用姿势小结第4张

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]

3. WebAsyncTask

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

4. DeferredResult

DeferredResultWebAsyncTask最大的区别就是前者不确定什么时候会返回结果,

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=哈哈时,前面的请求才会有结果返回

【SpringBoot WEB系列】异步请求知识点与使用姿势小结第5张

那么这个可以设置超时么,如果一直把前端挂住,貌似也不太合适吧

  • 在构造方法中指定超时时间: new DeferredResult<>(3000L)
  • 设置全局的默认超时时间
@Configuration
@EnableWebMvc
public class WebConf implements WebMvcConfigurer {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 超时时间设置为60s
        configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(10));
    }
}

II. 其他

0. 项目

相关博文

系列博文

源码

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

免责声明:文章转载自《【SpringBoot WEB系列】异步请求知识点与使用姿势小结》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇CyUSB_Linux 调试笔记使用 ejs 渲染数据库中的数据下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

【转】Python中的GIL、多进程和多线程

转自:http://lesliezhu.github.io/public/2015-04-20-python-multi-process-thread.html 目录 1. GIL(Global Interpretor Lock,全局解释器锁) 2. threading 2.1. 创建线程 2.2. 使用线程队列 3. dummy_threadi...

互动直播中的前端技术——即时通讯

前言 在疫情期间,上班族开启了远程办公,体验了各种远程办公软件。老师做起了主播,学生们感受到了被钉钉支配的恐惧,歌手们开启了在线演唱会,许多综艺节目也变成了在线直播。在这全民互动直播的时期,我们来聊聊互动直播中的即时通讯技术在前端中的使用。 即时通讯技术 即时通讯(Instant Messaging,简称IM)是一个实时通信系统,允许两人或多人使用网络实时...

Java Web开发之详解JSP

JSP作为Java Web开发中比较重要的技术,一般当作视图(View)的技术所使用,即用来展现页面。Servlet由于其本身不适合作为表现层技术,所以一般被当作控制器(Controller)所使用,而JavaBean作为模型(Model)层使用。这就是经典的MVC模型。 Servlet和JSP的关系上篇博客已经讲过了,并演示了一个相当简单的例子。在具体讲...

python中的轻量级定时任务调度库:schedule

提到定时任务调度的时候,相信很多人会想到芹菜celery,要么就写个脚本塞到crontab中。不过,一个小的定时脚本,要用celery的话太“重”了。所以,我找到了一个轻量级的定时任务调度的库:schedule。 schedule库是一个轻量级的定时任务方案,优势是使用简单,也不需要做什么配置;缺点是无法动态添加任务,也无法将任务持久化。   库的安装还是...

WPF 同一窗口内的多线程 UI(VisualTarget)

WPF 的 UI 逻辑只在同一个线程中,这是学习 WPF 开发中大家几乎都会学习到的经验。如果希望做不同线程的 UI,大家也会想到使用另一个窗口来实现,让每个窗口拥有自己的 UI 线程。然而,就不能让同一个窗口内部使用多个 UI 线程吗? 答案其实是——可以的!使用 VisualTarget 即可。 阅读本文将收获一份对 VisualTarget 的解读以...

Windows环境搭建Web自动化测试框架Watir

Windows环境搭建Web自动化测试框架Watir 一、前言     Web自动化测试一直是一个比较迫切的问题,对于现在web开发的敏捷开发,却没有相对应的敏捷测试,故开此主题,一边研究,一边将Web自动化测试应用于工作中,进而形成能够独立成章的博文,希望能够为国内web自动化测试的发展做一点绵薄的贡献吧,笑~ 二、Watir搭建流程...