Servlets and JavaServer Pages: The J2EE Technology Web Tier

Wrappers

Another very powerful feature of Filters is the ability to optionally wrap a request and/or a response. Wrapping is used to encapsulate a given request or response inside another (customized) one. The benefit of wrapping comes from custom coding a particular wrapping object to manipulate a request or response in a way not normally done. Wrappers are an extremely important part of Filters. There are many uses for this sort of functionalitysome of which are demonstrated in this chapter and later in the book.

A side point to note about wrappers is they are not a feature specific to Filters. The functionality and associated classes were introduced with Filters in the Servlet 2.3 specifications, but request and response wrapping can be done by a normal Servlet. Commonly, this fact is ignored, but as the topic is discussed further, keep in mind that theRequestDispatcher forward() and include() methods can be used with wrappers to do the exact same thing.

Request Wrapping

A request and a response are wrapped differently. A wrapper is really just an implementation of the respective object being wrapped. A request wrapper would be an implementation of ServletRequest. In the case of an HttpServletRequest, the wrapper would be an implementation of HttpServletRequest. The Servlet API provides the ServletRequestWrapper andHttpServletRequestWrapper classes as wrappers for subclassing. Creating a new custom wrapper can be done by extending the appropriate class and custom coding the desired methods.

In the case of wrapping a request there have been few good uses of the functionality. Pre-processing of request information is normally done to populate objects with information that is later displayed by a Servlet or JSP. In particular, JavaBeans are commonly populated by Filters so the information can easily be shown by a JSP via thegetProperty standard action or the EL. In this common use case, a request wrapper works, but it is usually easier to populate the JavaBean using a few lines of code and setting it in request scope.

There are a few use cases where request wrapping is absolutely needed. These cases occur whenever the functionality in either the ServletRequest or HttpServletRequest methods needs to be changed. As an example of one of these use cases, imagine a customization of the HttpServletRequest object to provide audit information. Information about the invocation is recorded to a log file each time one of the methods is invoked. Normally this type of auditing would be difficult to accomplish as it would involve editing the source code for the HttpServletRequest implementation, but request wrapping easily provides the functionality. The wrapper in Listing 8-8 is a customized version of HttpServletRequest that leaves an audit trail of method invocations.

Listing 8-8. Audit Trail Wrapper for HttpServletRequest
package com.jspbook;

 

import java.io.*;
import java.util.logging.*;
import javax.servlet.*;
import javax.servlet.http.*;

class AuditRequestWrapper extends HttpServletRequestWrapper {
  Logger logger;
  public AuditRequestWrapper(Logger logger,
      HttpServletRequest request) {
    super(request);
    this.logger = logger;
  }

  public String getContentType() {
    String contentType = super.getContentType();
    logger.fine("ContentType[ " + contentType + "]");
    return contentType;
  }

  public int getContentLength() {
    int contentLength = super.getContentLength();
    logger.fine("getContentLength[" + contentLength + "]");
    return contentLength;
  }

  public long getDateHeader(String s) {
    long date =  super.getDateHeader(s);
    logger.fine("getDateHeader[" + s + ": " + date + "]");
    return date;
  }

  public String getHeader(String s) {
    String header = super.getHeader(s);
    logger.fine("getHeader[" + s + ": " + header + "]");
    return header;
  }

  public int getIntHeader(String s) {
    int header = super.getIntHeader(s);
    logger.fine("getIntHeader[" + s + ": " + header + "]");
    return header;
  }

  public String getQueryString() {
    String queryString = super.getQueryString();
    logger.fine("getQueryString[" + queryString + "]");
    return queryString;
  }
  // other methods left out for clarity
}

 

The wrapper is very simple. As shown here it logs calls to certain methods using the logger available to the Web Application. Notice that the wrapper has a constructor that takes a Logger and an HttpServletRequest. This is needed by the wrapper class the Filter uses. Listing 8-9 includes the code for the wrapper.

Listing 8-9. AuditFilter.java
package com.jspbook;

 

import java.io.*;
import java.util.logging.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class AuditFilter implements javax.servlet.Filter {

  public void doFilter(ServletRequest req, ServletResponse res,
      FilterChain chain) throws IOException, ServletException {
    Logger logger = SiteLogger.getLogger();
    AuditRequestWrapper wrappedRequest =
      new AuditRequestWrapper(logger, (HttpServletRequest)req);
    chain.doFilter(wrappedRequest, res);
  }

  public void init(FilterConfig filterConfig) {
    // noop
  }

  public void destroy() {
    // noop
  }
}

 

Notice where the Filter wraps the request. Before calling the chain.doFilter() method, the AuditFilter wraps theHttpServletRequest object with the AuditRequestWrapper class.

 

...
    Logger logger = SiteLogger.getLogger();
    AuditRequestWrapper wrappedRequest =
      new AuditRequestWrapper(logger, (HttpServletRequest)req);
    chain.doFilter(wrappedRequest, res);
...

 

Now subsequent resources, such as JSP or Servlets that use the wrapped request, will leave the audit trail of method invocation information.

Test out the AuditFilter by deploying it with the jspbook Web Application. Add the following entry to web.xml.

 

<filter>
  <filter-name>AuditFilter</filter-name>
  <filter-class>com.jspbook.AuditFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>AuditFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

 

Save the changes to web.xml, compile AuditFilter.javaAuditRequestWrapper.java, and reload the jspbook Web Application for the changes to take effect. Resources in the jspbook Web Application now leave an audit trail of the methods they invoke on the HttpServletRequest object.

A direct test of the AuditFilter can be given with a simple JSP. Save Listing 8-10 as TestAuditFilter.jsp in the base directory of the jspbook Web Application.

Listing 8-10. TestAuditFilter.jsp
<html>
<body>
Testing the Audit Filter.
<%
    request.getContentLength();
    request.getHeader("Host");
%>
</body>
</html>

 

