异步编程 - 10 Web Servlet的异步非阻塞处理

news2024/11/30 8:32:31

文章目录

  • OverView
  • Servlet概述
  • Servlet 3.0提供的异步处理能力
  • Servlet 3.1提供的非阻塞IO能力
  • Spring Web MVC的异步处理能力
    • 基于DeferredResult的异步处理
    • 基于Callable实现异步处理
  • 小结

在这里插入图片描述


OverView

我们这里主要讨论Servlet3.0规范前的同步处理模型和缺点,Servlet3.0规范提供的异步处理能力与Servlet3.1规范提供的非阻塞IO能力,以及Spring MVC中提供的异步处理能力。


Servlet概述

Servlet是一个基于Java技术的Web组件,由容器管理,生成动态内容。像其他基于Java技术的组件一样,Servlet是与平台无关的Java类格式,它们被编译为与具体平台无关的字节码,可以被基于Java技术的Web Server动态加载并运行。容器(有时称为Servlet引擎)是Web服务器为支持Servlet功能扩展的部分。客户端通过Servlet容器实现请求/应答模型与Servlet交互。

Servlet容器是Web Server或Application Server的一部分,其提供基于请求/响应模型的网络服务,解码基于MIME的请求,并且格式化基于MIME的响应。Servlet容器也包含了管理Servlet生命周期的能力,Servlet是运行在Servlet容器内的。Servlet容器可以嵌入宿主的Web Server中,或者通过Web Server的本地扩展API单独作为附加组件安装。Servelt容器也可能内嵌或安装到包含Web功能的Application Server中。

所有Servlet容器必须支持基于HTTP协议的请求/响应模型,并且可以选择性支持基于HTTPS协议的请求/应答模型。容器必须实现的HTTP协议版本包含HTTP/1.0和HTTP/1.1。

Servlet容器应该使Servlet执行在一个安全限制的环境中。在Java平台标准版(J2SE,v.1.3或更高)或者Java平台企业版(Java EE,v.1.3或更高)的环境下,这些限制应该被放置在Java平台定义的安全许可架构中。比如,为了保证容器的其他组件不受负面影响,高端的Application Server可能会限制Thread对象的创建。常见的比较经典的Servlet容器实现有Tomcat和Jetty。


Servlet 3.0提供的异步处理能力

Web应用程序中提供异步处理最基本的动机是处理需要很长时间才能完成的请求。这些比较耗时的请求可能是一个缓慢的数据库查询,可能是对外部REST API的调用,也可能是其他一些耗时的I/O操作。这种耗时较长的请求可能会快速耗尽Servlet容器线程池中的线程并影响应用的可伸缩性。

在Servlet3.0规范前,Servlet容器对Servlet都是以每个请求对应一个线程这种1:1的模式进行处理的,如图所示(这里Servlet容器固定使用Tomcat来进行讲解)。

【Servlet同步处理模型】
在这里插入图片描述

由图可知,每当用户发起一个请求时,Tomcat容器就会分配一个线程来运行具体的Servlet。在这种模式下,当在Servlet内执行比较耗时的操作,比如访问了数据库、同步调用了远程rpc,或者进行了比较耗时的计算时,当前分配给Servlet执行任务的线程会一直被该Servlet持有,不能及时释放掉后供其他请求使用,而Tomcat内的容器线程池内线程是有限的,当线程池内线程用尽后就不能再对新来的请求进行及时处理了,所以这大大限制了服务器能提供的并发请求数量。


为了解决上述问题,在Servlet 3.0规范中引入了异步处理请求的能力,处理线程可以及时返回容器并执行其他任务,一个典型的异步处理的事件流程如下:

  • 请求被Servlet容器接收,然后从Servlet容器(例如Tomcat)中获取一个线程来执行,请求被流转到Filter链进行处理,然后查找具体的Servlet进行处理。

  • Servlet具体处理请求参数或者请求内容来决定请求的性质。

  • Servlet内使用“req.startAsync();”开启异步处理,返回异步处理上下文Async-Context对象,然后开启异步线程(可以是Tomcat容器中的其他线程,也可以是业务自己创建的线程)对请求进行具体处理(这可能会发起一个远程rpc调用或者一个数据库请求);开启异步线程后,当前Servlet就返回了(分配给其执行的容器线程也就释放了),并且不对请求方产生响应结果。

  • 异步线程对请求处理完毕后,会通过持有的AsyncContext对象把结果写回请求方。

