JavaEE Servlet的异步处理

时间:2022-12-29 21:00:52

Servlet3.0对异步处理提供了支持。

阻塞的Servlet

每个请求到达Web应用后,Web应用会为其分配一个线程来专门负责该请求,直到响应发送前,该线程都不会被线程池回收。若有些请求需要长时间处理(比如某些耗时运算或者需要等待某个资源),就会阻塞线程,若这类的请求很多,许多线程都将被长时间占用,对于系统就会产生较大负担,甚至会造成程序的效能瓶颈。

基本上一些需要长时间处理的请求,用户通常也不要求请求后就立即响应。如果可以让这类请求先释放分配给该请求的线程,让Web应用有机会将线程资源分配给其它请求,这样就可以减轻系统负担。而原先释放了所分配线程的请求,其响应将被延后,直到任务完成后再对用户发送响应。

Servlet3.0对异步处理提供了支持。

AsyncContext

在Servlet3.0中,ServletRequest类提供了几个新方法对异步处理进行支持:
- startAsync():利用原来的请求和响应创建一个异步处理上下文;
- startAsync(ServletRequest, ServletResponse):利用特定的请求和响应创建一个异步处理上下文;
- isAsyncSupported():判断当前请求是否支持异步操作;
- isAsyncStarted():判断当前请求是否开始异步处理;
- getAsyncContext():返回异步处理上下文,在异步处理开始后才能被调用,否则会抛出异常。

在调用了startAsync()方法取得AsyncContext对象后,这次的响应将被延后,并释放被分配到的线程。

AsyncContext提供了如下方法:
- getRequest():返回请求对象;
- getResponse():返回响应对象;
- setTimeout(long):设置超时时间;
- addListener(AsyncListener):注册监听器;
- start(Runnable):开始异步处理耗时任务;
- complete():异步处理完成,将向用户发送响应;
- dispatch(String):将请求转发给另一个Servlet或jsp页面。

在调用了complete()方法后,此次异常处理结束,响应将在此时发送给用户。

这里需要注意的是,AsyncContext不是异步输出,而是同步输出,但是会释放服务器端的线程。使用AsyncContext的时候,对于浏览器来说是在同步等待输出的,但是对于服务器端来说,处理此请求的线程并没有卡在那里等待,则是把当前的耗时任务加入一个线程池中处理,而请求线程将被释放。和自己发起一个新线程去处理耗时任务不同的是,服务器端会创建一个线程池去处理那些需要异步处理的请求,而如果每次请求都发起一个线程去处理的话,这就有可能会消耗大量的线程资源。

一个例子

下面是一个简单的异步处理程序:

// AsyncServlet.java

@WebServlet(asyncSupported = true, urlPatterns = { "/async" })
public class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=GBK");
PrintWriter out=response.getWriter();
out.println("<title>async task</title>");
out.println("进入Servlet:"+new Date()+"<br>");
out.flush();

AsyncContext ac=request.startAsync(request, response);
ac.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
event.getAsyncContext().getResponse().getWriter().println("业务完成:"+new Date());
event.getAsyncContext().dispatch("/async");
}

@Override
public void onError(AsyncEvent event) throws IOException {
event.getAsyncContext().getResponse().getWriter().println("业务错误:"+new Date());
}

@Override
public void onStartAsync(AsyncEvent event) throws IOException {
event.getAsyncContext().getResponse().getWriter().println("业务开始:"+new Date());
}

@Override
public void onTimeout(AsyncEvent event) throws IOException {
event.getAsyncContext().getResponse().getWriter().println("业务超时:"+new Date());
}
});
ac.setTimeout(30*1000);
ac.start(new Task(ac));

out.println("离开Servlet:"+new Date()+"<br>");
out.flush();
}

public static class Task implements Runnable {
private AsyncContext ac;

public Task(AsyncContext a) {
ac=a;
}

@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
ac.complete();
}

}
}

在这个Servlet中,我们调用startAsync(request, response)方法获得了一个AsyncContext对象,并设置超时时间,注册AsyncListener监听器后开始异步执行任务。此任务阻塞5秒来模拟耗时操作,然后调用complete方法结束此异步处理。

AsyncListener可以对异步处理进行监听:
- onComplete:异步处理完成后将回调此方法;
- onError:异步处理出错后将回调此方法;
- onStartAsync:异步处理开始后将回调此方法;
- onTimeout:异步处理超时后将回调此方法。

另外,还需要使用@WebServlet注解的asyncSupported属性对此Servlet进行配置。如果存在Filter作用于此Servlet,则这些Filter也需要设置asyncSupported属性为true

运行此Web应用,在浏览器地址栏输入:http://localhost:8080/WebDemo/async,大约等待5秒后会出现响应页面:

进入ServletSun Feb 05 17:54:41 CST 2017
离开ServletSun Feb 05 17:54:41 CST 2017
业务完成:Sun Feb 05 17:54:46 CST 2017

可以看到,现在这个Servlet不会被耗时任务所阻塞了。