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

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

image.png


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


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


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


I. 异步请求



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


1. 异步请求描述


先介绍一下同步与异步:


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


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


异步请求:


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


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

image.png


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


image.png


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


image.png

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

image.png


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


  • 在构造方法中指定超时时间: new DeferredResult<>(3000L)
  • 设置全局的默认超时时间


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



相关文章
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
659 4
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
1139 7
Spring Boot 入门:简化 Java Web 开发的强大工具
|
XML Java 网络架构
使用 Spring Boot 公开 SOAP Web 服务端点:详细指南
使用 Spring Boot 公开 SOAP Web 服务端点:详细指南
1938 0
|
监控 Java Maven
springboot学习二:springboot 初创建 web 项目、修改banner、热部署插件、切换运行环境、springboot参数配置,打包项目并测试成功
这篇文章介绍了如何快速创建Spring Boot项目,包括项目的初始化、结构、打包部署、修改启动Banner、热部署、环境切换和参数配置等基础操作。
2446 0
|
NoSQL Java 数据库连接
springBoot:整合其他框架&condition&切换web配置 (五)
本文档介绍了如何在Spring Boot项目中整合JUnit、Redis和MyBatis等框架,并提供了相应的依赖配置示例。同时,还展示了如何通过条件注解实现Bean的条件创建,以及如何切换Web服务器配置,从默认的Tomcat切换到Jetty。
262 0
|
Java Spring Apache
Spring Boot邂逅Apache Wicket:一次意想不到的完美邂逅,竟让Web开发变得如此简单?
【8月更文挑战第31天】Apache Wicket与Spring Boot的集成提供了近乎无缝的开发体验。Wicket以其简洁的API和强大的组件化设计著称,而Spring Boot则以开箱即用的便捷性赢得开发者青睐。本文将指导你如何在Spring Boot项目中引入Wicket,通过简单的步骤完成集成配置。首先,创建一个新的Spring Boot项目并在`pom.xml`中添加Wicket相关依赖。
578 0
|
Java Spring 开发者
Java Web开发新潮流:Vaadin与Spring Boot强强联手,打造高效便捷的应用体验!
【8月更文挑战第31天】《Vaadin与Spring Boot集成:最佳实践指南》介绍了如何结合Vaadin和Spring Boot的优势进行高效Java Web开发。文章首先概述了集成的基本步骤,包括引入依赖和配置自动功能,然后通过示例展示了如何创建和使用Vaadin组件。相较于传统框架,这种集成方式简化了配置、提升了开发效率并便于部署。尽管可能存在性能和学习曲线方面的挑战,但合理的框架组合能显著提升应用开发的质量和速度。
608 0
|
9月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
632 4
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
JavaScript 前端开发 API
鸿蒙5开发宝藏案例分享---Web加载时延优化解析
本文深入解析了鸿蒙开发中Web加载完成时延的优化技巧,结合官方案例与实际代码,助你提升性能。核心内容包括:使用DevEco Profiler和DevTools定位瓶颈、四大优化方向(资源合并、接口预取、图片懒加载、任务拆解)及高频手段总结。同时提供性能优化黄金准则,如首屏资源控制在300KB内、关键接口响应≤200ms等,帮助开发者实现丝般流畅体验。