如下所示,具体处理请求响应的逻辑已经不再是Servlet调用线程来做了,Servlet内开启异步处理后会立刻释放Servlet容器线程,具体对请求进行处理与响应的是业务线程池中的线程。

【Servlet异步处理模型】

在这里插入图片描述

一个典型的Servlet,当我们访问http://127.0.0.1:8080/test时,Tomcat容器会接收该请求,然后从容器线程池中获取一个线程来激活容器的Filter链,然后把请求路由到MyServlet,此时MyServlet的Service方法会被调用,方法内线程休眠3s用来模拟MyServlet中的耗时操作,接着代码3把响应结果设置到响应对象,该MyServlet就退出了;由于MyServlet内是同步执行,所以从Filter链的执行到MyServlet的Service内代码执行都是用同一个Tomcat容器内的线程。下面我们将上面代码改造为异步处理:

//1.开启异步支持
@WebServlet(urlPatterns = "/test", asyncSupported = true)
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 2.开启异步,获取异步上下文
        System.out.println("---begin serlvet----");
        final AsyncContext asyncContext = req.startAsync();

        // 3.提交异步任务
        asyncContext.start(new Runnable() {

            @Override
            public void run() {
                try {
                    // 3.1执行业务逻辑
                    System.out.println("---async res begin----");
                    Thread.sleep(3000);

                    // 3.2设置响应结果
                    resp.setContentType("text/html");
                    PrintWriter out = asyncContext.getResponse().getWriter();
                    out.println("<html>");
                    out.println("<head>");
                    out.println("<title>Hello World</title>");
                    out.println("</head>");
                    out.println("<body>");
                    out.println("<h1>welcome this is my servlet1!!!</h1>");
                    out.println("</body>");
                    out.println("</html>");
                    System.out.println("---async res end----");

                } catch (Exception e) {
                    System.out.println(e.getLocalizedMessage());
                } finally {
                    // 3.3异步完成通知
                    asyncContext.complete();
                }
            }
        });

        // 4.运行结束,即将释放容器线程
        System.out.println("---end servlet----");
    }
}

由上述代码可知:

  • 如上代码1,这里使用注解@WebServlet来标识MyServlet是一个Servlet,其中asyncSupported为true代表要异步执行,然后框架就会知道该Servlet要启动异步处理功能。

  • MyServlet的Service方法中代码2调用HttpServletRequest的startAsync()方法开启异步调用,该方法返回一个AsyncContext,其中保存了与请求/响应相关的上下文信息。

  • 代码3调用AsyncContext的start方法并传递一个任务,该方法会马上返回,然后代码4打印后,当前Servlet就退出了,其调用线程(容器线程)也被释放。

  • 代码3提交异步任务后,异步任务的执行还是由容器中的其他线程来具体执行的,这里异步任务中代码3.1休眠3s是为了模拟耗时操作。代码3.2从asyncContext中获取响应对象,并把响应结果写入响应对象。代码3.3则调用asyncContext.complete()标识异步任务执行完毕。

上面代码的异步执行虽然及时释放了调用Servlet时执行的容器线程,但是异步处理还是使用了容器中的其他线程,其实我们可以使用自己的线程池来进行任务的异步处理,将上面的代码修改为如下形式:

//1.开启异步支持
@WebServlet(urlPatterns = "/test", asyncSupported = true)
public class MyServlet extends HttpServlet {
    // 0自定义线程池
    private final static int AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVALIABLE_PROCESSORS,
            AVALIABLE_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(5),
            new ThreadPoolExecutor.CallerRunsPolicy());

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 2.开启异步,获取异步上下文
        System.out.println("---begin servlet----");
        final AsyncContext asyncContext = req.startAsync();

        // 3.提交异步任务
        POOL_EXECUTOR.execute(new Runnable() {

            @Override
            public void run() {
                try {
                    // 3.1执行业务逻辑
                    System.out.println("---async res begin----");
                    Thread.sleep(3000);

                    // 3.2设置响应结果
                    resp.setContentType("text/html");
                    PrintWriter out = asyncContext.getResponse().getWriter();
                    out.println("<html>");
                    out.println("<head>");
                    out.println("<title>Hello World</title>");
                    out.println("</head>");
                    out.println("<body>");
                    out.println("<h1>welcome this is my servlet1!!!</h1>");
                    out.println("</body>");
                    out.println("</html>");
                    System.out.println("---async res end----");

                } catch (Exception e) {
                    System.out.println(e.getLocalizedMessage());
                } finally {
                    // 3.3异步完成通知
                    asyncContext.complete();
                }
            }
        });

        // 4.运行结束,即将释放容器线程
        System.out.println("---end servlet----");
    }
}

