该篇文章翻译自: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. angularjs2 学习笔记(三) 服务

    在anglar2中服务是什么? 如果在项目中有一段代码是很多组件都要使用的,那么最好的方式就是把它做成服务. 服务是一种在组件中共享功能的机制,当我们使用标签将多个组件组合在一起时我们需要操作一些数据 ...

  2. 刀哥多线程之一次性代码gcd-11-once

    一次性执行 有的时候,在程序开发中,有些代码只想从程序启动就只执行一次,典型的应用场景就是"单例" // MARK: 一次性执行 - (void)once { static dis ...

  3. .NET开源工作流RoadFlow-流程设计-流程步骤设置-策略设置

    策略设置包括当前步骤的流转方式,处理人员,退回策略等设置. 流转类型:当前步骤后面有多个步骤时,此类型选择可以决定后续步骤的发送方式. 1.系统控制:由系统根据您在线上设置的流转条件来判断该发送到哪一 ...

  4. oracle 事务处理 注意事项(笔记)

    事务:一个独立的逻辑工作单元.它有特定的一系列必须作为一个整体一起成功或者失败的SQL语句组成.是一个要么全有要么全无,很个性的一个东东. 事务的四大属性——ACID属性:原子性(atomicity) ...

  5. gravity、layout_gravity、ayout_weight 区别及用法

    layout_gravity  表示组件自身在父组件中的位置. gravity             表示组件的子组件在组件中的位置. weight  意思是权重.比重,即当前控件在画布中所占的空间 ...

  6. Go support for Android

    Go support for Android David Crawshaw June 2014 Abstract We propose to introduce Go support for the ...

  7. Go append方法

    append用来将元素添加到切片末尾并返回结果.看代码: package main import "fmt" func main() { x := [],,} y := [],,} ...

  8. iOS学习之C语言函数

    一.函数的定义 返回值类型 函数名(参数类型 参数名, ...) { 功能语句; return 返回值; } 按照返回值和参数划分: 第一种: 无返回值 无参 void sayHello() { pr ...

  9. 微信扫码支付asp.net(C#)实现步骤

    支付提交页面: [HttpPost] public ActionResult index(decimal amount) { //生成订单10位序列号,此处用时间和随机数生成,商户根据自己调整,保证唯 ...

  10. verilog运算符及表达式

    1.运输符 算术运算符(+,-,X,/,%) 赋值运算符(=,<=) 关系运算符(>,<,>=,<=) 逻辑运算符(&&,||,!)//与或非 条件运算符 ...