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


    在这里插入图片描述


    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("");
                        out.println("");
                        out.println("Hello World");
                        out.println("");
                        out.println("");
                        out.println("

    welcome this is my servlet1!!!

    "
    ); out.println(""); out.println(""); 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    由上述代码可知:

    • 如上代码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("");
                        out.println("");
                        out.println("Hello World");
                        out.println("");
                        out.println("");
                        out.println("

    welcome this is my servlet1!!!

    "
    ); out.println(""); out.println(""); 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    通过如上代码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----");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

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


    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("");
                        out.println("");
                        out.println("Hello World");
                        out.println("");
                        out.println("");
                        out.println("

    welcome this is my servlet1!!!

    "
    ); out.println(""); out.println(""); 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    如上代码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("");
                                out.println("");
                                out.println("Hello World");
                                out.println("");
                                out.println("");
                                out.println("

    welcome this is my servlet1!!!

    "
    ); out.println(""); out.println(""); 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----"); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 代码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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    上述代码我们创建了一个业务线程池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";
            }
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上述代码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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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


    小结

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

    在这里插入图片描述

  • 相关阅读:
    Java SE 学习笔记(十四)—— IO流(2)
    改变光标形状的多种方式
    小型数据库系统开发作业
    ATFX:从百济神州半年报,看生物科技板块前景
    JAVA继承
    网络安全攻防:ZigBee安全
    企业实践开源的动机
    DevExpress WinForms甘特图组件 - 轻松集成项目管理功能到应用
    Golang Gin框架搭建项目(六)Grpc服务
    认识通讯协议——TCP/IP、UDP协议的区别,HTTP通讯协议的理解
  • 原文地址:https://blog.csdn.net/yangshangwei/article/details/132724445