通过如上代码0,我们创建了自己的JVM内全局的线程池,然后代码3把异步任务提交到了我们的线程池来执行,这时候整个处理流程是:Tomcat容器收到请求后,从容器中获取一个线程来执行Filter链,接着把请求同步转发到MyServlet的service方法来执行,然后代码3把具体请求处理的逻辑异步切换到我们业务线程池来执行,此时MyServlet就返回了,并释放容器线程。

在Servlet 3.0中,还为异步处理提供了一个监听器,用户可以实现AsyncListener接口来对异步执行结果进行响应。比如基于上面代码,我们添加AsyncListener接口后代码如下:

/1.开启异步支持
@WebServlet(urlPatterns = "/test", asyncSupported = true)
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 2.开启异步,获取异步上下文
        System.out.println("---begin servlet----");
        final AsyncContext asyncContext = req.startAsync();

        //添加事件监听器
        asyncContext.addListener(new AsyncListener() {

            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                System.out.println("onTimeout" );
            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
                System.out.println("onStartAsync" );

            }

            @Override
            public void onError(AsyncEvent event) throws IOException {
                System.out.println("onError" );

            }

            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                System.out.println("onComplete");
            }
        });
        
        // 3.提交异步任务
        asyncContext.start(new Runnable() {

            @Override
            public void run() {
                try {
                    ....
                } catch (Exception e) {
                    System.out.println(e.getLocalizedMessage());
                } finally {
                    // 3.3异步完成通知
                    asyncContext.complete();
                }
            }
        });

        // 4.运行结束,即将释放容器线程
        System.out.println("---end servlet----");
    }
}

通过上述代码,我们可以对异步处理的结果进行处理。


Servlet 3.1提供的非阻塞IO能力

虽然Servlet 3.0规范让Servlet的执行变为了异步,但是其IO还是阻塞式的。IO阻塞是说,在Servlet处理请求时,从ServletInputStream中读取请求体时是阻塞的。而我们想要的是,当数据就绪时通知我们去读取就可以了,因为这可以避免占用Servlet容器线程或者业务线程来进行阻塞读取。下面我们通过代码直观看看什么是阻塞IO:

@WebServlet(urlPatterns = "/testSyncReadBody", asyncSupported = true)
public class MyServletSyncReadBody extends HttpServlet {

    // 1自定义线程池
    private final static int AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVALIABLE_PROCESSORS,
            AVALIABLE_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(5),
            new ThreadPoolExecutor.CallerRunsPolicy());

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 2.开启异步,获取异步上下文
        System.out.println("---begin servlet----");
        final AsyncContext asyncContext = req.startAsync();

        // 3.提交异步任务
        POOL_EXECUTOR.execute(new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println("---async res begin----");
                    // 3.1读取请求体
                    long start = System.currentTimeMillis();
                    final ServletInputStream inputStream = asyncContext.getRequest().getInputStream();
                    try {
                        byte buffer[] = new byte[1 * 1024];
                        int readBytes = 0;
                        int total = 0;

                        while ((readBytes = inputStream.read(buffer)) > 0) {
                            total += readBytes;
                        }

                        long cost = System.currentTimeMillis() - start;
                        System.out
                                .println(Thread.currentThread().getName() + 
" Read: " + total + " bytes,costs:" + cost);

                    } catch (IOException ex) {
                        System.out.println(ex.getLocalizedMessage());
                    }

                    // 3.2执行业务逻辑
                    Thread.sleep(3000);

                    // 3.3设置响应结果
                    resp.setContentType("text/html");
                    PrintWriter out = asyncContext.getResponse().getWriter();
                    out.println("<html>");
                    out.println("<head>");
                    out.println("<title>Hello World</title>");
                    out.println("</head>");
                    out.println("<body>");
                    out.println("<h1>welcome this is my servlet1!!!</h1>");
                    out.println("</body>");
                    out.println("</html>");
                    System.out.println("---async res end----");

                } catch (Exception e) {
                    System.out.println(e.getLocalizedMessage());
                } finally {
                    // 3.3异步完成通知
                    asyncContext.complete();
                }
            }
        });

        // 4.运行结束,即将释放容器线程
        System.out.println("---end servlet----");
    }
}