Browse to http://127.0.0.1/jspbook/TestAuditFilter.jsp to test the Filter. The output of the JSP does not change. It is a simple line of text, "Testing the Audit Filter". The important point to note is that the API calls the JSP is using are leaving an audit trail[2]. The trail looks something similar to the following.

[2] The Audit Filter logs at the FINE level of the java.util.logging package. If the SiteLogger Servlet is configured to ignore FINE messages, then you will not see the audit trail. Browse tohttp://127.0.0.1/jspbook/SiteLoggerAdmin.jsp?level=ALL to change the logging level appropriately.

 

May 15, 2002 3:10:28 PM com.jspbook.AuditRequestWrapper getQueryString
INFO: getQueryString[null]
May 15, 2002 3:10:28 PM com.jspbook.AuditRequestWrapper getContentLength
INFO: getContentLength[-1]
May 15, 2002 3:10:28 PM com.jspbook.AuditRequestWrapper getHeader
INFO: getHeader[Host: 127.0.0.1]

 

Notice the JSP calls to the getContentLength() method and the getHeader() methods are logged. However, the Filter also logs a call to the getQueryString() method even though it is not explicitly called by the JSP. The getQueryString()method must be called by the container. The important thing to realize is that all calls should go through the wrapper classes, even calls made by the container. In other words, the container should not access the underlying request and response objects directly through internal APIs. It should always follow the contract of accessing the objects through the request and response interfaces.

Response Wrapping

Manipulating the response is much more difficult than wrapping the request. The request object is substantially read only, whereas a response contains lots of generated information including the output data. If you want to manipulate the output data, the information needs to either first be captured or a custom writer object needs to be used. Either of these changes usually means the HTTP response headers need to be also customized, particularly Content-Length andContent-Type. Wrapping a response is done in a similar fashion as wrapping a request. The Servlet API includes theServletResponseWrapper and HttpServletResponseWrapper classes, which can easily be extended and customized.

Along with the complexity of wrapping a response comes a great deal of functionality. Being able to capture and manipulate output content allows a filter to have full authority over what a client sees. This functionality can be put to many good uses, and it is well worth understanding response wrapping.

Compression Filters

Arguably one of the best uses of response wrapping is to provide dynamic compression of content. The idea behind a compression Filter is sending less content to a client means the content downloads faster. This concept is absolutely true; whenever the same information can be sent using less space, it is desirable, especially when dealing with low-bandwidth clients.

Compression works by eliminating redundant information. Most text files are full of redundancy, particularly in the case of markup languages such as HTML. Compressing a lengthy chunk of HTML or DHTML can result in a size decrease by a factor of six. Compression cannot blindly be applied to all content in a Web Application. Web browsers expect to see information in a format they can understand. Most often a developer simply assumes that every browser can understand HTML sent with the standard encoding. This mindset works, but completely ignores optimizations many Web browsers implement, namely, compression. You'll recall the ShowHeaders Servlet built in Chapter 2 (Listing 2-8). This Servlet dumps a listing of all the HTTP request headers sent by a client. While not used in that example, we can take advantage of them now, specifically the user-encoding header. This HTTP header provides a listing of all the encoding formats the client's software understands. If a client lists a compression format here, it means the browser can understand content compressed using the specific format.

Here is a sample listing of the user-encoding header from Chapter 2:

 

accept-encoding: gzip, deflate, compress;q=0.9

 

Notice the listing of the popular GZIP compression algorithm. This algorithm is commonly implemented by current Web browsers and provides an easy method of compressing the output of a lengthy Web page. The Java 2 Standard Edition conveniently includes the GzipOutputStream class that is a stream implementation of the compression algorithm. By combining this class with a simple Filter, we can dynamically compress the output of a lengthy Web page. The end result is that a client that supports gzip compression will receive the exact same content as other clients, but will only have to wait a fraction of the time to download it.

A simple compression filter is quite handy to have. The following code is such a Filter. Save Listing 8-11 asGZIPFilter.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-11. GZIPFilter.java
package com.jspbook;

 

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class GZIPFilter implements Filter {

  public void doFilter(ServletRequest req, ServletResponse res,
      FilterChain chain) throws IOException, ServletException {
    if (req instanceof HttpServletRequest) {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;
      String ae = request.getHeader("accept-encoding");
      if (ae != null && ae.indexOf("gzip") != -1) {
        System.out.println("GZIP supported, compressing.");
        GZIPResponseWrapper wrappedResponse =
          new GZIPResponseWrapper(response);
        chain.doFilter(req, wrappedResponse);
        wrappedResponse.finishResponse();
        return;
      }
      chain.doFilter(req, res);
    }
  }

  public void init(FilterConfig filterConfig) {
    // noop
  }

  public void destroy() {
    // noop
  }
}

 

As mentioned earlier, response manipulating Filters are a bit more difficult to properly code. This occurs because response manipulating Filters need to both capture the original output and also reset HTTP response headers to match the changes. The preceding code for the GZIP Filter illustrates this point. The GZIP Filter by itself does little. In the main doFilter() method it checks to see if the client supports GZIP compression.

 

