Asynchronous Java Servlets

The advancement of the web technology since 2005 has been about attaining asynchronous communications. The first breakthrough with these techniques came through the AJAX frameworks, which exploited a little used client-side API in the Internet Explorer web browser and then the subsequent web browsers to send a request asynchronously to a server, and then wait for a response in a non-blocking fashion. The key AJAX was the evolution of JavaScript as an accepted programming language in the browser and non-blocking input and output.

The technology then moved to COMET, which is a neo-logistic term (coined by Alex Russell) for a collection of techniques for streaming data from the server to the multiple clients without these clients then requesting data. Finally, we have HTML 5 WebSockets as an up and coming standard.

Java API for WebSockets is covered in Chapter 7, Java API for HTML5WebSocket, of this book. Both the AJAX and COMET terms are a play on words; both are common household cleaning agents in USA and perhaps in Europe. As a side note in history, it was possible to have asynchronous communications since the very first Java Applets, as they could create and open TCP/IP sockets for long periods of time, provided the web page remained opened in the browser.

This section reviews the asynchronous requests and responses with Java Servlets.

The asynchronous input and output

In the earliest days of the Java SDK, all the inputs and outputs were synchronous; by this term, we mean the inputs and outputs were blocking. A Java thread would block on data push to the output or a data read, if the resource was not available. So the Java SDK engineers fixed this issue with a new API named New Input and Output (NIO), in the Java SDK 1.4. The non-blocking input and output operations are the basis of building the scalable server-side applications.

NIO is a non-blocking API based on Channels, Buffers, and Selectors. Since Java SE 7, in 2012, Java NIO has been upgraded to NIO2, which supports the portable file system operations such as symbolic links, polling directory and the file notifications, and the file attribute information.

Non-blocking I/O is only available for Java Servlets and filters, and only for asynchronous requests. In order to process a web request in an asynchronous fashion, another thread will be activated to process the request, and generate the response. The idea is not to block the dispatching threads that the container actually uses itself.

A synchronous reader example

It is instructive to review the code for blocking I/O in order to obtain an appreciation of the asynchronous operations. Suppose our task was to write a Servlet that would process large volumes of data. We are instructed to write a Servlet component that analyses volumes of text and the web client will push data as an HTTP POST request. This data could arrive from an AJAX style HTTP POST form request, which is a typical modern web application that performs user-friendly file upload, and provide a visual update to the user.

A first cut at this code in a class named DataBlockingIOServlet is as follows:

@WebServlet(urlPatterns = {"/reader/*"}
public class DataBlockingIOServlet extends HttpServlet {
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
    ServletInputStream input = request.getInputStream();
    byte[] buffer = new byte[2048];
    int len = -1;
    while (( len = input.read(buffer)) ! = -1) {
      String data = new String(buffer, 0, len);
      System.out.printf("data=[%s]
", data);
      }
    }
  }

The essential method doPost() obtains ServletInputStream from the Servlet request instance. We use traditional Java I/O methods from package java.io to read data from the stream into a buffer of 2048 bytes in size. We simply dump the input data into the standard console.

This code is fine from the standpoint of a single web client using the server. Our problems will arise from scalability when lots of web clients start to access DataBlockingIOServlet. When the file sizes exceed our 2048 buffer, or when they exceed the internal buffers of web container, or when they exceed the size of the network operating system, we will find the JVM blocks on ServletInputStream. If the web client starts sending data to Servlet, and then for some strange reason pauses for a long time, then Servlet and the associated container thread will be blocked. The point is, any sort of blocking on a web container thread (or the application server thread) is bad. It is bad for other resources also, including other web applications that also share the facilities of the container. For these reasons alone, blocking the inputs and outputs puts severe restrictions on the ability of a container to scale and meet the demands of multiple consumers.

The blocking of input and output will also occur on ServletOutputStream, although we have not shown it in the code example. For instance, if we wrote a similar Servlet to the stream investment data to a consuming web client.

An asynchronous reader example

Clearly the solution is to use the non-blocking input and output. Servlet 3.1 provides a way out and allows the web container to transfer responsibility to Servlet developer to just read and write data to and from consumer, but still enforce the control of the Servlet container threads.

The Servlet 3.1 specification adds event listeners javax.servlet.ReadListener and javax.servlet.WriteListener.

The interface declaration for ReadListener is as follows:

public interface ReadListener extends java.util.EventListener {
  public void onDataAvailable();
  public void onAllDataRead();
  public void onError(Throwable t);
  }

ReadListener is an event listener, which is invoked by web container on notification of an available asynchronous input. The methods on this class are invoked when the HTTP request data is guaranteed to be readable without non-blocking.

After an asynchronous context has been obtained and started, then a ReadListener implementation is registered with a call to ServletInputStream.setReadListener().

Method

Descriptor

onDataAvailable

Provided an instance of ReadListener is registered with ServletInputStream, this method is only invoked by the container the first time, when it is possible to read data. Subsequently, the container will invoke this method, if and only if the ServletInputStream.isReady() method has been called and has returned false. This method is one to retrieve data from the stream and then for the implementation to process it.

onAllDataRead

The container invokes this method when all of the data has been read from the input. In reality, this means the web client has closed the connection after sending all of the data.

onError

The container invokes this method when something has gone wrong and there is a plausible error. Here, the implementation can take action on the error. The exception is passed to the method.

There are two other methods in ServletInputStream, which can be of use. These were added in Servlet 3.1. The isReady() method returns a boolean, and it specifies when data can be read from the stream without blocking. The isFinished() method also returns a boolean, and it specifies when all data for this particular request has been read.

With this information, we can write an asynchronous Servlet. The implementation code is as follows:

package je7hb.servlets.simple;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

@WebServlet(name = "AsyncReaderServlet", urlPatterns = {"/reader"}, asyncSupported = true)
public class AsyncReaderServlet extends HttpServlet {
  @Override
  public void init(ServletConfig config)
  throws ServletException {
    super.init(config);
    System.out.printf("init() called on %s
", getClass().getSimpleName());
    }
  
  public void doGet(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
    System.out.printf("doGet() called on %s
", getClass().getSimpleName());
    processRequest(request);
    }
  
  public void doPost(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
    System.out.printf("doPost() called on %s
", getClass().getSimpleName());
    processRequest(request);
    }
  
  private void processRequest(HttpServletRequest request)
  throws IOException {
    System.out.printf("processRequest() called %s" + " on thread [%s]
", getClass().getSimpleName(), Thread.currentThread().getName());
    AsyncContext context = request.startAsync();
    ServletInputStream input = request.getInputStream();
    input.setReadListener(new AsyncReadListener(input, context));
    }
  
  private class AsyncReadListener implements ReadListener {
    private ServletInputStream input;
    private AsyncContext context;
    
    private AsyncReadListener(ServletInputStream input, AsyncContext context) {
    this.input = input;
    this.context = context;
    }
  
  @Override
  public void onDataAvailable() {
    try {
      StringBuilder sb = new StringBuilder();
      int len = -1;
      byte buffer[] = new byte[2048];
      while (input.isReady() && (len = input.read(buffer)) ! = -1) {
        String data = new String(buffer, 0, len);
        System.out.printf("thread [%s] data: %s
", Thread.currentThread().getName(), data);
        }
      } catch (IOException ex) {
        ex.printStackTrace(System.err);
        }
      }
    
    @Override
    public void onAllDataRead() {
      System.out.printf("thread [%s] onAllDataRead()
", Thread.currentThread().getName());
      context.complete();
      }
    
    @Override
    public void onError(Throwable t) {
      System.out.printf("thread [%s] Error occurred=%s
", Thread.currentThread().getName(), t.getMessage());
      context.complete();
      }
    }
  }

In order to be recognized by web container as an asynchronous Servlet, it must be annotated with @WebServlet and the asyncSupport attribute set to true.

Although this example Servlet AsyncReaderServlet is long, there is really nothing to it. The most important method is the refactored method processRequest(), which starts the asynchronous communication. The processRequest() method is shared between the doGet() and doPost() methods. Inside the method, Servlet retrieves the javax.servlet.AsyncContext instance from the HttpServletRequest instance and starts the process. The AsyncContext class represents the execution context for an asynchronous operation. The method, then, retrieves ServletInputStream, and then creates an inner class, an event listener, AsyncReadListener, and registers it with the stream.

AsyncReadListener is the event listener that reads data asynchronously from the web client. Inside the method onDataAvailable(), the event listener only dumps data to the standard output. In a business application, you will store data or act on it. The inner class also prints out the current thread by the name for analysis.

If there is an error in processing, then the container will invoke the onError() method. It is important to close the asynchronous context, and this is exactly what this method does.

Similarly, once all the data has been read, the container will invoke the onAllDataRead() method, and the asynchronous context is also closed with a call to complete(). In both of these callbacks, the application can take action.

In order to test this asynchronous Servlet, we need a test client. Since we can execute a container-less embedded GlassFish server application, we can write a modified and refactored example.

The code for this new test client is as follows:

package je7hb.common.webcontainer.embedded.glassfish;

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import java.util.StringTokenizer;

public class EmbeddedAsyncReaderRunner extends AbstractEmbeddedRunner {
  private static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipisicing " + "elit, sed do eiusmod tempor incididunt ut labore et " + "dolore magna aliqua. Ut enim ad minim veniam, quis " + /* ... */ est laborum.";
  
  public EmbeddedAsyncReaderRunner(int port) {
    super(port);
    }
  
  public static void main(String args[]) throws Exception {
    EmbeddedAsyncReaderRunner runner = (EmbeddedAsyncReaderRunner)
    new EmbeddedAsyncReaderRunner(8080).init().start();
    runner.deployWithRename("build/libs/ch06-servlets-basic-1.0.war", "mywebapp");
    Thread.sleep(1000);
    String path = String.format("http://localhost:%d/%s/%s", 8080, "mywebapp", "reader");
    
    URL url = new URL(path);
    System.out.printf("Client connecting to server on path %s
", path);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setChunkedStreamingMode(2);
    conn.setDoOutput(true);
    conn.connect();
    try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()))) {
      System.out.println("Sending data ...");
      output.write("Beginning Text");
      output.flush();
      System.out.println("Sleeping ...");
      Thread.sleep(3000);
      System.out.println("Sending more data ...");
      StringTokenizer stk = new StringTokenizer(LOREM_IPSUM," 	,.");
      while ( stk.hasMoreTokens()) {
        output.write( stk.nextToken());
        output.flush();
        Thread.sleep(200);
        }
      System.out.println("Finishing client");
      output.write("Ending Text");
      output.flush();
      output.close();
      }
    System.out.println("Check standard console ");
    Thread.sleep(1000);
    System.out.println("Disconnecting and shutdown");
    conn.disconnect();
    runner.stop();
    
     System.exit(0);
    }
}

The EmbeddedAsyncReaderRunner class is a main application and starts an embedded GlassFish server instance; it deploys the WAR file as a web application as we saw earlier in the chapter.

Instead of waiting for a console input to terminate the application, the program creates a URL connection to the asynchronous Java Servlet, and then invokes an HTTP POST request to it. The text of the Lorem Ipsum is fed one word at a time to Java Servlet using the HTTP URL connection. The words are pushed at the rate of one every 200 milliseconds. Notice how the client flushes the buffer to ensure the data is sent over the network.

After all of the words of the text are fed in, the connection is disconnected, and the embedded runner is halted. We have to call System.exit in order to halt the Java threads in the container-less application.

The output of the application-and it is instructive to review the thread names-is as follows:

INFO: Loading application [mywebapp] at [/mywebapp]
Feb 11, 2013 11:24:40 AM org.glassfish.deployment.admin.DeployCommand execute
INFO: mywebapp was successfully deployed in 2,452 milliseconds.
Client connecting to server on path http://localhost:8080/mywebapp/reader
Sending data ...
Sleeping ...
init() called on AsyncReaderServlet
doPost() called on AsyncReaderServlet
processRequest() called AsyncReaderServlet on thread [http-listener(2)]
thread [http-listener(2)] data: Beginning Text
Sending more data ...
thread [http-listener(4)] data: Lorem
thread [http-listener(3)] data: ipsum
thread [http-listener(1)] data: dolor
thread [http-listener(5)] data: sit
thread [http-listener(2)] data: amet
thread [http-listener(4)] data: consectetur

thread [http-listener(5)] data: laborum
Finishing client
Check standard console 
thread [http-listener(2)] data: Ending Text
thread [glassfish-web-async-thread-1] onAllDataRead()
Disconnecting and shutdown
Feb 11, 2013 11:24:59 AM org.glassfish.admin.mbeanserver.JMXStartupService shutdown
FileMonitoring shutdown
INFO: JMXStartupService and JMXConnectors have been shut down.
contextDestroyed() on AppServletContextListener 

The threads' names http-listener(1) to http-listener(5) demonstrate that the GlassFish server allocates a thread pool to serve each HTTP push from the test client. Clearly, flushing the output buffer in the test buffer causes data to be sent across the network and then the asynchronous servlet, and its AysncReadListener retrieves the data from the web client. This is seen by the output of one word of the Lorem Ipsum text to the console at a time. Once the stream has finished, the test application closes the HTTP URL connection, which causes the Servlet container to invoke the onAllDataRead() method; notice how the thread is named glassfish-web-async-thread-1.

The actual Java thread will be different with another application server or web container. It is therefore recommended not to rely on specific server semantics.

An asynchronous writer

The Servlet 3.1 specification adds the event listeners for the production of asynchronous output javax.servlet.WriteListener.

The interface declaration for WriteListener is as follows:

package javax.servlet;
public interface WriteListener extends java.util.EventListener {
  public void onWritePossible();
  public void onError(final Throwable t);
  }

After an asynchronous context has been obtained and started, then a WriteListener implementation is registered with a call to the instance method ServletOutputStream.setWriteListener().

Method

Descriptor

onWritePossible

Provided an instance of WriteListener is registered with ServletOutputStream, this method will be invoked by the container the first time, when it is possible to write data. Subsequently, the container will invoke this method if and only if the ServletOutputStream.isReady() method has been called and has returned false. This method is one to write data that pushes data down the stream to the client.

onError

The container invokes this method when something has gone wrong and there is a plausible error. Here, the implementation can take action on the error. The exception is passed to the method.

To illustrate the asynchronous output, we can write a Servlet that streams the Lorem Ipsum text to a web client. The code for the Servlet AsyncWriterServlet is as follows:

package je7hb.servlets.simple;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintStream;
import java.util.StringTokenizer;

@WebServlet(name = "AsyncWriterServlet", urlPatterns = {"/writer"}, asyncSupported = true)
public class AsyncWriterServlet extends HttpServlet {
  
  private static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipisicing " + "elit, sed do eiusmod tempor incididunt ut labore et " + "dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut " + "aliquip ex ea commodo consequat. Duis aute irure " + "dolor in reprehenderit in voluptate velit esse " + "cillum dolore eu fugiat nulla pariatur. Excepteur " + "sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id " + "est laborum.";
  
  @Override
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    System.out.printf("init() called on %s
", getClass().getSimpleName());
    }
  
  public void doGet(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
    System.out.printf("doGet() called on %s
", getClass().getSimpleName());
    processResponse(request, response);
    }
  
  public void doPost(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
    System.out.printf("doPost() called on %s
", getClass().getSimpleName());
    processResponse(request, response);
    }
  
  private void processResponse(HttpServletRequest request, HttpServletResponse response)
  throws IOException {
    System.out.printf("processRequest() called %s" + " on thread [%s]
", getClass().getSimpleName(), Thread.currentThread().getName());
    AsyncContext context = request.startAsync();
    ServletOutputStream output = response.getOutputStream();
    output.setWriteListener(new AsyncWriteListener( output, context));
    }
  /*... inner class ... */
  }

This AsyncWriterServlet Servlet is the opposite of the reader Servlet, in that it creates WriterListener as an event listener. This servlet is annotated with @WebServlet, and it has an attribute asyncSupported set to true. The refactored method processRequest() is the key, in that it creates an asynchronous context. The context is started, and AsyncWriterListener is created and registered on ServletOutputStream.

AsyncWriterServlet utilizes an inner class AsyncWriteListener in order to push data asynchronously to the client endpoint. The listing for this class-and note that it is a type of WriteListener-is as follows:

private class AsyncWriteListener implements WriteListener {
  private ServletOutputStream output;
  private AsyncContext context;
  
  private static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipisicing " + "elit, sed do eiusmod tempor incididunt ut labore et " + /* ... */ est laborum.";
  
  private AsyncWriteListener(ServletOutputStream output, AsyncContext context) {
    this.output = output;
    this.context = context;
    System.out.printf("thread [%s] AsyncWriteListener()
 ", Thread.currentThread().getName());
    }
  
  @Override
  public void onWritePossible() {
    System.out.printf("thread [%s] onWritePossible() " + "Sending data ...
", Thread.currentThread().getName());
    StringTokenizer stk = new StringTokenizer(LOREM_IPSUM," 	,.");
    PrintStream ps = new PrintStream(output);
    try {
      while (output.isReady() && stk.hasMoreTokens()) {
        if (stk.hasMoreTokens()) {
          ps.println(stk.nextToken());
          ps.flush();
          Thread.sleep(200);
          }
        }
      ps.println("End of server *push*");
      ps.flush();
      ps.close();
      System.out.printf("thread [%s] Finished sending ...
",
      Thread.currentThread().getName());
      }
    catch (Exception e) {
      e.printStackTrace(System.err);
      }
    finally {context.complete();}
    }
  
  @Override
  public void onError(Throwable t) {
    System.out.printf("thread [%s] Error occurred=%s
", Thread.currentThread().getName(), t.getMessage());
    context.complete();
    }
  }

In order to generate an asynchronous response, web container invokes the WriterListener instance with calls to the onWritePossible() method. It is likely the method call is invoked on a separate worker thread. The difference between WriterListener and ReaderListener is that the whole thread is responsible for pushing the entire data set asynchronously. AsyncWriteListener sends the data as a word of the Lorem Ipsum at time every 200 milliseconds and flushes the output buffer in order to push the data across the network.

The Servlet container only ever invokes the onError() method if something goes wrong with the server push. The disadvantage of WriteListener seems to be that the entire Java thread is held onto by the whole dataset, because we have to push it fully to the web client. Perhaps it is best to save the pointers on how much of the dataset is written in an instance variable in a business application.

Let's look at the test application that will invoke this Servlet. It is another embedded GlassFish runner application. However, this program will wait for the streamed responses from AsyncWriterServlet.

The code in its entirety is as follows:

package je7hb.common.webcontainer.embedded.glassfish;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

public class EmbeddedAsyncWriterRunner extends AbstractEmbeddedRunner {
  public EmbeddedAsyncWriterRunner(int port) {
    super(port);
    }
  
  public static void main(String args[]) throws Exception {
    EmbeddedAsyncWriterRunner runner = (EmbeddedAsyncWriterRunner)
    new EmbeddedAsyncWriterRunner(8080).init().start();
    runner.deployWithRename("build/libs/ch06-servlets-basic-1.0.war", "mywebapp");
    Thread.sleep(1000);
    String path = String.format("http://localhost:%d/%s/%s", 8080, "mywebapp", "writer");
    URL url = new URL(path);
    System.out.printf("Client connecting to server on path %s
", path);
    HttpURLConnection conn = (HttpURLConnection)
    url.openConnection();
    conn.setChunkedStreamingMode(2);
    conn.setDoInput(true);
    conn.setDoOutput(true);
    conn.connect();
    try (BufferedReader input = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
      System.out.println("Client receiving data ...");
      int len = -1;
      char buffer[] = new char[2048];
      while ((len = input.read(buffer)) != -1 ) {
        String data = new String(buffer,0,len).trim();
        System.out.printf("--> client received data: %s
", data);
        }
      System.out.println("Client finished with receiving data ...");
      }
    System.out.println("Check standard console ");
    Thread.sleep(3000);
    System.out.println("Client disconnecting and shutdown");
    conn.disconnect();
    runner.stop();
    
    System.exit(0);
    }
  }

EmbeddedAsyncWriterRunner is another container-less application that executes an embedded GlassFish server, and deploys a WAR file to it. The program makes an HTTP URL connection to the deployed Servlet and sends a request to it. The program waits on the connection for Servlet to respond. It dumps each successful read of the input stream, which is a word from the Lorem Ipsum text, to the standard output. After the input stream is closed, because AsysncWriteServlet closes the connection, the application will shut down the embedded runner, and then terminate itself.

The output of executing this program is as follows:

INFO: mywebapp was successfully deployed in 2,587 milliseconds.
Client connecting to server on path http://localhost:8080/mywebapp/writer
init() called on AsyncWriterServlet
doPost() called on AsyncWriterServlet
processRequest() called AsyncWriterServlet on thread [http-listener(2)]
thread [http-listener(2)] AsyncWriteListener()
thread [http-listener(2)] onWritePossible() Sending data ...
Client receiving data ...
--> client received data: Lorem
--> client received data: ipsum
--> client received data: dolor
--> client received data: sit
--> client received data: amet
--> client received data: consectetur

--> client received data: laborum
--> client received data: End of server *push*
thread [http-listener(2)] Finished sending ...
Client finished with receiving data ...
Check standard console 
Client disconnecting and shutdown
Feb 11, 2013 9:08:36 PM org.glassfish.admin.mbeanserver.JMXStartupService shutdown
FileMonitoring shutdown
INFO: JMXStartupService and JMXConnectors have been shut down.
contextDestroyed() on AppServletContextListener

INFO: Shutdown procedure finished

In the GlassFish server implementation, a Java thread http-listener(2) is used to push the data entirely to the web client. However, because the application uses the standard API, this will be hidden from developers. The Servlet container providers are free to develop implementations that will execute from the thread pool services that give the best performance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.187.62