如上代码3.1从ServletInputStream中读取http请求体的内容(需要注意的是,http header的内容不在ServletInputStream中),其中使用循环来读取内容,并且统计读取数据的数量。

而ServletInputStream中并非一开始就有数据,所以当我们的业务线程池POOL_EXECUTOR中的线程调用inputStream.read方法时是会被阻塞的,等内核接收到请求方发来的数据后,该方法才会返回,而这之前POOL_EXECUTOR中的线程会一直被阻塞,这就是我们所说的阻塞IO。阻塞IO会消耗宝贵的线程。

下面借助下图来进一步解释。

【Servlet同步阻塞IO处理】
在这里插入图片描述

如图所示,Servlet容器接收请求后会从容器线程池获取一个线程来执行具体Servlet的Service方法,由Service方法调用StartAsync把请求处理切换到业务线程池内的线程,如果业务线程内调用了ServletInputStream的read方法读取http的请求体内容,则业务线程会以阻塞方式读取IO数据(因为数据还没就绪)。

这里的问题是,当数据还没就绪就分配了一个业务线程来阻塞等待数据就绪,造成资源浪费。下面我们看看Servlet 3.1是如何让数据就绪时才分配业务线程来进数据读取,做到需要时(数据就绪时)才分配的。

在Servlet3.1规范中提供了非阻塞IO处理方式:Web容器中的非阻塞请求处理有助于增加Web容器可同时处理请求的连接数量。Servlet容器的非阻塞IO允许开发人员在数据可用时读取数据或在数据可写时写数据。非阻塞IO对在Servlet和Filter中的异步请求处理有效,否则,当调用ServletInputStream.setReadListener或Servlet OutputStream.setWriteListener方法时将抛出IllegalStateException。基于内核的能力,Servlet3.1允许我们在ServletInputStream上通过函数setReadListener注册一个监听器,该监听器在发现内核有数据时才会进行回调处理函数。上面代码注册监听器后的形式如下:

@WebServlet(urlPatterns = "/testaSyncReadBody", asyncSupported = true)
public class MyServletaSyncReadBody extends HttpServlet {

    // 1.自定义线程池
    private final static int AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVALIABLE_PROCESSORS,
            AVALIABLE_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(5),
            new ThreadPoolExecutor.CallerRunsPolicy());

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 2.开启异步,获取异步上下文
        System.out.println("---begin serlvet----");
        final AsyncContext asyncContext = req.startAsync();

        // 3.设置数据就绪监听器
        final ServletInputStream inputStream = req.getInputStream();
        inputStream.setReadListener(new ReadListener() {

            @Override
            public void onError(Throwable throwable) {
                System.out.println("onError:" + throwable.getLocalizedMessage());
            }

            /**
             * 当数据就绪时,通知我们来读取
             */
            @Override
            public void onDataAvailable() throws IOException {
                try {
                    // 3.1读取请求体
                    long start = System.currentTimeMillis();
                    final ServletInputStream inputStream = asyncContext.getRequest().getInputStream();
                    try {
                        byte buffer[] = new byte[1 * 1024];
                        int readBytes = 0;
                        while (inputStream.isReady() && !inputStream.isFinished()) {
                            readBytes += inputStream.read(buffer);

                        }

                        System.out.println(Thread.currentThread().getName() + " Read: " + readBytes);

                    } catch (IOException ex) {
                        System.out.println(ex.getLocalizedMessage());
                    }

                } catch (Exception e) {
                    System.out.println(e.getLocalizedMessage());
                } finally {
                }
            }

            /**
             * 当请求体的数据全部被读取完毕后,通知我们进行业务处理
             */
            @Override
            public void onAllDataRead() throws IOException {

                // 3.2提交异步任务
                POOL_EXECUTOR.execute(new Runnable() {

                    @Override
                    public void run() {
                        try {

                            System.out.println("---async res begin----");
                            // 3.2.1执行业务逻辑
                            Thread.sleep(3000);

                            // 3.2.2设置响应结果
                            resp.setContentType("text/html");
                            PrintWriter out = asyncContext.getResponse().getWriter();
                            out.println("<html>");
                            out.println("<head>");
                            out.println("<title>Hello World</title>");
                            out.println("</head>");
                            out.println("<body>");
                            out.println("<h1>welcome this is my servlet1!!!</h1>");
                            out.println("</body>");
                            out.println("</html>");
                            System.out.println("---async res end----");

                        } catch (Exception e) {
                            System.out.println(e.getLocalizedMessage());
                        } finally {
                            // 3.2.3异步完成通知
                            asyncContext.complete();
                        }
                    }
                });
            }
        });

        // 4.运行结束,即将释放容器线程
        System.out.println("---end serlvet----");
    }
}
  • 代码3设置了一个ReadListener到ServletInputStream流,当内核发现有数据已经就绪时,就会回调其onDataAvailable方法,该方法内就可以马上读取数据。这里代码3.1通过inputStream.isReady()发现数据已经准备就绪后,就可以从中读取数据了。需要注意的是,这里的onDataAvailable是容器线程来执行的,只有在数据已经就绪时才调用容器线程来读取数据。

  • 另外,当请求体的数据全部读取完毕后才会调用onAllDataRead方法,该方法默认也是容器线程来执行的。这里我们使用代码3.2切换到业务线程池来执行。

