原文出处:http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html?page=1
Almost every single web application you will ever make will seriously benefit from using servlet filters to both cache and compress content. A caching filter optimizes the time it takes to send back a response from your web server, and a compression filter optimizes the size of the content that you send from your web server to a user via the Internet. Since generating content and sending content over the World Wide Web are the bread and butter of web applications, it should be no surprise that simple components that aid in these processes are incredibly useful. This article details the process of building and using a caching filter and a compression filter that are suitable for use with just about any web application. After reading this article, you will understand caching and compressing, have code to do both, and be able to apply caching and compression to any of your future (or existing!) web applications.
Servlet filters are powerful tools that are available to web application developers using the Servlet 2.3 specification or above. Filters are designed to be able to manipulate a request or response (or both) that is sent to a web application, yet provide this functionality in a method that won't affect servlets and JSPs being used by the web application unless that is the desired effect. A good way to think of servlet filters is as a chain of steps that a request and response must go through before reaching a servlet, JSP, or static resource such as an HTML page in a web application. Figure 1 shows the commonly used illustration of this concept.
Figure 1. The servlet filter concept
The large gray box is a web application that has some endpoints, such as JSP, and some filters applied to intercept all requests and responses. The filters are shown in a stack, three high, that each request and response must pass through before reaching an endpoint. At each filter, custom Java code would have a chance to manipulate the request or response, or anything that has to do with either of those objects.
Related Reading
Java Servlet & JSP Cookbook Table of Contents |
Understand that a user's request for a web application resource can be forced to go through any number of filters, in a given order, and any of the filters may manipulate the request, including stopping it altogether, and respond in a variety of different ways. This is important to understand because later in this article, two filters will be presented that manipulate the HttpServletRequest
and HttpServletResponse
objects to provide some very convenient functionality. Don't worry if you don't know anything about coding a filter -- it would certainly help if you understood the code, but in the end of the article, all of the code is provided in a JAR that can easily be used without knowing a thing about how it was made.
Before moving on, if you would like to learn more about the basics of servlet filters, I suggest checking out Servlets and JavaServer Pages; the J2EE Web Tier. It is a Servlet 2.4 and JSP 2.0 book I co-authored with Kevin Jones, and the book provides complete coverage of servlets and servlet filters, including the two filters presented later in this article. It would be nice if you bought the book, but the chapters on servlets and filters will soon be available for free on the book support site -- if they're online already, read away.
Compression is the act of removing redundant information, representing what you want in as little possible space. It is incredibly helpful for sending information across the World Wide Web, because the speed at which people get information from a web application is almost always dependent on how much information you are trying to send. The smaller the size of your information, the faster it can all be sent. Therefore, if you compress the content your web application generates, it will get to a user faster and appear to be displayed on the user's screen faster. In practice, simply applying compression to a decent-sized web page often results in saving several seconds of time.
Now, the theory is nice, but the practice is nicer. This theoretical compression isn't something you have to labor over each time you go to code a servlet, JSP, or any other part of a web application. You can obtain very effective compression by having a servlet filter conditionally pipe whatever your web application produces to a GZIP-compressed file. Why GZIP? Because the HTTP protocol, the protocol used to transmit web pages, allows for GZIP compression. Why conditionally? Because not every browser supports GZIP compression, but almost every single modern web browser does. If you blindly send GZIP-compressed content to an old browser, the user might get nothing but gibberish. Since checking for GZIP compression support is nearly trivial, it is no problem to have a filter send GZIP-compressed content to only those users that can handle it.
Source Code Download jspbook.zip for all of the source code found in this article. Also download jspbook.jar for the ready-to-use JAR with compiled versions of both the cache and compression filter. |
I'm saying this GZIP compression stuff is good. But how good? GZIP compression will usually get you around a 6:1 compression ratio; it depends on how much content you are sending and what the content is. In practice, this means you will send content to a user up to six times faster if you simply use GZIP compression whenever you can. The only trick is that you need to be able to convert normal content in to GZIP-compressed content. Thankfully, the standard Java API provides code for doing exactly this: the java.util.zip
package. The task is as easy as sending output sent in a web application's response conditionally through the java.util.zip.GZIPOutputStream
class. Here is some code for doing exactly that.
As with most every filter, three classes are needed to do the job. A customized implementation of the javax.servlet.Filter
interface, a customized implementation of the javax.servlet.ServletOutputStream
class, and a customized implementation of the javax.servlet.http.HttpServletResponse
class. Full source code for these three classes is provided at the end of the article; for now I will focus only on the relevant code. First, a check needs to be made if a user has support for GZIP-compressed content. This check is best done in the implementation of the Filter
class.
...
public class GZIPFilter implements Filter {
// custom implementation of the doFilter method
public void doFilter(ServletRequest req,
ServletResponse res,
FilterChain chain)
throws IOException, ServletException {
// make sure we are dealing with HTTP
if (req instanceof HttpServletRequest) {
HttpServletRequest request =
(HttpServletRequest) req;
HttpServletResponse response =
(HttpServletResponse) res;
// check for the HTTP header that
// signifies GZIP support
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);
}
}
Information about GZIP support is conveyed using the accept-encoding
HTTP header. This header can be accessed using the HttpServletRequest
object's getHeader()
method. The conditional part of the code need be nothing more than an if
statement that either sends the response as is or sends the response off to be GZIP compressed.
The next important part of the GZIP filter code is compressing normal content with GZIP compression. This code occurs after the above filter has found that the user does know how to handle GZIP-compressed content, and the code is best placed in a customized version of the ServletOutputStream
class. Normally, the ServletOutputStream
class handles sending text or non-text content to a user while ensuing appropriate character encoding is used. However, we want to have the ServletOutputStream
class send content though a GZIPOutputStream
before sending it to a client. This can be accomplished by overriding the write()
methods of the ServletOutputStream
class to GZIP content before sending it off in an HTTP response.
...
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 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);
}
...
There are also a few loose ends to tie up, such as setting the content's encoding to be the MIME type for GZIP, and ensuring the subclass of ServletOutputStream
has implementations of the flush()
and close()
methods that work with the changes to the write()
methods. However, all of these changes are minor, and you may see code that does them by looking at the source code provided at the end of this article. The most important point to understand is that the filter has altered the response to ensure that all content is GZIP compressed.
Test out the above code by grabbing a copy of jspbook.jar, which includes compiled classes of the GZIP filter, and putting the JAR in the WEB-INF/lib directory of your favorite web application. Next, deploy the com.jspbook.GZIPFilter
class to intercept all requests to resources ending with ".jsp" or anything that produces HTML. Reload the web application for the changes to take effect. The GZIP filter should now be automatically compressing all responses, if the user's browser supports GZIP compression.
Unfortunately, there is really no good way you can tell if content displayed by your browser was GZIP compressed or not. Part of the process working is that it is unnoticeable if the content was compressed or not. In order to test out the compression, we need to spoof some HTTP requests and see what exactly is returned.
This can be done relatively easily by tossing some code in a JSP; here is such a JSP.
<%@ page import="java.util.*,
java.net.*,
java.io.*" %>
<%
String url = request.getParameter("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();
request.setAttribute("t1",
new Integer(b1.length));
request.setAttribute("t2",
new Integer(b2.length));
}
request.setAttribute("url", url);
%>
Cache Test
Cache Test Page
Enter a URL to test.
Testing: ${url}
Request 1: ${t1} bytes
Request 2: ${t2} bytes
Space saved: ${t1-t2} bytes
or ${(1-t2/t1)*100}%