该篇文章翻译自:http://developerlife.com/tutorials/?p=1437

  一、简介

  Servlet API 3.0 之前,需要使用类似Comet的方式来实现创建异步的Servlet。然而,Tomcat7 与 Servlet API 3.0 支持同步与异步方式。在同步Servlet中,一个处理客户端HTTP请求的线程将在整个请求的过程中被占用。对于运时较长的任务,服务器主要在等待一个应答,这导致了线程的饥渴,并且负载加重。这是由于即使服务器只是等待,服务端的线程还是被请求占光。

  异步Servlet,使用在其他线程中执行耗时(等待)的操作,而允许Tomcat的线程返回线程池的方式来解决问题。当任务完成并且结果就绪,Servlet容器需要得到通知,然后另外一个线程将被分配用于处理将结果返回给客户端。客户端完全感受不到服务器的差别,并且不需要任何改变。为了允许异步魔法,新的Servlet需要使用一种回调机制(有App Server提供),告知app server结果就绪。另外,需要告知Servlet容器(app server)何时可以释放当前正在处理的请求(随后该任务将在后台的线程中得到真正的处理)。

  二、简要的伪代码

  1、客户端请求通过HTTP请求,然后被分配到某个Servlet。

  2、Servlet.service方法在Servlet容器的某个线程中执行。

  3、Servlet.service方法创建一个AsyncContext对象(使用sartAsync())。

  4、Servlet.service方法后将创建AsyncContext对象传递给另外的线程执行。
  5、Servlet.service方法随后返回并结束执行。

  然后,客户端照样请求服务器,并且之前的那个连接被挂起等待,直到某时触发了如下事件:

  1、后台的线程处理完AsyncContext任务,并且结果已经就绪,将通知AsyncContext处理已经完成。它将向HttpResponse中写入返回的数据,并且调用AsyncContext的Complete方法。这将通知Servlet容器将结果返回给客户端。

  三、异常情况

  假如某些异常在后台线程处理期间发生,客户端应用将得到某些网络异常。然而,若后台线程处理任务时有错误,有一种方式处理这种情况。当创建AsyncContext时,可以指定两件事情:

  1、设置后台线程处理得到结果的最大时间,超过时产生一个超时异常。

  2、可以在超时事件上设置监听器来处理这种情况。

  因此,假如在后台线程出现了某些错误,经过一个你指定的合适时间后,监听器将被触发并且告知超时条件被触发。超时处理函数将被执行,可以向客户端发送一些错误信息。若后台线程在这之后试图向HttpResponse写入数据,将会触发一个异常。之后就需要将之前执行过程中产生的结果丢弃。

  四、简单实现

  将提供两组异步Servlet,一个简单实现一个一个复杂的。简单实现只是介绍异步Servlet的理念以及部分web.xml。另一个将展示耗时操作超时与触发异常,以便看到如何处理他们。

@javax.servlet.annotation.WebServlet(
// servlet name
name = "simple",
// servlet url pattern
value = {"/simple"},
// async support needed
asyncSupported = true
)
public class SimpleAsyncServlet extends HttpServlet { /**
* Simply spawn a new thread (from the app server's pool) for every new async request.
* Will consume a lot more threads for many concurrent requests.
*/
public void service(ServletRequest req, final ServletResponse res)
throws ServletException, IOException { // create the async context, otherwise getAsyncContext() will be null
final AsyncContext ctx = req.startAsync(); // set the timeout
ctx.setTimeout(30000); // attach listener to respond to lifecycle events of this AsyncContext
ctx.addListener(new AsyncListener() {
public void onComplete(AsyncEvent event) throws IOException {
log("onComplete called");
}
public void onTimeout(AsyncEvent event) throws IOException {
log("onTimeout called");
}
public void onError(AsyncEvent event) throws IOException {
log("onError called");
}
public void onStartAsync(AsyncEvent event) throws IOException {
log("onStartAsync called");
}
}); // spawn some task in a background thread
ctx.start(new Runnable() {
public void run() { try {
ctx.getResponse().getWriter().write(
MessageFormat.format("<h1>Processing task in bgt_id:[{0}]</h1>",
Thread.currentThread().getId()));
}
catch (IOException e) {
log("Problem processing task", e);
} ctx.complete();
}
}); } }