String ae = request.getHeader("accept-encoding");
if (ae != null && ae.indexOf("gzip") != -1) {
  GZIPServletResponseWrapper wrappedResponse =
    new GZIPServletResponseWrapper(response);
  chain.doFilter(req, wrappedResponse);
  wrappedResponse.finishResponse();
  return;

 

If so, a GZIPResponseWrapper is used. If not, the Filter does nothing. The GZIPResponseWrapper is being used to capture the normally generated content to pipe it through a GZIPOutputStream. The GZIPResponseWrapper class is also a custom-made class. Here is the code for the GZIPResponseWrapper. Save Listing 8-12 as GZIPResponseWrapper.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-12. GZIPResponseWrapper
package com.jspbook;

 

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class GZIPResponseWrapper extends HttpServletResponseWrapper
{
  protected HttpServletResponse origResponse = null;
  protected ServletOutputStream stream = null;
  protected PrintWriter writer = null;

  public GZIPResponseWrapper(HttpServletResponse response) {
    super(response);
    origResponse = response;
  }

  public ServletOutputStream createOutputStream() throws IOException {
    return (new GZIPResponseStream(origResponse));
  }

  public void finishResponse() {
    try {
      if (writer != null) {
        writer.close();
      } else {
        if (stream != null) {
          stream.close();
        }
      }
    } catch (IOException e) {}
  }

  public void flushBuffer() throws IOException {
    stream.flush();
  }

  public ServletOutputStream getOutputStream() throws IOException {
    if (writer != null) {
      throw new IllegalStateException("getWriter() has already been called!");
    }

    if (stream == null)
      stream = createOutputStream();
    return (stream);
  }

  public PrintWriter getWriter() throws IOException {
    if (writer != null) {
      return (writer);
    }

    if (stream != null) {
      throw new IllegalStateException("getOutputStream() has already been called!");
    }

   stream = createOutputStream();
   writer = new PrintWriter(new OutputStreamWriter(stream, "UTF-8"));
   return (writer);
  }

  public void setContentLength(int length) {}
}

 

The preceding code performs only one important task: use GZIPResponseStream instead of ServletOutputStream. The rest of the code is just filler to ensure the wrapper class uses the GZIPResponseStream with all the HttpServletResponsemethods. The GZIPResponseStream is where all the work is done. In this class the content that is normally sent directly to a client is instead captured in a buffer and piped through a GZIPOutputStream. The compressed content is then sent to a client along with corrections to the appropriate HTTP headers. As with the GZIPResponseWrapper, the GZIPResponseStreamis also a custom class needed for this example. Here is the code for the GZIPResponseStream class. Save Listing 8-13 asGZIPResponseStream.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-13. GZIPResponseStream.java
package com.jspbook;

 

import java.io.*;
import java.util.zip.GZIPOutputStream;
import javax.servlet.*;
import javax.servlet.http.*;

public class GZIPResponseStream extends ServletOutputStream {
  protected ByteArrayOutputStream baos = null;
  protected GZIPOutputStream gzipstream = null;
  protected boolean closed = false;
  protected HttpServletResponse response = null;
  protected ServletOutputStream output = null;

  public GZIPResponseStream(HttpServletResponse response) throws IOException {
    super();
    closed = false;
    this.response = response;
    this.output = response.getOutputStream();
    baos = new ByteArrayOutputStream();
    gzipstream = new GZIPOutputStream(baos);
  }

  public void close() throws IOException {
    if (closed) {
      throw new IOException("This output stream has already been closed");
    }
    gzipstream.finish();

    byte[] bytes = baos.toByteArray();


    response.addHeader("Content-Length",
                       Integer.toString(bytes.length));
    response.addHeader("Content-Encoding", "gzip");
    output.write(bytes);
    output.flush();

    output.close();
    closed = true;
  }

  public void flush() throws IOException {
    if (closed) {
      throw new IOException("Cannot flush a closed output stream");
    }
    gzipstream.flush();
  }

  public void write(int b) throws IOException {
    if (closed) {
      throw new IOException("Cannot write to a closed output stream");
    }
    gzipstream.write((byte)b);
  }

  public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
  }

  public void write(byte b[], int off, int len) throws IOException {
    System.out.println("writing...");
    if (closed) {
      throw new IOException("Cannot write to a closed output stream");
    }
    gzipstream.write(b, off, len);
  }

  public boolean closed() {
    return (this.closed);
  }

  public void reset() {
    //noop
  }
}

 

The GZIPResponseStream has many important points to understand. The first is what the class is doing. With more complex Filters, a Web developer can completely override the normal functionality found with the Servlet API. In this case theServletOutputStream object is given a major rework. Such a radical change to one of the base classes of Servlets is possible thanks to the custom response wrapper. The change works because the class extends ServletOutputStream.

 

...
public class GZIPResponseStream extends ServletOutputStream {
...

 

The response wrapper, GZIPServletResponseWrapper, then takes advantage of Java polymorphism and uses the class as if it were a ServletOutputStream.

The next important point is what the GZIPResponseStream is doing with information written to it by a JSP, Servlet, or static page. The information is not sent to a client; instead, it is written to a java.util.zip.GZIPOutputStream that is buffered locally.

 

...
  public void write(int b) throws IOException {
    ...
    gzipstream.write((byte)b);
  }

 

  public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
  }

  public void write(byte b[], int off, int len) throws IOException {
    ...
    gzipstream.write(b, off, len);
  }

 

After the entire output is buffered and compressed, the compressed content is then sent directly to the client along with modified HTTP response headers. The close() method of the GZIPResponseStream is responsible for this as it is guaranteed to be called last and only once for each response.

 

...
  public void close() throws IOException {
    if (closed) {
      throw new IOException("This output stream has already been closed");
    }

 

    gzipstream.finish();
    //get the compressed bytes
    byte[] bytes = baos.toByteArray();

    response.addHeader("Content-Encoding", "gzip");
    output.write(bytes);
    output.flush();
    output.close();
    closed = true;
  }
...

 

The end result is that should a client support GZIP compression, the GZIPFilter automatically compresses content requested by that client. Should compression not be supported, then the normal, uncompressed form of the content is sent. Test out the Filter by deploying it with the jspbook Web Application. Add Listing 8-14 into web.xml to apply theGZIPFilter to all resources in the jspbook Web Application.

Listing 8-14. GZIPFilter web.xml Entry
<filter>
  <filter-name>GZIPFilter</filter-name>
  <filter-class>com.jspbook.GZIPFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>GZIPFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

 

Save the preceding changes to web.xml, compile GZIPFilter.javaGZIPResponseWrapper.java, and GZIPResponseStream.java, and reload the jspbook Web Application for the changes to take effect. The Filter can be tested by visiting any of the existing examples made by the jspbook Web Application. Should your browser support compression, the example's output is compressed before being sent. To double-check if your browser does support compression, revisit the ShowHeaders Servlet,http://127.0.0.1/jspbook/ShowHeaders. If the accept-encoding header includes the value gzip, then your browser does.