下面我们结合下图来具体说明Servlet3.1中的ReadListener是如何高效利用线程的。

【Servlet非阻塞IO处理】
在这里插入图片描述

如上图所示,Servlet容器接收请求后会从容器线程池获取一个线程来执行具体Servlet的Service方法,Service方法内调用StartAsync开启异步处理,然后通过setReadListener注册一个ReadListener到ServletInputStream,最后释放容器线程。

当内核发现TCP接收缓存有数据时,会回调注册的ReadListener的onData Available方法,这时使用的是容器线程,但是我们可以选择是否在onData Available方法内开启异步线程来对就绪数据进行读取,以便及时释放容器线程。

当发现http的请求体内容已经被读取完毕后,会调用onAllDataRead方法,在这个方法内我们使用业务线程池对请求进行处理,并把结果写回请求方。

结合上文可知,无论是容器线程还是业务线程,都不会出现阻塞IO的情况。因为当线程被分配来进行处理时,当前数据已经是就绪的,可以马上进行读取,故不会造成线程的阻塞。

需要注意的是,Servlet3.1不仅增加了可以非阻塞读取请求体的ReadListener,还增加了可以避免阻塞写的WriteListener接口,在ServletOutputStream上可以通过set-WriteListener进行设置。当一个WriteListener注册到ServletOutputStream后,当可以写数据时onWritePossible()方法将被容器首次调用,这里我们不再展开讨论。


Spring Web MVC的异步处理能力

Spring Web MVC是基于Servlet API构建的Web框架,从一开始就包含在Spring Framework中。正式名称Spring Web MVC来自其源模块(spring-webmvc)的名称,但它通常被称为Spring MVC。Spring MVC的出现让我们不用再聚焦在具体的Servlet上,而是直接编写与业务相关的controller。

与许多其他Web框架一样,Spring MVC围绕前端控制器模式(Front Controller Pattern)设计,其中中央Servlet DispatcherServlet为请求处理提供共享的路由算法,负责对请求进行路由分派,实际的请求处理工作由可配置的委托组件执行。该模型非常灵活,支持多种工作流程。

DispatcherServlet与任何Servlet一样,需要使用Java配置或webxml根据Servlet规范进行声明和映射。反过来,DispatcherServlet使用Spring配置来发现请求映射、视图解析、异常处理等所需的委托组件。

Spring MVC与前面讲解的Servlet 3.0异步请求处理有很深的集成:

  • DeferredResult和Callable作为controller方法中的返回值,并为单个异步返回值提供基本支持。

  • controller可以流式传输多个值,包括SSE和原始数据。

  • controller可以使用反应式客户端并返回反应式类型,以进行反应式处理。

Spring MVC内部通过调用request.startAsync()将ServletRequest置于异步模式。这样做的主要目的是Servlet(以及任何Filter)可以退出(同时容器线程也得到了释放),但响应保持打开状态,以便进行后续处理(异步处理完毕后使用其把结果写回请求方)。

Spring MVC内部对request.startAsync()的调用返回AsyncContext,可以使用它来进一步控制异步处理。例如,它提供了dispatch方法,类似于Servlet API中的forward,不同的是它允许应用程序在Servlet容器线程上恢复请求处理。