服务端简单示例

  代码说明:

  1、可以直接命名Servlet并且提供一个特殊的url模式,以避免弄乱web.xml条目。

  2、需要传递asyncSupported=true告知app server这个servlet需要采用异步模式。

  3、在service方法中,超时时间设置为30秒,因此只要后台线程执行时间小于30秒就不会触发超时错误。

  4、runnbale对象实际上传递给了app server在另外一个线程中执行。

  5、AsyncContext监听器没有执行有价值的操作,只是打印一行日志。

public class LoadTester {

public static final AtomicInteger counter = new AtomicInteger(0);
public static final int maxThreadCount = 100; public static void main(String[] args) throws InterruptedException { new LoadTester(); } public LoadTester() throws InterruptedException { // call simple servlet ExecutorService exec1 = Executors.newCachedThreadPool(); for (int i = 0; i < maxThreadCount; i++) { exec1.submit(new UrlReaderTask("http://localhost:8080/test/simple")); } exec1.shutdown(); Thread.currentThread().sleep(5000); System.out.println("....NEXT...."); // call complex servlet counter.set(0); ExecutorService exec2 = Executors.newCachedThreadPool(); for (int i = 0; i < maxThreadCount; i++) { exec2.submit(new UrlReaderTask("http://localhost:8080/test/complex")); } exec2.awaitTermination(1, TimeUnit.DAYS); } public class UrlReaderTask implements Runnable { private String endpoint;
public UrlReaderTask(String s) {
endpoint = s;
}
public void run() { try {
actuallyrun();
}
catch (Exception e) {
System.err.println(e.toString());
} } public void actuallyrun() throws Exception { int count = counter.addAndGet(1); BufferedReader in = new BufferedReader(
new InputStreamReader(
new URL(endpoint).openStream())); String inputLine; while ((inputLine = in.readLine()) != null) {
System.out.println(MessageFormat.format("thread[{0}] : {1} : {2}",
count, inputLine, endpoint));
} in.close(); } } }//end class ComplexLoadTester

客户端代码示例

  代码说明:

  1、这个简单的控台app只是产生100个线程并且同时执行GET请求,到简单与复杂的异步Servlet。

  2、简单异步Servlet运行没问题,并且返回所有的回复。需要注意app server中的线程ID将会有很大的差异,这些线程来自由tomcat 7管理的线程池。另外在下面的复杂示例中你将看到比当前示例少得多的线程id。

  五、复杂实现

  在这个复杂实现中,有如下主要的改变,

  1、这个Servlet管理自己固定大小的线程池,大小通过初试参数设置。这里设置成3。

  2、头四个请求都在处理过程中发生异常,只是为了说明在service函数中未处理的异常发生时的情况。

  3、耗时任务将被执行一个最大值为5秒的随机值,给AsyncContext设置的超时未60秒。这个结果在客户端请求的后20个将导致超时,因为只有3个服务器线程处理所有的100个并发请求。(100个并发的请求只有3个线程处理,每个任务1~5秒,排在后面的任务会在60秒之后才会得到执行)。

  4、当tomcat7检测到超时,并且监听器被触发后,监听器需要调用AsyncContext.complete()函数。

  5、一旦超时条件触发,tomcat7将会使AsyncContext包含的HttpRequest、HttpResponse对象无效。这是给耗时任务的一个信号,AsyncContext已经无效。这是为什么需要在往HttpResponse写入数据时检查Http对象是否为null。当一个超时发生时,耗时任务将不知道,并且必须检查request、response是否为null。如果为null,意味着需要停止处理,因为应答消息可以已经通过监听器或者tomcat7回复。

  