Unfortunately for the sake of learning, the process of compression on the server-side and decompression on the client-side is done seamlessly to an end user. To see the actual compression yourself, a lower view HTTP is required. Recall inChapter 2 with the introduction to HTTP, an example used the telnet program to spoof an HTTP request. By doing this, the raw contents of an HTTP request and response can be seen. To see the effect of the GZIPFilter, spoof another HTTP request for a resource in the jspbook Web Application. Take, for example, the welcome page,http://127.0.0.1/jspbook/welcome.html. Spoof an HTTP request by running telnet on port 80.

 

telnet 127.0.0.1 80

 

After the program loads manually, make the HTTP request.

 

GET /jspbook/welcome.html HTTP/1.0

 

The result is an HTTP response with the contents of the page. Figure 8-6 shows the entire process.

Figure 8-6. Spoofed HTTP Request to welcome.html

graphics/08fig06.gif

 

Figure 8-6 is not compressed. This is because the spoofed HTTP request did not include an HTTP header to inform the server it accepts content encoded via GZIP comjjpression. Redo the spoofed request, but this time add the following line to the request.

 

GET /jspbook/welcome.html HTTP/1.0
accept-encoding:gzip;

 

Now the response content is compressed. The request and response are shown in Figure 8-7.

Figure 8-7. Spoofed HTTP Request with Compression to welcome.html

graphics/08fig07.jpg

 

The content is gibberish, but compressed gibberish. The important point is that the GZIP compression Filter is working. Should a client accept compressed content, it will be sent. Manually we cannot easily decompress the content, but should a browser be reading this response, it would have little trouble decompressing and rendering the content.

Compression: What Has Been Gained?

The compression Filter is one of the more lengthy examples of this book, but it is also one of the more helpful ones. A compression Filter is a re-usable component that can be applied to just about every Web Application. The strong point of this Filter is that it reduces the amount of information a client must download. The weak point is that it adds extra post-processing to the time it takes to generate the response. In most cases the time it takes to generate a response is dwarfed by the time it takes for a client to download the response. So usually using a plain compression Filter such as the GZIP Filter is always a helpful addition, but for optimum efficiency combine the compression Filter with a cache Filter.

It has been mentioned quite a few times that the GZIP compression algorithm is good because it shrinks the size of the content being sent to a client. Hopefully, you have accepted the usefulness of the compression algorithm, but for clarity let's see just how helpful the compression is. A simple Servlet can show the point. Save Listing 8-15 asCompressionTest.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-15. CompressionTest.java
package com.jspbook;

 

import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class CompressionTest extends HttpServlet {

  public void doGet(HttpServletRequest request,
                    HttpServletResponse response)
  throws IOException, ServletException {

    response.setContentType("text/html");
    PrintWriter out = response.getWriter();
    out.println("<html>");
    out.println("<head>");
    out.println("<title>Compression Test</title>");
    out.println("</head>");
    out.println("<body>");
    out.println("<h1>Compression Test</h1>");
    out.println("<form>");
    String url = request.getParameter("url");
    if (url != null) {
      out.print("<input size=\"50\" name=\"url\" ");
      out.println("value=\""+url+"\">");
    } else {
      out.println("<input size=\"50\" name=\"url\">");
    }
    out.print("<input type=\"submit\" value=\"Check\">");
    out.println("</form>");
    out.println("URL: "+ url);
    if (url != null) {
      URL noCompress = new URL(url);
      HttpURLConnection huc =
        (HttpURLConnection)noCompress.openConnection();
      huc.setRequestProperty("user-agent","Mozilla(MSIE)");
      huc.connect();
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      InputStream is = huc.getInputStream();
      while(is.read() != -1) {
        baos.write((byte)is.read());
      }
      byte[] b1 = baos.toByteArray();

      URL compress = new URL(url);
      HttpURLConnection hucCompress =
        (HttpURLConnection)noCompress.openConnection();
      hucCompress.setRequestProperty("accept-encoding","gzip");
      hucCompress.setRequestProperty("user-agent","Mozilla(MSIE)");
      hucCompress.connect();
      ByteArrayOutputStream baosCompress =
        new ByteArrayOutputStream();
      InputStream isCompress = hucCompress.getInputStream();
      while(isCompress.read() != -1) {
        baosCompress.write((byte)isCompress.read());
      }
      byte[] b2 = baosCompress.toByteArray();

      out.print("<pre>");
      out.println("Uncompressed: " + b1.length);
      out.println("Compressed: " + b2.length);
      out.print("Space saved: "+(b1.length-b2.length)+", or ");
      out.println((((b1.length - b2.length)*100)/b1.length)+"%");
      out.println("Downstream(2kbps)");
      out.println(" No GZIP: "+(float)b1.length/2000+"seconds");
      out.println(" GZIP:    "+(float)b2.length/2000+"seconds");
      out.println("Downstream(5kbps)");
      out.println(" No GZIP: "+(float)b1.length/5000+"seconds");
      out.println(" GZIP:    "+(float)b2.length/5000+"seconds");
      out.println("Downstream(10kbps)");
      out.println(" No GZIP: "+(float)b1.length/10000+"seconds");
      out.println(" GZIP:    "+(float)b2.length/10000+"seconds");
      out.println("</pre>");
    }
    out.println("</body>");
    out.println("</html>");
  }
}

 

The preceding code uses a Servlet that uses an HTML page to create a form to query for a URL. After getting a URL, thejava.net.URL class is used to open a connection up to the URL and receive the response. A second URL is opened up to the same connection and requests a compressed version of the same information. After getting the two, the size of the content is compared and displayed in HTML. Figure 8-8 shows a rendering of the results when testinghttp://127.0.0.1/jspbook/welcome.html.

Figure 8-8. CompressionTest Servlet on welcome.html

graphics/08fig08.gif

 