基于DeferredResult的异步处理

一旦在Servlet容器中启用了异步请求处理功能,controller方法就可以使用DeferredResult包装任何支持的方法返回值,如以下示例所示:

private static ThreadPoolExecutor BIZ_POOL = new ThreadPoolExecutor(8, 8, 1, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());

@PostMapping("/personDeferredResult")
DeferredResult<String> listPostDeferredResult() {

    DeferredResult<String> deferredResult = new DeferredResult<String>();
    BIZ_POOL.execute(new Runnable() {

        @Override
        public void run() {
            try {
                // 执行异步处理
                Thread.sleep(3000);

                // 设置结果
                deferredResult.setResult("ok");
            } catch (Exception e) {
                e.printStackTrace();
                deferredResult.setErrorResult("error");
            }

        }
    });
    return deferredResult;
}

上述代码我们创建了一个业务线程池BIZ_POOL,然后controller方法在listPost DeferredResult内创建了一个DeferredResult对象,接着向业务线程池BIZ_POOL提交我们的请求处理逻辑(其内部处理完毕后把结果设置到创建的DeferredResult),最后返回创建的DeferredResult对象。其整个处理过程如下:

  • 1)Tomcat容器接收路径为personDeferredResult的请求后,会分配一个容器线程来执行DispatcherServlet进行请求分派,请求被分到含有personDeferredResult路径的controller,然后执行listPostDeferredResult方法,该方法内创建了一个DeferredResult对象,然后把处理任务提交到了线程池进行处理,最后返回DeferredResult对象。

  • 2)Spring MVC内部在personDeferredResult方法返回后会保存DeferredResult对象到内存队列或者列表,然后会调用request.startAsync()开启异步处理,并且调用DeferredResult对象的setResultHandler方法,设置当异步结果产生后对结果进行重新路由的回调函数(逻辑在WebAsyncManager的startDeferredResultProcessing方法),接着释放分配给当前请求的容器线程,与此同时当前请求的DispatcherServlet和所有filters也执行完毕了,但是response流还是保持打开(因为任务执行结果还没写回)。

  • 3)最终在业务线程池中执行的异步任务会产生一个结果,该结果会被设置到DeferredResult对象,然后设置的回调函数会被调用,接着Spring MVC会分派请求结果回到Servlet容器继续完成处理,DispatcherServlet被再次调用,使用返回的异步结果继续进行处理,最终把响应结果写回请求方。


基于Callable实现异步处理

controller中的方法可以使用java.util.concurrent.Callable包装任何支持的返回类型,比如下面的例子:

@PostMapping("/personPostCallable")
Callable<String> listPostCall() {

    System.out.println("----begin personPostCallable----");
    return new Callable<String>() {
        public String call() throws Exception {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("----end personPostCallable----");
            return "test";
        }
    };
}

上述代码controller内的listPostCall方法返回了一个异步任务后就直接返回了,其中的异步任务会使用Spring框架内部的TaskExecutor线程池来执行,其整个执行流程如下:

1)Tomcat容器接收路径为personPostCallable的请求后,会分配一个容器线程来执行DispatcherServlet进行请求分派,接着请求被分到含有personPostCallable路径的controller,然后执行listPostCall方法,返回一个Callable对象。

2)Spring MVC内部在listPostCall方法返回后,调用request.startAsync()开启异步处理,然后提交Callable任务到内部线程池TaskExecutor(非容器线程)中进行异步执行(WebAsyncManager的startCallableProcessing方法内),接着释放分配给当前请求的容器线程,与此同时当前请求的DispatcherServlet和所有filters也执行完毕了,但是response流还是保持打开(因为Callable任务执行结果还没写回)。

3)最终在线程池TaskExecutor中执行的异步任务会产生一个结果,然后Spring MVC会分派请求结果回到Servlet容器继续完成处理,DispatcherServlet被再次调用,使用返回的异步结果继续进行处理,最终把响应结果写回请求方。

这种方式下异步执行默认使用内部的SimpleAsyncTaskExecutor,其对每个请求都会开启一个线程,并没有很好地复用线程,我们可以通过自定义自己的线程池来执行异步处理:

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(8);
        executor.setCorePoolSize(8);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setQueueCapacity(5); 
        executor.afterPropertiesSet();
        configurer.setTaskExecutor(executor);
    }
}

