zoukankan      html  css  js  c++  java
  • 使用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。 

  • 相关阅读:
    【Android Developers Training】 73. 布局变化的动画
    【Android Developers Training】 72. 缩放一个视图
    【Android Developers Training】 71. 显示翻牌动画
    svn更改地址怎么办
    python学习手册
    failed to bind pixmap to texture
    Ubuntu 12.04安装Google Chrome
    svn update 时总是提示 Password for '默认密钥' GNOME keyring: 输入密码
    重设SVN 的GNOME keyring [(null)] 的密码
    Nginx + uWSGI + web.py 搭建示例
  • 原文地址:https://www.cnblogs.com/chang290/p/3276281.html
Copyright © 2011-2022 走看看