For welcome.html the results are unimpressive. After all this work, the Filter is only saving 16% of the time it takes to download the content. You can try any other example in the jspbook Web Application and you'll see similar results. The 16% hardly seems like it is worth saving. One could argue with really big downloads the 16% is noticeable, but that is not enough justification to Filter content with GZIP compression. The fact is that GZIP is a good algorithm and it can produce up to about 1:6 compression ratio, but the GZIP algorithm approaches this ratio only with highly redundant information or with lots of information so that more redundancy is more likely to occur. All of the examples in the jspbook Web Application are simply too small!

Take into consideration a more realistic example. Consider the main page of JSP Insider, http://www.jspinsider.com. This is a fair example of a complex page that has plenty of styled content and some DHTML. Fortunately JSP Insider uses the same GZIP Filter. Try running the compression test Filter on http://www.jspinsider.comFigure 8-9 shows the results.

Figure 8-9. CompressionTest Filter on http://www.jspinsider.com

graphics/08fig09.gif

 

The results are quite impressive! On a more realistic Web page the compressed content is 79% smaller than the uncompressed. It is about 5 times faster to download the page when using the GZIP Filter.

The lesson learned is the GZIP compression Filter is most helpful when there is a lot of content. For small examples, usually up to 5k, there is really no good reason to compress the content. However, in larger pages, 5k+, the benefit really starts to show.

Cache Filters

Caching is another one of the more helpful uses of response wrapping. The idea behind a cache Filter is to minimize the amount of time it takes to produce a dynamic page. This functionality complements compression Filters and can greatly reduce the overhead involved in using dynamic resources such as JSP and Servlets.

A cache Filter works by keeping a copy of a response in memory or on a local disk and re-using it for future requests to the same resource. By doing this the cache Filter can intercept an incoming request, check if the cache for it is valid, and optionally send the cached version of the response. The time benefit comes from using the cache instead of generating the dynamic response. In cases where a Servlet or JSP performs some complex logic or accesses some time-consuming resources, a caching mechanism can greatly increase performance.

The downside to caching is the removal of dynamic generation; some pages cannot be cached. The whole point of using dynamic pages is to create a page on the fly that cannot be done statically. A cache is simply a static version of a page's output. Dynamic pages and caches seem to conflict, but in practice there are plenty of places caching is handy. Consider that not all dynamic pages change each time a client makes a request. Plenty of dynamic pages are only dynamic because they include resources that may possibly change, but are likely not to. One of the most common cases is a dynamic include such as a header or footer. Using a dynamic include is nice, but the end result of the page does not change until one of the includes changes. All of the requests between changes receive the same response. Caching in this case can be very effective as long as the cache is reset occasionally or whenever the includes are changed. The same type of caching can be applied to news pages or listings of links generated by a database.

The point is a good cache Filter is helpful to have because not all dynamic resources need to be generated every time a client makes a request. We have yet to see some good examples in this book to apply a cache filter to, but there will be a few appearing in later chapters. For those examples and to give a concrete example of caching, we are now going to build a simple cache Filter. Save Listing 8-16 as CacheFilter.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-16. CacheFilter.java
package com.jspbook;

 

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;

public class CacheFilter implements Filter {

  ServletContext sc;
  FilterConfig fc;
  long cacheTimeout = Long.MAX_VALUE;

  public void doFilter(ServletRequest req, ServletResponse res,
                       FilterChain chain)
                       throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    // check if was a resource that shouldn't be cached.
    String r = sc.getRealPath("");
    String path = fc.getInitParameter(request.getRequestURI());
    if (path!= null && path.equals("nocache")) {
      chain.doFilter(request, response);
      return;
    }
    path = r+path;

    // customize to match parameters
    String id = request.getRequestURI()+request.getQueryString();

    // optionally append i18n sensitivity
    String localeSensitive = fc.getInitParameter("locale-sensitive");
    if (localeSensitive != null) {
      StringWriter ldata = new StringWriter();
      Enumeration locales = request.getLocales();
      while (locales.hasMoreElements()) {
        Locale locale = (Locale)locales.nextElement();
        ldata.write(locale.getISO3Language());
      }
      id = id + ldata.toString();
    }

    File tempDir = (File)sc.getAttribute(
      "javax.servlet.context.tempdir");

    // get possible cache
    String temp = tempDir.getAbsolutePath();
    File file = new File(temp+id);

    // get current resource
    if (path == null) {
      path = sc.getRealPath(request.getRequestURI());
    }
    File current = new File(path);

    try {
      long now = Calendar.getInstance().getTimeInMillis();
      //set timestamp check
      if (!file.exists() || (file.exists() &&
          current.lastModified() > file.lastModified()) ||
          cacheTimeout < now - file.lastModified()) {
        String name = file.getAbsolutePath();
        name = name.substring(0,name.lastIndexOf("/"));
        new File(name).mkdirs();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        CacheResponseWrapper wrappedResponse =
          new CacheResponseWrapper(response, baos);
        chain.doFilter(req, wrappedResponse);

        FileOutputStream fos = new FileOutputStream(file);
        fos.write(baos.toByteArray());
        fos.flush();
        fos.close();
      }
    } catch (ServletException e) {
      if (!file.exists()) {
        throw new ServletException(e);
      }
    }
    catch (IOException e) {
      if (!file.exists()) {
        throw e;
      }
    }

    FileInputStream fis = new FileInputStream(file);
    String mt = sc.getMimeType(request.getRequestURI());
    response.setContentType(mt);
    ServletOutputStream sos = res.getOutputStream();
    for (int i = fis.read(); i!= -1; i = fis.read()) {
      sos.write((byte)i);
    }
  }

  public void init(FilterConfig filterConfig) {
    this.fc = filterConfig;
    String ct = fc.getInitParameter("cacheTimeout");
    if (ct != null) {
      cacheTimeout = 60*1000*Long.parseLong(ct);
    }
    this.sc = filterConfig.getServletContext();
  }

  public void destroy() {
    this.sc = null;
    this.fc = null;
  }
}

 