如上代码所示,我们向容器注入了一个WebMvcConfigurer的bean,然后在其configureAsyncSupport方法中创建了一个业务线程池,并把其设置到AsyncSupport Configurer中,则当容器进行异步处理时就会使用我们设置的线程池。


小结

我们这里总结了Servlet 3.0前的Servlet同步处理模型及其缺点,然后探讨了Servlet 3.0提供的异步处理能力与Servlet 3.1的非阻塞IO能力,以及Spring MVC中提供的异步处理能力

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/986662.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Amazon Aurora MySQL 和 Amazon RDS for MySQL 集群故障转移和只读实例扩容时间测试

01 测试背景 Amazon Aurora MySQL 是与 MySQL 兼容的关系数据库&#xff0c;专为云而打造&#xff0c;性能和可用性与商用数据库相当&#xff0c;成本只有其 1/10。 Amazon RDS for MySQL 让您能够在云中更轻松设置、操作和扩展 MySQL 部署。借助 Amazon RDS&#xff0c;您可以…

小白备战大厂算法笔试(三)——栈、队列、双向队列

文章目录 栈栈常用操作栈的实现基于链表的实现基于数组的实现 两种实现对比栈典型应用 队列队列常用操作队列实现基于链表的实现基于数组的实现 队列典型应用 双向队列双向队列常用操作双向队列实现基于双向链表的实现基于数组的实现 双向队列应用 栈 栈是一种遵循先入后出的逻…

Java从入门到精通-流程控制(二)

习题讲解&#xff1a; 上次我们给大家留了一些流程控制的问题&#xff0c;这次给大家分析讲解一下&#xff1a; 条件语句练习&#xff1a; 1.编写Java程序&#xff0c;用于接受用户输入的数字&#xff0c;然后判断它是偶数还是奇数&#xff0c;并输出相应的消息。 import ja…

电表采集器是如何接线的?

随着社会的进步和科技的发展&#xff0c;智能化和自动化已经成为了各个行业的发展趋势。在电力系统领域&#xff0c;电表采集器的应用越来越广泛&#xff0c;它实现了电能数据的远程采集、传输和分析&#xff0c;为电力系统的稳定运行提供了重要的数据支持。那么&#xff0c;电…

广东成人高考报名将于9月14日开始!

截图来自广东省教育考试院官网* 今年的广东成人高考正式报名时间终于确定了&#xff01; 报名时间&#xff1a;2023年 9 月14—20日 准考证打印时间&#xff1a;考前一周左右 考试时间&#xff1a;2023年10月21—22日 录取时间&#xff1a;2023年12 月中上旬 报名条件: …

恒运资本:存储市场有望触底反弹 电子竞技迎催化

昨日&#xff0c;沪指早盘震动下探&#xff0c;午后拉升翻红&#xff1b;深成指、创业板指跌幅收窄&#xff1b;到收盘&#xff0c;沪指涨0.12%报3158.08点&#xff0c;深成指跌0.24%报10515.21点&#xff0c;创业板指跌0.47%报2101.4点&#xff0c;科创50指数涨0.43%&#xff…

集成快递物流平台(快递100、快递鸟、闪送)连通多个应用

场景描述&#xff1a; 基于快递物流平台&#xff08;快递100、快递鸟、闪送等&#xff09;开放能力&#xff0c;无代码集成快递物流平台与多个应用互连互通。通过Aboter可搭建业务自动化流程&#xff0c;实现多个应用之间的数据连接。 连接器&#xff1a; 快递100快递鸟闪送…

容器编排学习(五)卷的概述与存储卷管理

一 卷 1 容器化带来的问题 容器中的文件在磁盘上是临时存放的&#xff0c;这给容器中运行的重要的应用程序带来一些问题 问题1&#xff1a;当容器崩溃或重启的时候&#xff0c;kubelet 会以干净的状态(镜像的状态)重启容器&#xff0c;容器内的历史数据会丢失 问题2&…

Mediasoup本地部署

一、环境 1、nodejs、npm、mac、ssh 注意:不能使用python3&#xff0c;请用自带的python 二、代码下载 1、mediasoup-demo 包含app(客户端)、server(服务端)、broadcasters(推流模块) git clone https://github.com/versatica/mediasoup-demo.git git checkout v3 2、me…

RC-u3 兰州拉面派餐系统--睿抗2023国赛

