Servlet 3.0 的异步请求处理的原理和使用案例

2,600 阅读7分钟

这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战

Java Web 的Servlet3.0 中提供的异步请求处理机制的原理,并提供了使用案例!

1 异步处理的概述

Web容器(比如tomcat)默认情况下会为每个请求分配一个请求处理线程(在tomcat7/8中,能够同时处理到达的请求的线程数量默认为200),默认情况下,在响应完成前,该线程资源都不会被释放。也就是说,处理HTTP请求和执行具体业务代码的线程是同一个线程!

如果Servlet或Filter中的业务代码处理时间相当长,常见的就是数据库操作,以及其它的跨网络调用等,那么请求处理线程将一只被占用,直到任务结束,这种情况下,随着并发请求数量的增加,将会可能导致处理请求线程全部被占用,此时tomcat会将后来的请求堆积到内部阻塞队列容器中,如果存放请求的阻塞队列也满了,那么后续的进来请求将会遭遇拒绝服务,直到有线程资源可以处理请求为止。

Servlet 3.0开始支持异步处理请求。在接收到请求之后,Servlet线程可以将耗时的操作委派给另一个线程来完成,自己在不生成响应的情况下返回至容器,以便能处理另一个请求。此时当前请求的响应将被延后,在异步处理完成后时再对客户端进行响应(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用)。

开启异步请求处理之后,Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。异步处理的特性可以帮助应用节省容器中的线程,特别适合执行时间长而且用户需要得到响应结果的任务,这将大大减少服务器资源的占用,并且提高并发处理速度。如果用户不需要得到结果,那么直接将一个Runnable对象交给内存中的Executor并立即返回响应即可。

我们还能发现,实际上这里的异步请求处理对于客户端浏览器来说仍然是同步输出,它并没有提升响应速度,用户是没有感知的,但是异步请求处理解放了服务器端的请求处理线程的使用,处理请求线程并没有卡在业务代码那里等待,当前的业务逻辑被转移给其他线程去处理了,能够让tomcat同时接受更多的请求,从而提升了并发处理请求的能力!

另外,Servlet的异步处理和tomcat的NIO是两个概念,关于tomcat的NIO模式,我们在此前就讲过了:Java Web(2)—Tomcat的server.xml配置文件详解

2 异步请求处理的使用

2.1 开启异步支持

在Servlet和Filter使用异步处理之前需要开启异步处理的支持!

可以在web.xml中开启:

<servlet>
    <servlet-name>AsyncServlet</servlet-name>
    <servlet-class>com.example.async.AsyncServlet</servlet-class>
    <!--servlet开启异步处理-->
    <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
    <servlet-name>AsyncServlet</servlet-name>
    <url-pattern>/AsyncServlet</url-pattern>
</servlet-mapping>

<filter>
    <filter-name>AsyncFilter</filter-name>
    <filter-class>com.example.async.AsyncFilter</filter-class>
    <!--filter开启异步处理-->
    <async-supported>true</async-supported>
</filter>
<filter-mapping>
    <filter-name>AsyncFilter</filter-name>
    <servlet-name>AsyncServlet</servlet-name>
</filter-mapping>

更方便的是通过注解直接开启:

@WebServlet(urlPatterns = "/AsyncServlet",asyncSupported = true)

@WebFilter(servletNames = "AsyncServlet", asyncSupported = true)

2.2 编写异步的Servlet和Filter

