Java Web 的Servlet3.0 中提供的异步请求处理机制的原理,并提供了使用案例!
文章目录
- 1 异步处理的概述
- 2 异步请求处理的使用
- 2.1 开启异步支持
- 2.2 编写异步的Servlet和Filter
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配置文件详解。
Servlet3.0 中提供的异步请求处理机制又被实际应用过吗?实际上是有的,并且被应用的很广泛:
- Apollo配置更新使用异步Servlet技术。
- Dubbo服务端提供者的异步调用参考了异步Servlet。
- Nacos配置更新使用异步Servlet技术。
如果你是个Java新手,那么这些Apollo、Dubbo、Nacos这些名词可能会让你感到陌生,但是你以后一定会有所耳闻的!你只需要记住异步Servlet肯定是有所为的就可以了!
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类中需要完成以下工作:
- 调用request.startAsync()方法,获取一个AsyncContext对象。
- 该方法前者会直接利用原有的请求与响应对象来创建AsyncContext。可以通过AsyncContext的getRequest()、getResponse()方法取得请求、响应对象。也可以使用具有两个参数的startAsync方法,支持传入自行创建的请求、响应封装对象。
- 此次对客户端的响应将暂缓至调用AsyncContext的complete()或dispatch()方法为止,或者异步请求超时。
- 调用asyncContext.setTimeout()方法,可以设置一个容器必须等待指定任务完成的毫秒数。这个步骤是可选的,但是如果没有设置这个时限,将会采用容器的默认时间。如果任务没能在规定实限内完成,将可能会抛出异常,如果任务完成了但没有提交,那么超时时间到了之后将会尝试通过complete()提交。默认时间为30000 ms,值设置为零或负数表示没有超时。
- 调用asyncContext.addListener()方法设置异步请求监听器(可选设置),该监听器必须是AsyncListener接口的实现,AsyncListener接口有如下方法,分别监听异步请求的不同事件:
- onComplete:异步请求在调用complete()或者dispatch()方法之后进行回调,或者超时之后自动回调。
- onError:异步请求异常时回调。抛出某些异常时可能不会回调,而是会在等待超时之后回调onTimeout和onComplete方法。
- onStartAsync:执行startAsync()时回调,没测出来。
- onTimeout:异步请求超时后回调。任务超时会先回调onTimeout,紧接着会回调onComplete。
- 调用asyncContext.start()方法,传递一个执行长时间任务的Runnable,这个Runnable就是我们需要执行的业务代码逻辑。
- 一定要注意:该方法会通过一个新线程来执行任务,但是实际上仍然调用Connector线程池中的一个线程来执行,也就是说,默认情况下,这里的线程仍然是来自Servlet线程的线程池子。这样一来,我们原本想释放Servlet线程的,但是但实际上并没有,因为这里仍然是另一个Servlet线程来执行任务。
- 为此,在Web应用中自己单独维护一个全局线程池来执行业务任务会更好,这样才能真正的实现异步请求处理。
- 在Runnable内部,任务完成时需要调用asyncContext.complete()方法或者asyncContext.dispatch(String path)方法。前者表示响应完成,后者表示将调派指定的URL进行响应。
- dispatch(String path)方法的参数路径和ServletRequest.getRequestDispatcher(String)方法的路径要求一致,并且类似于请求包含!
- 多次执行complete()或者dispatch(String path)方法将会抛出异常!
- 如果没有执行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();
}
}