Servlet 是 Java 为了编写服务端程序而定义的一个接口规范, 在 Servlet 3.0 以后支持了异步的操作.
最近项目添加了一个代码热部署的功能, 在客户端输入信号, 信号到达 web 服务器后, 需要 Web 服务器将信号以 UDP 的方式递送给另外一个网关服务器, 网关服务器再以同样的通信方式返回信号, 最后在返回给客户端. 如图.
说到异步, 自然会联想到它的对立『同步』. 操作系统的知识告诉我们, 异步 / 同步实际上是指的一种消息通信机制.
由于在项目中 Web 服务器接受 UDP 信号是使用的异步线程, 所以必须要使用到 Servlet 的异步特性, 这是一个原因.
传统的 Servlet 规范中, 一个 Web 服务器 (即 Servlet 容器) 同时会接收到多个 HTTP 请求, 其中一个请求对应一个线程处理, 这个处理线程会涉及到业务逻辑处理, 甚至是数据库查询, 这些都是很费时的.
如果使用传统的 Servlet 同步规范, 举个极端的例子: 一个 Tomcat 服务器最多同时支持 150 个并发请求, 假设一个请求加上逻辑处理和数据库操作一次耗时 5s , 如果在 5s 内同时有 150 个人发来了 HTTP 请求, 这时 Tomcat 的处理能力正好满足需求, 如果这时候需要做代码的热部署, 再向 Web 服发送 HTTP 请求的话, 那么就会造成等待.
综合上面两个因素, 不得不使用到 Servlet 的异步特性.
首先就是异步 Servlet 中最重要的一个接口 AsyncContext, 它的源代码如下:
- public interface AsyncContext {
- // 获得一次请求中的 request 对象
- public ServletRequest getRequest();
- // 获得一次请求中的 response 对象
- public ServletResponse getResponse();
- // 检查 AsyncContext 是否由原生的 request 和 response 对象初始化生成
- public boolean hasOriginalRequestAndResponse();
- //
- public void dispatch();
- //
- public void dispatch(String path);
- //
- public void dispatch(ServletContext context, String path);
- // 将由 request 开启的异步操作设置为完成状态, 并关闭 response
- public void complete();
- // 调用此方法, Servlet 容器会分发一个线程来执行传进来的 Runnable 任务, 并且会向 Runnable 任务中传入必要的上下文信息, 即开启一个异步周期
- public void start(Runnable run);
- // 把指定的异步监听 AsyncListner 注册到 AsyncContext 中, 在一个异步周期中任何的 complete, time out, error 事件都会被监听器监听
- public void addListener(AsyncListener listener);
- public void addListener(AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse);
- public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException;
- // 为开启的异步周期设置超时时间, 如果不手动设置, 容器会为我们设置一个默认的超时时间
- public void setTimeout(long timeout);
- //
- public long getTimeout();
- }
在上面提到了异步监听器 AsyncListner, 下面来看看这个监听器的接口规范是什么样的:
- public interface AsyncListener extends EventListener {
- public void onComplete(AsyncEvent event) throws IOException;
- public void onTimeout(AsyncEvent event) throws IOException;
- public void onError(AsyncEvent event) throws IOException;
- public void onStartAsync(AsyncEvent event) throws IOException;
- }
传入一个 AsyncEvent 异步事件, 来完成各种监听. 观察 AsyncEvent 源代码知道这个异步事件的构成:
- public class AsyncEvent {
- private AsyncContext context;
- private ServletRequest request;
- private ServletResponse response;
- private Throwable throwable;
- public AsyncEvent(AsyncContext context, ServletRequest request, ServletResponse response, Throwable throwable) {
- this.context = context;
- this.request = request;
- this.response = response;
- this.throwable = throwable;
- }
- }
之所以在异步事件里保管 request 和 response 的引用, 是因为需要通过触发的异步事件对客户端进行响应, 要进行响应自然要用到 response 对象.
下面通过一个例子来看一看 异步 Servlet 规范到底如何使用:
- public class LoadClassServlet extends HttpServlet {
- private static final long serialVersionUID = 1L;
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- doPost(req, resp);
- }
- @Override
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- System.out.println("Start Servlet" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- System.out.println("doPost() Thread Name :" + Thread.currentThread().getName());
- System.out.println( "In doPost" + request);
- System.out.println( "In doPost" + response);
- request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
- // 开启异步周期
- AsyncContext asyncContext = request.startAsync();
- asyncContext.setTimeout(10*1000);
- asyncContext.start(new Runnable() {
- @Override
- public void run() {
- try {
- System.out.println("run() Thread Name :" + Thread.currentThread().getName());
- // 模拟启用线程异步递送信号, 一般来说是一些耗时的操作
- System.out.println("Before Sleep" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- Thread.sleep(5 * 1000);
- System.out.println("After Sleep" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- asyncContext.complete();
- System.out.println("After complete()" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- });
- System.out.println( "In AsyncContext" + asyncContext.getRequest() );
- System.out.println( "In AsyncContext" + asyncContext.getResponse() );
- asyncContext.addListener( new AsyncListener() {
- @Override
- public void onTimeout(AsyncEvent event) throws IOException {
- // /////////////////////////////////////
- // 这个方法内一般会写一些关于超时的逻辑
- // 假设 10s 还没有收到返回的信号, 就将错误消息
- // 在这里通过 response 对象返回给客户端
- // /////////////////////////////////////
- System.out.println("onTimeout()" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- event.getSuppliedResponse().getWriter().println("timeout");
- }
- @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 {
- // ///////////////////////////
- // 这个方法无论怎样都会被调用
- // 不管是手动调用 complete()方法
- // 还是超时, 这个方法都会被执行
- // ///////////////////////////
- System.out.println("onComplete()" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- System.out.println("onComplete() Thread Name :" + Thread.currentThread().getName());
- System.out.println( "In AsyncEvent"+event.getSuppliedRequest() );
- System.out.println( "In AsyncEvent"+event.getSuppliedResponse() );
- event.getSuppliedResponse().getWriter().println("complete");
- }
- } );
- System.out.println( "End of Servlet" + DateUtil.getDefaultFormatDate(System.currentTimeMillis()));
- }
- }
首先只打开关于 request 和 response 对象的打印语句, 在浏览器请求此 Servlet, 截取相关的输出如下:
- In doPost org.apache.catalina.connector.RequestFacade@4500c794
- In doPost org.apache.catalina.connector.ResponseFacade@307fd18d
- In AsyncContext org.apache.catalina.connector.RequestFacade@4500c794
- In AsyncContext org.apache.catalina.connector.ResponseFacade@307fd18d
- In AsyncEvent org.apache.catalina.connector.RequestFacade@4500c794
- In AsyncEvent org.apache.catalina.connector.ResponseFacade@307fd18d
可以看到 3 个地方的打印出来的 request 和 response 对象都是同一个.
为了方便控制台浏览, 现在将所有 request 和 response 的打印都注释掉.
为了弄清楚异步 Servlet 的执行流程, 观察四种情况下的控制台打印情况.
<1> 休眠时间为 5s, 休眠结束后立即调用 complete() 方法
请求 Servlet, 控制台打印如下:
- Start Servlet 2018-10-27 18:57:20
- doPost() Thread Name : http-nio-8080-exec-7
- run() Thread Name : http-nio-8080-exec-1
- Before Sleep 2018-10-27 18:57:20
- End of Servlet 2018-10-27 18:57:20
- After Sleep 2018-10-27 18:57:25
- After complete() 2018-10-27 18:57:25
- onComplete() 2018-10-27 18:57:25
- onComplete() Thread Name : http-nio-8080-exec-8
浏览器输出为:
异步操作执行 5s 后, 手动调用 complete() 方法, onCmplete() 监听方法被执行, 并向浏览器输出 complete 字符串, 这是一种情况.
<2 > 休眠时间改为 15s, 休眠结束后立即调用 complete() 方法
请求 Servlet, 控制台打印如下:
- Start Servlet 2018-10-27 18:59:09
- doPost() Thread Name : http-nio-8080-exec-2
- run() Thread Name : http-nio-8080-exec-4
- Before Sleep 2018-10-27 18:59:09
- End of Servlet 2018-10-27 18:59:09
- onTimeout() 2018-10-27 18:59:20
- onComplete() 2018-10-27 18:59:20
- onComplete() Thread Name : http-nio-8080-exec-5
- After Sleep 2018-10-27 18:59:24
浏览器输出为:
<3> 休眠时间为 5s, 休眠结束后不调用 complete()方法
请求 Servlet, 控制台打印如下:
- Start Servlet 2018-10-27 19:10:42
- doPost() Thread Name : http-nio-8080-exec-2
- run() Thread Name : http-nio-8080-exec-3
- Before Sleep 2018-10-27 19:10:42
- End of Servlet 2018-10-27 19:10:42
- After Sleep 2018-10-27 19:10:47
- onTimeout() 2018-10-27 19:10:53
- onComplete() 2018-10-27 19:10:53
- onComplete() Thread Name : http-nio-8080-exec-4
浏览器输出为:
<4> 休眠时间为 15s, 休眠结束后不调用 complete()方法
请求 Servlet, 控制台打印如下:
- Start Servlet 2018-10-27 19:13:11
- doPost() Thread Name : http-nio-8080-exec-3
- run() Thread Name : http-nio-8080-exec-7
- Before Sleep 2018-10-27 19:13:11
- End of Servlet 2018-10-27 19:13:11
- onTimeout() 2018-10-27 19:13:22
- onComplete() 2018-10-27 19:13:22
- onComplete() Thread Name : http-nio-8080-exec-5
- After Sleep 2018-10-27 19:13:26
浏览器输出为:
分析四种打印结果, 得出以下几个结论:
监听器的 onComplete 方法无论怎样都会执行, 要么是调用 AsyncContext#complete 方法后执行, 要么是超时时间到了自动执行.
只有在设定的 Timeout 时间内调用 AsyncContext#complete 方法才不会触发 onTimeout 方法, 其余情况都会被触发执行.
另外有趣的一点是, 执行过程中出现了 3 个不同的线程, 分别是: 处理 HTTP 请求的线程; 异步执行模拟递送信号的线程(异步线程也可以使用自己创建的线程); 执行回调监听方法的线程
在实际应用中, 一般不会像上面例子那样使用 Web 容器为我们分配的线程. 从上面的打印就能看出, 在 Tomcat 的实现中, 调用 AsyncContex#start() 后, 默认使用的的异步线程是处理 HTTP 请求的线程, 这样虽然能达到异步的目的, 但是对于提高请求的并发量没起到作用.
所以一般的做法是, 自己维护一个异步处理的线程池, 维护的线程数量一般大于 Web 容器用来处理 HTTP 请求的线程数. 这样既能实现异步操作, 又能提高 HTTP 请求处理的并发量.
(完)
来源: https://www.cnblogs.com/KKSJS/p/9857593.html