编写异步的Serlvet或Filter相对比较简单。如果你有一个任务需要相对比较长时间才能完成,最好创建一个异步的Servlet或者Filter,在异步的Servlet或者Filter类中需要完成以下工作:

  1. 调用request.startAsync()方法,获取一个AsyncContext对象。
    1. 该方法前者会直接利用原有的请求与响应对象来创建AsyncContext。可以通过AsyncContext的getRequest()、getResponse()方法取得请求、响应对象。也可以使用具有两个参数的startAsync方法,支持传入自行创建的请求、响应封装对象。
    2. 此次对客户端的响应将暂缓至调用AsyncContext的complete()或dispatch()方法为止,或者异步请求超时。
  2. 调用asyncContext.setTimeout()方法,可以设置一个容器必须等待指定任务完成的毫秒数。这个步骤是可选的,但是如果没有设置这个时限,将会采用容器的默认时间。如果任务没能在规定实限内完成,将可能会抛出异常,如果任务完成了但没有提交,那么超时时间到了之后将会尝试通过complete()提交。默认时间为30000 ms,值设置为零或负数表示没有超时。
  3. 调用asyncContext.addListener()方法设置异步请求监听器(可选设置),该监听器必须是AsyncListener接口的实现,AsyncListener接口有如下方法,分别监听异步请求的不同事件:
    1. onComplete:异步请求在调用complete()或者dispatch()方法之后进行回调,或者超时之后自动回调。
    2. onError:异步请求异常时回调。抛出某些异常时可能不会回调,而是会在等待超时之后回调onTimeout和onComplete方法。
    3. onStartAsync:执行startAsync()时回调,没测出来。
    4. onTimeout:异步请求超时后回调。任务超时会先回调onTimeout,紧接着会回调onComplete。
  4. 调用asyncContext.start()方法,传递一个执行长时间任务的Runnable,这个Runnable就是我们需要执行的业务代码逻辑。
    1. 一定要注意:该方法会通过一个新线程来执行任务,但是实际上仍然调用Connector线程池中的一个线程来执行,也就是说,默认情况下,这里的线程仍然是来自Servlet线程的线程池子。这样一来,我们原本想释放Servlet线程的,但是但实际上并没有,因为这里仍然是另一个Servlet线程来执行任务。
    2. 为此,在Web应用中自己单独维护一个全局线程池来执行业务任务会更好,这样才能真正的实现异步请求处理。
  5. 在Runnable内部,任务完成时需要调用asyncContext.complete()方法或者asyncContext.dispatch(String path)方法。前者表示响应完成,后者表示将调派指定的URL进行响应。
    1. dispatch(String path)方法的参数路径和ServletRequest.getRequestDispatcher(String)方法的路径要求一致,并且类似于请求包含!
    2. 多次执行complete()或者dispatch(String path)方法将会抛出异常!
    3. 如果没有执行complete()或者dispatch(String path)方法,那么将会等待直到超时时才会尝试响应!

一个简单的异步Servlet案例如下:

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

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        //Servlet线程
        System.out.println("Servlet线程: " + Thread.currentThread().getName());

        /*
         * 获取asyncContext
         */
        AsyncContext asyncContext = request.startAsync();

        /*
         * 设置超时时间毫秒
         */
        //asyncContext.setTimeout(4000);

        /*
         * 设置异步监听器,监听器方法可能会通过其他的线程来执行
         */
        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) {
                System.out.println("完成异步请求或者超时时回调: " + Thread.currentThread().getName());
            }

            @Override
            public void onTimeout(AsyncEvent event) {
                System.out.println("超时时回调: " + Thread.currentThread().getName());
            }

            @Override
            public void onError(AsyncEvent event) {
                System.out.println("异常时回调: " + Thread.currentThread().getName());
            }

            @Override
            public void onStartAsync(AsyncEvent event) {
                System.out.println("执行startAsync时回调: " + Thread.currentThread().getName());
            }
        });
        /*
         * 开始异步请求任务
         * 该方法会调用Connector线程池中的一个线程来执行这个线程任务。
         */
        asyncContext.start(() -> {
            //执行任务的线程
            System.out.println("执行任务的线程: " + Thread.currentThread().getName());
            //异步执行的任务代码
            //LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
            //执行时间超过了超时时间,将可能会抛出异常
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));

            //抛出异常,可能不会触发onError事件
            //int i = 1 / 0;

            PrintWriter out;
            try {
                response.setContentType("text/html");
                out = response.getWriter();
                out.println("hello_hello");
            } catch (IOException e) {
                e.printStackTrace();
            }

            //任务完成时调用
            //表示响应完成
            asyncContext.complete();

            //表示将调派指定的URL进行响应
            //asyncContext.dispatch("/index.jsp");

            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
            System.out.println("complete执行完毕");
        });
        /*
         * 也可以使用自己创建的线程或者线程池来执行,在应用中使用一个自己维护的全局线程池会更好
         */
//        new Thread(() -> {
//            //执行任务的线程
//            System.out.println("执行任务的线程: " + Thread.currentThread().getName());
//            //异步执行的任务代码
//            //LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
//            //执行时间超过了超时时间,将可能会抛出异常
//            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
//
//            //抛出异常,可能不会触发onError事件
//            //int i = 1 / 0;
//
//            PrintWriter out;
//            try {
//                out = response.getWriter();
//                out.println("hello_hello");
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//
//            //任务完成时调用
//            //表示响应完成
//            asyncContext.complete();
//
//            //表示将调派指定的URL进行响应
//            //asyncContext.dispatch("/index.jsp");
//
//            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
//            System.out.println("complete执行完毕");
//        }).start();
    }
}

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!