使用tomcat7创建异步servlet
该篇文章翻译自: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的更多相关文章
- Servlet 3特性:异步Servlet
解异步Servlet之前,让我们试着理解为什么需要它.假设我们有一个Servlet需要很多的时间来处理,类似下面的内容: LongRunningServlet.java package com.jou ...
- 关于servlet3.0中的异步servlet
刚看了一下维基百科上的介绍,servlet3.0是2009年随着JavaEE6.0发布的: 到现在已经有六七年的时间了,在我第一次接触java的时候(2011年),servlet3.0就已经出现很久了 ...
- WebFlux01 webflux概念、异步servlet、WebFlux意义
1 概念 待更新...... 2 异步servlet 2.1 同步servlet servlet容器(如tomcat)里面,每处理一个请求会占用一个线程,同步servlet里面,业务代码处理多久,se ...
- 异步servlet的原理探究
异步servlet是servlet3.0开始支持的,对于单次访问来讲,同步的servlet相比异步的servlet在响应时长上并不会带来变化(这也是常见的误区之一),但对于高并发的服务而言异步serv ...
- EasyUI创建异步树形菜单和动态添加标签页tab
创建异步树形菜单 创建树形菜单的ul标签 <ul class="easyui-tree" id="treeMenu"> </ul> 写j ...
- Filter 快速开始 异步Servlet 异步请求 AsyncContext 异步线程 异步派发 过滤器拦截
[web.xml] <filter> <filter-name>normalFilter</filter-name> <filter-class>net ...
- 雷林鹏分享:jQuery EasyUI 树形菜单 - 创建异步树形菜单
jQuery EasyUI 树形菜单 - 创建异步树形菜单 为了创建异步的树形菜单(Tree),每一个树节点必须要有一个 'id' 属性,这个将提交回服务器去检索子节点数据. 创建树形菜单(Tree) ...
- 雷林鹏分享:jQuery EasyUI 表单 - 创建异步提交表单
jQuery EasyUI 表单 - 创建异步提交表单 本教程向您展示如何通过 easyui 提交一个表单(Form).我们创建一个带有 name.email 和 phone 字段的表单.通过使用 e ...
- 异步Servlet和异步过虑器
异步处理功能可以节约容器线程.此功能的作用是释放正在等待完成的线程,是该线程能够被另一请求所使用. 要编写支持异步处理的 Servlet 或者过虑器,需要设置 asyncSupported 属性为 t ...
随机推荐
- angularjs2 学习笔记(三) 服务
在anglar2中服务是什么? 如果在项目中有一段代码是很多组件都要使用的,那么最好的方式就是把它做成服务. 服务是一种在组件中共享功能的机制,当我们使用标签将多个组件组合在一起时我们需要操作一些数据 ...
- 刀哥多线程之一次性代码gcd-11-once
一次性执行 有的时候,在程序开发中,有些代码只想从程序启动就只执行一次,典型的应用场景就是"单例" // MARK: 一次性执行 - (void)once { static dis ...
- .NET开源工作流RoadFlow-流程设计-流程步骤设置-策略设置
策略设置包括当前步骤的流转方式,处理人员,退回策略等设置. 流转类型:当前步骤后面有多个步骤时,此类型选择可以决定后续步骤的发送方式. 1.系统控制:由系统根据您在线上设置的流转条件来判断该发送到哪一 ...
- oracle 事务处理 注意事项(笔记)
事务:一个独立的逻辑工作单元.它有特定的一系列必须作为一个整体一起成功或者失败的SQL语句组成.是一个要么全有要么全无,很个性的一个东东. 事务的四大属性——ACID属性:原子性(atomicity) ...
- gravity、layout_gravity、ayout_weight 区别及用法
layout_gravity 表示组件自身在父组件中的位置. gravity 表示组件的子组件在组件中的位置. weight 意思是权重.比重,即当前控件在画布中所占的空间 ...
- Go support for Android
Go support for Android David Crawshaw June 2014 Abstract We propose to introduce Go support for the ...
- Go append方法
append用来将元素添加到切片末尾并返回结果.看代码: package main import "fmt" func main() { x := [],,} y := [],,} ...
- iOS学习之C语言函数
一.函数的定义 返回值类型 函数名(参数类型 参数名, ...) { 功能语句; return 返回值; } 按照返回值和参数划分: 第一种: 无返回值 无参 void sayHello() { pr ...
- 微信扫码支付asp.net(C#)实现步骤
支付提交页面: [HttpPost] public ActionResult index(decimal amount) { //生成订单10位序列号,此处用时间和随机数生成,商户根据自己调整,保证唯 ...
- verilog运算符及表达式
1.运输符 算术运算符(+,-,X,/,%) 赋值运算符(=,<=) 关系运算符(>,<,>=,<=) 逻辑运算符(&&,||,!)//与或非 条件运算符 ...