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。 

  • 相关阅读:
    sublime tex创建可服用的片段
    php unset
    使用VS.NET来制作安装程序
    手机暗码大全.(挺好玩的..)
    【转】双目运算符 "??"
    linuxcp命令
    ios——在iPhone程序中打开其它程序
    IOS地图项目01网络编程很难么???
    C# BitArray 实例
    Windows Phone 8使用初体验
  • 原文地址:https://www.cnblogs.com/chang290/p/3276281.html
Copyright © 2011-2022 走看看