兰州拉面是著名美食&#xff0c;其煮面很有讲究&#xff0c;不同种类的面需要煮不同的时长。拉面馆的煮面师傅的规则很简单&#xff0c;只要手头有煮面篮子是空闲的&#xff0c;就把下一份客单指定的面放到空闲篮子里煮&#xff1b;如果空闲的篮子不止一个&#xff0c;那么先放…

无涯教程-JavaScript - BITLSHIFT函数

描述 BITLSHIFT函数返回一个左移指定位数的数字。 语法 BITLSHIFT (number, shift_amount)争论 Argument描述Required/OptionalnumberNumber must be an integer greater than or equal to 0.Requiredshift_amountShift_amount must be an integer.Required Notes 向左移动…

Zenlayer 软件定义网络平台赋能海底光缆服务

上海&#xff0c;2023年9月6日&#xff0c;一年一度的印尼电信国际大会——Batic在巴厘岛隆重举行。该盛会云集了亚太地区电信及相关行业的重要领导者&#xff0c;就“共塑亚太地区数字化未来”进行了深入探讨。Zenlayer作为该会议的黄金赞助商&#xff0c;宣布将在原有L2、L3网…

Science adv | 转录因子SPIC连接胚胎干细胞中的细胞代谢与表观调控

代谢是生化反应网络的结果&#xff0c;这些反应吸收营养物质并对其进行处理&#xff0c;以满足细胞的需求&#xff0c;包括能量产生和生物合成。反应的中间体被用作各种表观基因组修饰酶的底物和辅助因子&#xff0c;因此代谢与表观遗传密切相关。代谢结合表观遗传涉及疾病&…

【特殊文本文件——Properties和xml文件】

特殊文本文件 一、Properties 是一个Map集合&#xff08;键值对集合&#xff09;&#xff0c;但是我们一般不会当集合用核心作用&#xff1a;Properties是用来代表属性文件的&#xff0c;通过Properties可以读写属性文件里的内容 1.使用Properties读取属性文件里的键值对数据…

基于51单片机烟雾温度检测报警系统设计

一、系统方案 本设计采用52单片机作为主控器&#xff0c;液晶1602显示&#xff0c;DS18B20采集温度&#xff0c;MQ2采集烟雾值&#xff0c;火焰传感器&#xff0c;按键设置报警&#xff0c;声光报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系…

MS-TTS:免费微软TTS语音合成工具(一键合成导出MP3音频)

声明 本工具是个免费工具&#xff0c;遇到问题&#xff0c;还请自行解决&#xff0c;下面有文字教程&#xff0c;B站有视频教程&#xff08;链接在文章末尾&#xff09;&#xff1b; 其次&#xff0c;微软接口卡顿&#xff0c;连接超时等问题下方有详细说明&#xff0c;请仔细…

视频怎么制作动图?分享简单的视频制作gif方法

现在的人们常用视频来记录自己的生活&#xff0c;但是视频的体积都会比较大&#xff0c;保存传输非常的不方便。我们可以将视频制作成gif动图来使用&#xff0c;而且gif动图也能将自己的心情想表达的想法通过这种个性的方式展示给对方。接下来&#xff0c;就给大家分享一款视频…

雅思 四处刷题的记录(一)

目录 阅读 九分达人7 test2 p2 听力 剑18 test1 section 1 剑18 test1 section 2 《九分达人》6 test3 section 1 and section 2 阅读 《九分达人》7 test2 passage1 词汇 Travelling的含义 town跟介词在一起的搭配 什么情况下使用on 大作文写作思路 当我们要使…

Python自动化测试(超详细~)

1. 概述 1.1 python自动化 什么是python自动化&#xff1f;我理解的Python自动化测试就是把以前人为测试转化为机器测试的一种过程。自动化测试是一种比手工测试更快获得故障反馈的方法。 自动化测试是一种质量保障的方式&#xff0c;最重要的还是以做好一款高质量产品为前提…

商用柴油车J1939油耗管理及北斗TBOX在物流运输中的应用

随着电商的兴起&#xff0c;网上购物的用户越来越多&#xff0c;网购、直播带货、精品电商等&#xff0c;促进了大量的物品流动&#xff0c;原本在一定程度上&#xff0c;物流管理上还有一些小混乱&#xff0c;但是随着监管和政策调整&#xff0c;物流互联数字平台对于货物流动…