The preceding code works by keeping a cache of all resources it is applied to, in the Servlet-defined temporary working directory. If a cache doesn't exist and a response can be cached, the Filter wraps the response and invokes thedoFilter() method in order to have the container generate a response. Then the Filter both caches the response and sends the content to a client. On subsequent requests for the same resource, the response is used straight from the cache, completely skipping the doFilter() method.

Given the preceding description, the Filter's code is easy to understand. First, a check is made to see if a resource should be cached.

 

// check if was a resource that shouldn't be cached.
String path = fc.getInitParameter(request.getRequestURI());
if (path!= null && path.equals("nocache")) {
  chain.doFilter(request, response);
  return;
}

 

By default it is assumed all resources should be cached, but a URL can be ignored by setting an initial parameter of the same name with a value of "nocache". For example, if /index.jsp should not be cached, the Filter would have a initial parameter named "/index.jsp" set with the value "nocache".

Should a response be valid for caching, the Filter checks to see if it has already been cached.

 


// customize to match parameters
String id = request.getRequestURI()+request.getQueryString();

 

// optionally append i18n sensitivity[3]
...

File tempDir = (File)sc.getAttribute(
  "javax.servlet.context.tempdir");

// get possible cache
String temp = tempDir.getAbsolutePath();
File file = new File(temp+id);

// get current resource
if (path == null) {
  path = sc.getRealPath(request.getRequestURI());
}
File current = new File(path);