@javax.servlet.annotation.WebServlet(
// servlet name
name = "complex",
// servlet url pattern
value = {"/complex"},
// async support needed
asyncSupported = true,
// servlet init params
initParams = {
@WebInitParam(name = "threadpoolsize", value = "3")
}
)
public class ComplexAsyncServlet extends HttpServlet { public static final AtomicInteger counter = new AtomicInteger(0);
public static final int CALLBACK_TIMEOUT = 60000;
public static final int MAX_SIMULATED_TASK_LENGTH_MS = 5000; /** executor svc */
private ExecutorService exec; /** create the executor */
public void init() throws ServletException { int size = Integer.parseInt(
getInitParameter("threadpoolsize"));
exec = Executors.newFixedThreadPool(size); } /** destroy the executor */
public void destroy() { exec.shutdown(); } /**
* Spawn the task on the provided {@link #exec} object.
* This limits the max number of threads in the
* pool that can be spawned and puts a ceiling on
* the max number of threads that can be used to
* the init param "threadpoolsize".
*/
public void service(final ServletRequest req, final ServletResponse res)
throws ServletException, IOException { // create the async context, otherwise getAsyncContext() will be null
final AsyncContext ctx = req.startAsync(); // set the timeout
ctx.setTimeout(CALLBACK_TIMEOUT); // attach listener to respond to lifecycle events of this AsyncContext
ctx.addListener(new AsyncListener() {
/** complete() has already been called on the async context, nothing to do */
public void onComplete(AsyncEvent event) throws IOException { }
/** timeout has occured in async task... handle it */
public void onTimeout(AsyncEvent event) throws IOException {
log("onTimeout called");
log(event.toString());
ctx.getResponse().getWriter().write("TIMEOUT");
ctx.complete();
}
/** THIS NEVER GETS CALLED - error has occured in async task... handle it */
public void onError(AsyncEvent event) throws IOException {
log("onError called");
log(event.toString());
ctx.getResponse().getWriter().write("ERROR");
ctx.complete();
}
/** async context has started, nothing to do */
public void onStartAsync(AsyncEvent event) throws IOException { }
}); // simulate error - this does not cause onError - causes network error on client side
if (counter.addAndGet(1) < 5) {
throw new IndexOutOfBoundsException("Simulated error");
}
else {
// spawn some task to be run in executor
enqueLongRunningTask(ctx);
} } /**
* if something goes wrong in the task, it simply causes timeout condition that causes
* the async context listener to be invoked (after the fact)
* <p/>
* if the {@link AsyncContext#getResponse()} is null, that means this context has
* already timedout (and context listener has been invoked).
*/
private void enqueLongRunningTask(final AsyncContext ctx) { exec.execute(new Runnable() {
public void run() { try { // simulate random delay
int delay = new Random().nextInt(MAX_SIMULATED_TASK_LENGTH_MS);
Thread.currentThread().sleep(delay); // response is null if the context has already timedout
// (at this point the app server has called the listener already)
ServletResponse response = ctx.getResponse();
if (response != null) {
response.getWriter().write(
MessageFormat.format("<h1>Processing task in bgt_id:[{0}], delay:{1}</h1>",
Thread.currentThread().getId(), delay)
);
ctx.complete();
}
else {
throw new IllegalStateException("Response object from context is null!");
}
}
catch (Exception e) {
log("Problem processing task", e);
e.printStackTrace();
} }
});
} }

复杂的服务器示例

  六、异步还是同步

  综上所述,API使用很直观,假设从一开始你就熟悉异步处理。然而假如你不熟悉异步处理,这种callback的方式会带来困惑与恐惧。另外Tomcat7与Servlet API 3.0更加容易配置servlet,在这个教程中都没有涉及,如符合语法规则的加载Servlet。

 

使用tomcat7创建异步servlet的更多相关文章

  1. Servlet 3特性:异步Servlet

    解异步Servlet之前,让我们试着理解为什么需要它.假设我们有一个Servlet需要很多的时间来处理,类似下面的内容: LongRunningServlet.java package com.jou ...

  2. 关于servlet3.0中的异步servlet

    刚看了一下维基百科上的介绍,servlet3.0是2009年随着JavaEE6.0发布的: 到现在已经有六七年的时间了,在我第一次接触java的时候(2011年),servlet3.0就已经出现很久了 ...

  3. WebFlux01 webflux概念、异步servlet、WebFlux意义

    1 概念 待更新...... 2 异步servlet 2.1 同步servlet servlet容器(如tomcat)里面,每处理一个请求会占用一个线程,同步servlet里面,业务代码处理多久,se ...

  4. 异步servlet的原理探究

    异步servlet是servlet3.0开始支持的,对于单次访问来讲,同步的servlet相比异步的servlet在响应时长上并不会带来变化(这也是常见的误区之一),但对于高并发的服务而言异步serv ...

  5. EasyUI创建异步树形菜单和动态添加标签页tab

    创建异步树形菜单 创建树形菜单的ul标签 <ul class="easyui-tree" id="treeMenu"> </ul> 写j ...

  6. Filter 快速开始 异步Servlet 异步请求 AsyncContext 异步线程 异步派发 过滤器拦截

    [web.xml] <filter> <filter-name>normalFilter</filter-name> <filter-class>net ...

  7. 雷林鹏分享:jQuery EasyUI 树形菜单 - 创建异步树形菜单

    jQuery EasyUI 树形菜单 - 创建异步树形菜单 为了创建异步的树形菜单(Tree),每一个树节点必须要有一个 'id' 属性,这个将提交回服务器去检索子节点数据. 创建树形菜单(Tree) ...

  8. 雷林鹏分享:jQuery EasyUI 表单 - 创建异步提交表单

    jQuery EasyUI 表单 - 创建异步提交表单 本教程向您展示如何通过 easyui 提交一个表单(Form).我们创建一个带有 name.email 和 phone 字段的表单.通过使用 e ...

  9. 异步Servlet和异步过虑器

    异步处理功能可以节约容器线程.此功能的作用是释放正在等待完成的线程,是该线程能够被另一请求所使用. 要编写支持异步处理的 Servlet 或者过虑器,需要设置 asyncSupported 属性为 t ...

随机推荐

  1. JTable的DefaultModel方法getValueAt(a,row)

    行和列都是从0开始索引的,而且不包括netbeans生成的表格头,而是从数据开始,否则就会报错

  2. 【转】SQLite提示database disk image is malformed的解决方法

    SQLite有一个很严重的缺点就是不提供Repair命令. 导致死亡提示database disk image is malformed 它的产生有很多种可能,比如,磁盘空间不足,还有就是写入数据过程 ...

  3. 在SQL Server 2012中新建用户

    一.问题描述 在最开始装SQL Server 2012时我选择的是Windows身份认证方式,现在想添加一个用户采用SQL Server身份验证. 二.具体思路 1.新建用户 2.将新建的用户添加到相 ...

  4. Android 文档之viewAnimator

    一.结构 public class ViewAnimator extends FrameLayout java.lang.Object android.view.View android.view.V ...

  5. .NET Async/Await 最佳实践

    .NET 异步编程Guildlines 名称 描述 例外 Avoid async void Prefer async Task methods over async void methods Even ...

  6. WCF 已超过传入消息(65536)的最大消息大小配额。若要增加配额,请使用相应绑定元素上的 MaxReceivedMessageSize 属性

    我出现这个问题主要是服务器返回数据量过大引起了,需要客户端服务端都要进行配置:我会说其实有神器的么....(工具=>wcf服务配置编辑器),用工具编辑下,就会完全搞定这个问题,再也不用纠结了 服 ...

  7. IOS之表视图添加索引

    我们要实现的效果如下. 1.修改ControlView.h,即添加变量dict,用于存储TabelView的数据源. #import <UIKit/UIKit.h> @interface  ...

  8. Android编程: 调试方法

    学习知识:Android的调试方法 ====调试方法==== 前提: IDE环境为Android Studio,熟悉LogCat,知道如何查看日志信息 工具: Android DDMS调试工具,一般点 ...

  9. Android实现传感器应用及位置服务

    Android实现传感器应用及位置服务 开发工具:Andorid Studio 1.3 运行环境:Android 4.4 KitKat 代码实现 这里需用获取加速度传感器和地磁传感器,手机获取旋转的方 ...

  10. Junit单元测试中优先使用AssertThat

    主要的优点: 1. 易读性 2. 错误信息更方便 推荐阅读:https://objectpartners.com/2013/09/18/the-benefits-of-using-assertthat ...