try {
  long now = Calendar.getInstance().getTimeInMillis();
  //set timestamp check
  if (!file.exists() || (file.exists() &&
      current.lastModified() > file.lastModified()) ||
      cacheTimeout < now - file.lastModified()) {

 

[3] Internationalization (i18N) is covered by a later graphics/ccc.gifchapter, but a good cache Filter should check more than just the URL to determine if graphics/ccc.gifcached content should be used. In Chapter 15 a proper discussion will address why this is so graphics/ccc.gifimportant.

Two File objects are created: one for the possible cachethat is, the saved response in the temporary work directoryand one for the file the container used to generate a responsethat is, a JSP. If the cache doesn't exist or the current file has a newer time-stamp, meaning the page has been updated and the cache is invalid, a new cache is made.

 

try {
  long now = Calendar.getInstance().getTimeInMillis();
  //set timestamp check
  if (!file.exists() || (file.exists() &&
      current.lastModified() > file.lastModified()) ||
      cacheTimeout < now - file.lastModified()) {
    String name = file.getAbsolutePath();
    name = name.substring(0,name.lastIndexOf("/"));
    new File(name).mkdirs();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    CacheResponseWrapper wrappedResponse =
      new CacheResponseWrapper(response, baos);
    chain.doFilter(req, wrappedResponse);

 

    FileOutputStream fos = new FileOutputStream(file);
    fos.write(baos.toByteArray());
    fos.flush();
    fos.close();
  }
}

 

The new cache is made by wrapping the response with a CacheResponseWrapper object, which we will code shortly, that is assumed to take an output stream and write the contents of a response to it. Ideally, the output stream is aByteArrayOutputStream, making it possible to buffer a complete response. A successfully buffered response is then cached as a file in the Temporary Work directory.

After a new cache is made or the filter decides to use an existing cache, the cached file is read and sent as the response.

 

FileInputStream fis = new FileInputStream(file);
String mt = sc.getMimeType(request.getRequestURI());
response.setContentType(mt);
ServletOutputStream sos = res.getOutputStream();
for (int i = fis.read(); i!= -1; i = fis.read()) {
  sos.write((byte)i);
}

 

Information about the responses MIME type is taken directly from web.xml. It is assumed a mime-mapping element is set for each type of content being cached. By default Tomcat includes all of the common mappings; however, if you were to use a non-conventional URL ending, such as ".xhtml" for XHTML, a new mime-mapping element is required in web.xml.

 

<mime-mapping>
<extension>xhtml</extension>
<mime-type>application/xhtml+xml</mime-type>
</mime-mapping>

 

The end result is that the Filter caches responses and uses its cache to skip the time required to generate dynamic pages. Cached content is always properly encoded and sent back identically, as if it was the generated response. However, one problem still exists. What about pages that slowly change? Pages that can be cached but need to be updated every once in a whilefor example, a news page that is generated by querying a database? It may be fine to cache the page for performance, but the cache should automatically expire every so often in order to refresh the news, regardless if the code for the news page changes or not. The cache Filter also provides this sort of functionality, by allowing a cache time-out value to be specified as an initial parameter. The parameter's name is cacheTimeout, and the value is the number of minutes before a cache should be invalidated.

CacheFilter.java is only part of the complete cache Filter. In order to create cached copies of a response, the response itself needs to be captured. The CacheResponseWrapper class is used by the CacheFilter to capture and cache responses. The code for the CacheResponseWrapper class is very similar to the GZIPResponseWrapper and is as follows. Save Listing 8-17 as CacheResponseWrapper.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-17. CacheResponseWrapper.java
package com.jspbook;

 

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class CacheResponseWrapper
    extends HttpServletResponseWrapper {
  protected HttpServletResponse origResponse = null;
  protected ServletOutputStream stream = null;
  protected PrintWriter writer = null;
  protected OutputStream cache = null;

  public CacheResponseWrapper(HttpServletResponse response,
      OutputStream cache) {
    super(response);
    origResponse = response;
    this.cache = cache;
  }

  public ServletOutputStream createOutputStream()
      throws IOException {
    return (new CacheResponseStream(origResponse, cache));
  }

  public void flushBuffer() throws IOException {
    stream.flush();
  }

  public ServletOutputStream getOutputStream()
      throws IOException {
    if (writer != null) {
      throw new IllegalStateException(
        "getWriter() has already been called!");
    }

    if (stream == null)
      stream = createOutputStream();
    return (stream);
  }

  public PrintWriter getWriter() throws IOException {
    if (writer != null) {
      return (writer);
    }

    if (stream != null) {
      throw new IllegalStateException(
        "getOutputStream() has already been called!");
    }

   stream = createOutputStream();
   writer = new PrintWriter(new OutputStreamWriter(stream, "UTF-8"));
   return (writer);
  }
}

 

There is only one important point to note about CacheResponseWrapper.java. Instead of a ServletResponseStream, the class returns a CacheResponseStream.

 

public ServletOutputStream createOutputStream()
    throws IOException {
  return (new CacheResponseStream(origResponse, cache));
}

 

A clear pattern should be appearing with output-capturing response wrappers. The wrapper class itself usually has little to do with capturing a response. All of the logic involved in creating a response is encapsulated by theServletOutputStream class. Custom wrapper objects are commonly just a method of changing the default ServletOutputStreamreturned by a ServletResponse to a custom subclass of ServletOutputStream. In this case the custom class isCacheResponseStream. The important caching code occurs in the CacheResponseStream class.

The CacheResponseStream class is used by the cache Filter to save a response's content to a given output stream. As the cache Filter uses the class, a ByteArrayOutputStream object is used as the output stream and a response is kept as an array of bytes. The cached bytes are then used to save the response as a file and to send the response to the client. The code for the CacheResponseStream is as follows. Save Listing 8-18 as CacheResponseStream.java in the /WEB-INF/class/com/jspbook directory of the jspbook Web Application.

Listing 8-18. CacheResponseStream.java
package com.jspbook;

 

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class CacheResponseStream extends ServletOutputStream {
  protected boolean closed = false;
  protected HttpServletResponse response = null;
  protected ServletOutputStream output = null;
  protected OutputStream cache = null;

  public CacheResponseStream(HttpServletResponse response,
      OutputStream cache) throws IOException {
    super();
    closed = false;
    this.response = response;
    this.cache = cache;
  }

  public void close() throws IOException {
    if (closed) {
      throw new IOException(
        "This output stream has already been closed");
    }

    cache.close();
    closed = true;
  }

  public void flush() throws IOException {
    if (closed) {
      throw new IOException(
        "Cannot flush a closed output stream");
    }
    cache.flush();
  }

  public void write(int b) throws IOException {
    if (closed) {
      throw new IOException(
        "Cannot write to a closed output stream");
    }
    cache.write((byte)b);
  }

  public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
  }

  public void write(byte b[], int off, int len)
    throws IOException {
    if (closed) {
      throw new IOException(
       "Cannot write to a closed output stream");
    }
    cache.write(b, off, len);
  }

  public boolean closed() {
    return (this.closed);
  }

   public void reset() {
    //noop
  }
}

 

There is one important point to note about CacheResponseStream. All of the write methods are overridden to write to a custom OutputStream class, which must be passed as an argument to the class's constructor. By doing this, theCacheResponseStream object ensures no content is sent to a clientall content is sent to the provided OutputStreamobject.

With the CacheFilterCacheResponseWrapper, and CacheResponseStream classes, the cache Filter is ready to be used. Deploy the Filter with the jspbook Web Application by adding Listing 8-19 in web.xml. Make sure the second part of this entry appears after the mapping for the GZIP Filter.

Listing 8-19. web.xml Entry for the Cache Filter
...
  <filter>
    <filter-name>CacheFilter</filter-name>
    <filter-class>com.jspbook.CacheFilter</filter-class>
  </filter>
...
  <filter-mapping>
    <filter-name>GZIPFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <filter-mapping>
    <filter-name>CacheFilter</filter-name>
    <url-pattern>/timemonger.jsp</url-pattern>
    <init-param>
      <param-name>cacheTimeout</param-name>
      <param-value>1</param-value>
    </init-param>
  </filter-mapping>
...

 

Unlike the GZIP Filter, the cache Filter should not be applied to an entire Web Application. As was previously mentioned, some pages must not be cached because they produce a dynamic output for each client request. This is the reason the web.xml entry in Listing 8-18 only maps the cache Filter to timemonger.jsp. Currently, timemonger.jsp is a fictitious page, but we will soon create it as an example to illustrate the cache Filter in use. Later, discussion will be finished about the cache Filter, explaining how it knows to reset a cache and why it was applied after the GZIP Filter.

The code for timemonger.jsp can be anything that consumes time. All dynamic pages will consume time, but it is helpful to have a clear example. For timemonger.jsp, we will create a page with a few nested loops that do nothing but consume time as shown in Listing 8-20.

Listing 8-20. timemonger.jsp
<html>
  <head>
    <title>Cache Filter Test</title>
  </head>
  <body>
A test of the cache Filter.
<%
// mock time-consuming code
for (int i=0;i<100000;i++) {
   for (int j=0;j<1000;j++) {
     //noop
   }
}
%>
  </body>
</html>

 

You can informally test the page by browsing to http://127.0.0.1/jspbook/timemonger.jsp. The page takes a few seconds to load, but on subsequent requests you should notice it takes a lot less time. However, an informal test is of little help. The point of a cache Filter is that it provides notable performance increases. We can formally test the page using a simple JSP.

Save Listing 8-21 as TestCache.jsp in the root directory of the jspbook Web Application.

Listing 8-21. TestCache.jsp
<%@ page import="java.util.*, java.net.*, java.io.*" %>
<%
  String url = request.getParameter("url");
  long[] times = new long[2];
  if (url != null) {
    for (int i=0;i<2;i++) {
      long start = Calendar.getInstance().getTimeInMillis();
      URL u = new URL(url);
      HttpURLConnection huc =
        (HttpURLConnection)u.openConnection();
      huc.setRequestProperty("user-agent","Mozilla(MSIE)");
      huc.connect();
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      InputStream is = huc.getInputStream();
      while(is.read() != -1) {
        baos.write((byte)is.read());
      }
      long stop = Calendar.getInstance().getTimeInMillis();
      times[i] = stop-start;
    }
  }
  request.setAttribute("t1", new Long(times[0]));
  request.setAttribute("t2", new Long(times[1]));
  request.setAttribute("url", url);

 

%><html>
<head>
  <title>Cache Test</title>
</head>
<body>
<h1>Cache Test Page</h1>
Enter a URL to test.
<form method="POST">
<input name="url" size="50">
<input type="submit" value="Check URL">
</form>
<p><b>Testing: ${url}</b></p>
Request 1: ${t1} milliseconds<br/>
Request 2: ${t2} milliseconds<br/>
Time saved: ${t1-t2} milliseconds<br/>
</body>
</html>

 

The page is a JSP that tests URLs, similar to TestCompression.jsp. A connection is opened to a specific URL and the content is requested twice. It is assumed that the server is not currently caching the response to the URL; the first request should take the full time of generating the request. However, the second request is assumed to hit the cache. There is little we can do to ensure that TestCache.jsp works as expected for all URLs, but because we know how our cache Filter works and we can control our cache, the test will work for us.

Make sure all of the cache Filter classes are compiled, the web.xml modifications are saved, and reload the jspbook Web Application to reflect the changes. Browse to http://127.0.0.1/jspbook/TestCache.jsp to start the test. Fill in the form with the URL to timemonger.jsphttp://127.0.0.1/jspbook/timemonger.jsp. Submit the information and a report is presented of the response times. Figure 8-10 provides a browser rendering of the results.

Figure 8-10. Browser Rendering of the Cache Test

graphics/08fig10.gif

 

The speed difference comes from the cache Filter doing its job. If you look in the temporary work directory Tomcat keeps for the jspbook Web Application, you will notice a new file has appeared, timemonger.jspnull. This file is a copy of the HTML generated by timemonger.jsp. The cache Filter is saving time by using this file instead of reevaluatingtimemonger.jsp each time a request is made. Note, that TestCache.jsp will only work once every minute with the current Filter configuration due to the one-minute cache time-out.

The benefit of a cache Filter should be clear; caching optimizes the amount of time it takes to generate a static, or usually static, response. The cache Filter introduced in this chapter is a helpful Filter to know about and use in a Web Application. The time difference between using a cached copy of content versus a dynamic response by a JSP or Servlet can be quite dramatic. When possible, it is always worth caching dynamically generated responses.

Caching Servlet Responses

The cache Filter we just created caches information based on a URL and by checking if the file that generates that URL is outdatedthat is, it checks the time-stamp of the JSP or HTML page. However, the response of a Servlet is cached as easily as the response generated from a JSP or HTML page, but when a Servlet is updated it is difficult to determine based on a URL what .java file corresponds to the Servlet. It can be done by parsing web.xml, checking the URL mapping element, but this is a task left to you. For simplicity, CacheFilter.java uses a manual check. If a URL is mapped to a Servlet, you may ensure that the cache is updated each time the Servlet's code is updated by setting an initial configuration element.

Previously in the chapter, a point was made on how CacheFilter.java can be made to not cache particular URLs. An initial parameter of the same name as the URL can be set with the value "nocache". For example, the following configuration would skip caching index.jsp.

 

<filter>
  <filter-name>Cache</filter-name>
  <filter-class>com.jspbook.CacheFilter</filter-class>
  <init-param>
    <param-name>/jspbook/index.jsp</param-name>
    <param-value>nocache</param-value>
  </init-param>
</filter>

 

Nothing was noted about what happens if the value is other than "nocache". Any other value is treated as if it is the location of a file in the Web Application that should be time-checked. For example, if the URL /index.jsp is generated by a Servlet, /WEB-INF/classes/com/jspbook/Index.java, the following mapping would map cache invalidation to that file.

 

<filter>
  <filter-name>Cache</filter-name>
  <filter-class>com.jspbook.CacheFilter</filter-class>
  <init-param>
    <param-name>/index.jsp</param-name>
    <param-value>/WEB-INF/classes/com/jspbook/Index.java</param-value>
  </init-param>
</filter>

 

Using the above mapping, if a change was made to the Index.java Servlet (or whatever else the file may be), the cache for /index.jsp would be reset. By providing this functionality, it is possible to control caching of both implicitly mapped resources, such as HTML or JSP files, and explicitly mapped resources, such as Servlets and Filters.

Caching and Fault Tolerance

A superb use of caching that is worth mentioning is fault tolerance. A cached copy of a response is good for more than simply speeding up a request. Imagine a situation where a Web Application is working perfectly fine with a cache Filter speeding up responses. What happens should a change occur, perhaps a JSP typo or a database failure? Normally an exception is thrown and the user is presented with an error page, as detailed in Chapter 4. However, assuming a cache of the would-be generated response exists, there may be no need to show the error page. Instead show the cache and continue to do so until the resource is fixedthat is, the code typo is corrected or the database comes back online.

The cache Filter we just created tries to be fault tolerant. The code that invokes the doFilter() method is surrounded by a try-catch statement.

 

try {
...
    } catch (ServletException e) {
      if (!file.exists()) {
        throw new ServletException(e);
      }
    }
    catch (IOException e) {
      if (!file.exists()) {
        throw e;
      }
    }

 

Should an exception be thrown, the filter optionally re-throws the exception based on if a cache exists or not. If a cache exists, the cache is displayed. When no cache exists, the exception is passed to the Web Application and assumed to be handled by an error page.

The implicit fault-tolerance behavior of CacheFilter.java may or may not be what you need, but it is helpful in almost any situation where a cache Filter is helpful. Therefore, bundling the code with the cache Filter is a valid decision. In any case, it is helpful to see that the fault-tolerance mechanism works. Browse to the time-wasting page,http://127.0.0.1/jspbook/timemonger.jsp, to ensure a version of it is in the current cache. Next, deliberately introduce an error in the pagemake a typo in the code. Browse back to the page and note that instead of an error page, the cached content is displayed.

posted on 2010-11-24 20:52  aurawing  阅读(569)  评论(0编辑  收藏  举报