站内搜索: 请输入搜索关键词
当前页面: 图书首页 > Servlets and JavaServer Pages: The J2EE Technology Web Tier

Servlets and JavaServer Pages: The J2EE Technology Web Tier

[ directory ]Chapter8.        Filters Wrappers

Introducing Filters

Filters are components that sit between a request and the intended endpoint of that request. The endpoint could be a static or dynamic resource such as an HTML page, JSP, or a Servlet[1]. Filters are able to:

[1] Filters also apply to "fictitious" resources. A request that would normally generate a 404 error can pass through a Filter and the same Filter can optionally generate a response for it instead of displaying the error.

This makes Filters very powerfulin fact, so powerful that developers have to be very careful when using them. When using Filters, the developer is much closer to the HTTP protocol than with a Servlet and must understand the protocol in much more detail. For example, when using Servlets, a developer can pretty much ignore the HTTP content-length header, but when using Filters and producing data, the Filter writer may have to know the size of the content and set the value of that header.

What Is a Filter?

Physically a Filter is a component that intercepts a request sent to a resource in a Web Application. Filters exist as part of a chain, with the last link in the chain being the requested resource. A Filter can choose to pass the request on, in which case the request will be forwarded to either the next Filter in the Filter chain or, if this is the last Filter in the chain, to the requested resource. The Filter also sees the response before it is returned to the client. Figure 8-1 illustrates the concept.

Figure 8-1. Listing Path to Servlet Through Filters

graphics/08fig01.gif


Figure 8-1 shows the code path when Filters are invoked. A client makes a request to a resource, such as a Servlet, JSP, or static file, and the Web Application is configured such that any number of Filters are invoked for the request. Each Filter is invoked in turn, and each Filter passes on the request down the Filter "chain" until the endpoint is executed.

Filters are helpful because they allow a Web developer to cleanly add any number of layers of pre-processing and post-processing to a request and response. The functionality can be mimicked using Servlets and request dispatching, but it is a slightly convoluted approach and requires forwarding all requests through one or more Servlets before reaching the final endpoint. By using Filters it is easy to seamlessly define and apply a Filter to existing Web Application resources.

The Servlet 2.4 specification further extends the utility of Filters. It is now possible to install Filters that get invoked on a RequestDispatcher forward or include or that get invoked in the case of an error.

The Filter Life Cycle

The Filter life cycle is conceptually identical to the three-phase Servlet life cycle. A Filter goes through initialization, service, and destruction. The initialization occurs only once, when the Filter is first loaded for use by a Web Application. The service phase of the Filter life cycle is invoked each time the Filter is applied to a request and response. The destruction phase is invoked after a Web Application is completely finished using the Filter and all resources of the Filter need to be properly terminated. Figure 8-2 illustrates the Filter life cycle.

Figure 8-2. The Filter Life Cycle

graphics/08fig02.gif


The diagram shown in Figure 8-2 does not need much explanation. Compared to the Servlet life cycle, Figure 2-1, it includes the exact same init() and destroy() methods matching initialization and destruction phases, respectively. The only difference requiring explanation is the new doFilter() method that corresponds to the service phase of the life cycle. The doFilter() method is the method invoked by a container when applying a Filter to a ServletRequest and ServletResponse. Both the request and response are passed as parameters to this method and that is where the Filter is customized to do its specific task.

Coding a Simple Filter

A Filter must implement the javax.servlet.Filter interface. This interface defines the life cycle of a Filter and enforces a custom Filter that supports all the needed methods. The Filter interface defines three methods:

The preceding three methods should be fairly intuitive, but there are two new objects introduced: FilterConfig and FilterChain. The FilterConfig object is used for Filter configuration, and the FilterChain object represents the current chain of Filters being applied to a request and response. Both of these objects are further explained later in this section.

For all practical purposes a Filter provides the same functionality as a Servlet. The only difference is a Filter enforces a clean separation between each resource in the chain. This is an important concept to understand because it is why Filters are a helpful addition to Servlets, and in some cases a superior form of a Servlet. As an illustration of this point, take for example the Filter in Listing 8-1. It is a Filter generating a simple response to a clientthe equivalent of the HelloWorld Servlet seen in Chapter 2.

Listing 8-1. HelloWorldFilter.java
package com.jspbook;

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

public class HelloWorldFilter implements Filter{

  public void init(FilterConfig config)
  {
  }

  public void doFilter(ServletRequest req,
                    ServletResponse res,
                    FilterChain filter)
    throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    response.setContentType("text/html");
    PrintWriter out = response.getWriter();
    out.println("<html>");
    out.println("<head>");
    out.println("<title>Hello World!</title>");
    out.println("</head>");
    out.println("<body>");
    out.println("<h1>Hello World!</h1>");
    out.println("</body>");
    out.println("</html>");

    return;
  }

  public void destroy()
  {
  }
}

Try out the HelloWorld Filter. Save Listing 8-1 as HelloWorldFilter.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application. Compile the code, but before seeing any results the Filter needs to be deployed with the Web Application. Deployment of a Filter is surprising similar to a Servlet. The only difference is the name of the web.xml elements that are used. Instead of the servlet, servlet-name, and servlet-class elements, the filter, filter-name, and filter-class elements are used. The three new elements provide the same respective functionality, with the only difference being a Filter instead of a Servlet is being loaded and configured. In use it would resemble the entry in Listing 8-2. Add the following code to web.xml in the /WEB-INF directory of the jspbook Web Application.

Listing 8-2. HelloWorldFilter Entry in web.xml
<filter>
  <filter-name>HelloWorldFilter</filter-name>
  <filter-class>com.jspbook.HelloWorldFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>HelloWorldFilter</filter-name>
  <url-pattern>/HelloWorldFilter</url-pattern>
</filter-mapping>

Make sure the preceding entry in web.xml occurs before any servlet elements but after the context-param element. Save the changes and reload the jspbook Web Application. Visit http://127.0.0.1/jspbook/HelloWorldFilter to see the newly added Filter in use. Figure 8-3 shows a browser rendering of the results.

Figure 8-3. Browser Rendering of HelloWorldFilter

graphics/08fig03.gif


It should be clear that a Filter can do exactly what a Servlet can. A "Hello World" example is simple and direct, but do not miss the point: Filters are Servlets with some extra functionality. Any example from Chapter 2 could have been implemented as a Filter. This chapter won't dwell on the point that you can re-create all of the examples in Chapter 2 as Filters.The focus is Filters do a better job than Servlets.

Filter Chains

Filter chains are what make Filters helpful. Listing 8-2 illustrated how a Filter can be used as a Servlet, but this is not what Filters were designed for. Recall the most important method in the Filter interface is the doFilter() method, and it is passed three parameters that are instances of a ServletRequest, ServletResponse, and a FilterChain object. The ServletRequest and ServletResponse objects were covered thoroughly in Chapter 2. They are what allows a Filter to mimic a Servlet, but FilterChain is new. The FilterChain object represents the possible stack of Filters being executed on a particular request and response. This is where a Filter provides a convenient enhancement over a Servlet.

As illustrated in Figure 8-1 Filters provide a mechanism for cleanly applying layers of functionality to a ServletRequest and ServletResponse. This is in contrast to the single endpoint a Servlet is designed to be. By using Filters it is easy to divide up functionality into many logical layers and stack them as desired. When using a Servlet this functionality must be done via some clever request dispatching. Figure 8-4 illustrates the concept.

Figure 8-4. Filters Versus Servlets

graphics/08fig04.gif


Shown in Figure 8-4 is a mock example of some real-world requirements of a Web Application. Security, efficiency, and content generation are all valid conceptual requirements. In practice how these requirements are implemented varies, but as a Web Application grows, it is difficult to easily manage these requirements unless a clean enforcement of functional separation is used. The separation can be done by both Servlets and Filters, but Filters provide a very direct separation and enforcement layer.

The layered separation Filters provide is due to the FilterChain object. A FilterChain object represents the chain of Filters a Web Application is configured to use on a particular real or fictitious endpoint. A Filter is passed this object during runtime invocation of the doFilter() method because it needs to decide if the Filter should fully handle a request and response or if it should only manipulate the pair and pass them on to the next Filter in the chain. In cases where multiple Filters are applied to the same endpoint, the ordering of Filter execution matches the ascending order of filter-mapping elements defined in web.xml.

As shown with the HelloWorldFilter, Listing 8-1, a Filter can stop execution of subsequent Filters in the chain by simply returning from the doFilter() method. This causes the Web Application to return back through any Filters previously executed and to finish sending the response to a client. Should a Filter need to continue executing further links down the chain, then the FilterChain object's doFilter() method needs to be invoked. The doFilter() method of the FilterChain object corresponds to the next Filter or the possible endpoint of the chain. By invoking the doFilter() method a Filter stops and waits for the next resource in the chain to manipulate the request and response. Upon returning from the FilterChain.doFilter() method, the current Filter finishes its own execution.

The concept of a Filter chain is important. It is what distinguishes a Filter from a Servlet. As an example let's start with a Filter that is very commonly used: a link tracking Filter. The Filter will do a very practical task: track the site's traffic. The concept of link tracking is not new; you have already seen a Servlet do the task in Chapter 2. However, the Servlet did not track incoming links and referrals. Filters are well suited for this task since they can seamlessly be applied to all requests. Save Listing 8-3 as LinkTrackerFilter.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application.

Listing 8-3. LinkTrackerFilter.java
package com.jspbook;

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

public class LinkTrackerFilter implements Filter {
  Calendar startDate = Calendar.getInstance();
  static int count = 0;
  public static Hashtable requests = new Hashtable();
  public static Hashtable responses = new Hashtable();
  public static Hashtable referers = new Hashtable();
  FilterConfig fc = null;

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

    String uri = request.getRequestURI();
    String path = request.getContextPath();
    String turi = uri.substring(path.length(), uri.length());

    if (turi.startsWith("/redirect")) {
      String url = request.getParameter("url");
      // error check!
      if (url == null || url.equals("")) {
        response.sendRedirect(path);
        return;
      }
      Link l = new Link();
      l.url = url;
      l.count = 1;
      l.lastVisited = Calendar.getInstance().getTimeInMillis();
      if (responses.get(l.url)!=null){
        l = (Link)responses.get(l.url);
        l.count++;
      }
      else {
        responses.put(l.url, l);
      }

      response.sendRedirect(url);
      return;
    }

    if (uri.endsWith(".js") || uri.endsWith(".css") ||
        uri.endsWith(".gif") || uri.endsWith(".png") ||
        uri.endsWith(".jpg")|| uri.endsWith(".jpeg")) {
      chain.doFilter(req, res);
      return;
    }

    {
      // Log request
      Link l = new Link();
      l.url = uri;
      l.count = 1;

      l.lastVisited = Calendar.getInstance().getTimeInMillis();
      if (requests.get(l.url)!=null){
        l = (Link)requests.get(l.url);
        l.count++;
      }
      else {
        requests.put(l.url, l);
      }
    }

    // log referer
    String referer = request.getHeader("referer");
    if (referer != null && !referer.equals("")) {
      Link l = new Link();
      l.url = referer;
      l.count = 1;
      l.lastVisited = Calendar.getInstance().getTimeInMillis();
      if (referers.get(l.url)!=null){
        l = (Link)referers.get(l.url);
        l.count++;
      }
      else {
        referers.put(l.url, l);
      }
    }

    //log the hit
    count++;
    chain.doFilter(req, res);
  }

  public void init(FilterConfig filterConfig) {
    this.fc = filterConfig;
  }

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

  // total visitors
  public int getCount() {
    return count;
  }

  public static Link[] getRequests() {
    Collection c = requests.values();
    Vector v = new Vector(c);
    Collections.sort(v, new LinkComparator());
    return (Link[])v.toArray(new Link[0]);
  }

  public static Link[] getResponses() {
    Collection c = responses.values();
    Vector v = new Vector(c);
    Collections.sort(v, new LinkComparator());
    return (Link[])v.toArray(new Link[0]);
  }

  public static Link[] getReferers() {
    Collection c = referers.values();
    Vector v = new Vector(c);
    Collections.sort(v, new LinkComparator());
    return (Link[])v.toArray(new Link[0]);
  }

  public long getDays() {
    Calendar now = Calendar.getInstance();
    long a = startDate.getTimeInMillis();
    long b = now.getTimeInMillis();
    long between = b-a;

    long days = (between/(1000*60*60*24));
    if (days < 1)
      days = 1;
    return days;
  }
}

The Filter works by keeping three Hashtable objects to track incoming requests, referrals of incoming requests, and outgoing responses.



public static Hashtable requests = new Hashtable();
public static Hashtable responses = new Hashtable();
public static Hashtable referers = new Hashtable();

Each time the Filter processes a request (which should be every time a request is made), the Hashtables are updated accordingly. If a request is sent to the "/redirect" URL, it will be logged in the referral table and the response is redirected.



if (uri.startsWith("/redirect")) {
  String url = request.getParameter("url");
  // error check!
  if (url == null || url.equals("")) {
    response.sendRedirect("/");
    return;
  }
  Link l = new Link();
  l.url = url;
  l.count = 1;
  l.lastVisited = Calendar.getInstance().getTimeInMillis();
  if (responses.get(l.url)!=null){
    l = (Link)responses.get(l.url);
    l.count++;
  }
  else {
    responses.put(l.url, l);
  }

  response.sendRedirect(url);
  return;
}

Logging is done via encapsulating the information in a JavaBean that we will create later. The point to see is that the LinkTracker Filter treats the "/redirect" URL specially. If using the LinkTracker Filter with your Web Application, don't have another resource use the "/redirect" URL or else it will never receive a request. All other requests are treated as requests for resources found within the Web Application. The LinkTracker Filter logs the URL the request was trying to reach and the referrer of the request.



{
  // Log request
  Link l = new Link();
  l.url = uri;
  l.count = 1;
  l.lastVisited = Calendar.getInstance().getTimeInMillis();
  if (requests.get(l.url)!=null){
    l = (Link)requests.get(l.url);
    l.count++;
  }
  else {
    requests.put(l.url, l);
  }
}

// log referer
String referer = request.getHeader("referer");
if (referer != null && !referer.equals("")) {
  Link l = new Link();
  l.url = referer;
  l.count = 1;
  l.lastVisited = Calendar.getInstance().getTimeInMillis();
  if (referers.get(l.url)!=null){
    l = (Link)referers.get(l.url);
    l.count++;
  }
  else {
    referers.put(l.url, l);
  }
}

The URL is easily obtained using the HttpServletRequest object's getRequestURI() method.



String uri = request.getRequestURI();

Referral information is obtained by mining the HTTP referer header.



String referer = request.getHeader("referer");
    if (referer != null && !referer.equals("")) {

Note that the referer header is not required to be set, so don't rely on it containing a value. The LinkTracker Filter explicitly checks to make sure the referer header is not null or an empty string before logging referral information.

Finally, the Filter invokes the all-important doFilter() method to allow other Filters, Servlets, and JSP to have a chance at handling the request.



chain.doFilter(req, res);

The doFilter() method is not difficult to use, but it is important to understand why the LinkTracker Filter invoked it and the HelloWorld Filter did not. The HelloWorld Filter was generating the entire responseit had no need to let other Filters, JSP, or Servlets handle the request. In contrast, the LinkTracker Filter is meant to be transparent to the rest of the Web Application; other Filters, Servlets, and JSP shouldn't be affected at all by the link tracking logic. In general, all Filters that are supposed to be transparentthat is, be applied to a whole Web Application and not "break" things, should properly invoke the doFilter() method.

Deploy the filter to intercept all requests. Add Listing 8-4 into web.xml.

Listing 8-4. LinkTracker Filter Deployment
<filter>
  <filter-name>LinkTrackerFilter</filter-name>
  <filter-class>com.jspbook.LinkTrackerFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>LinkTrackerFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Before we can test out the LinkTracker Filter, a few more additions are required. The Filter stores information via a JavaBean object. Without the JavaBean, LinkTrackerFilter.java won't compile. Listing 8-5 is the required JavaBean.

Listing 8-5. Link.java
package com.jspbook;

public class Link {
  protected int count;
  protected String url;
  protected long lastVisited;

  public int getCount() {
    return count;
  }
  public String getUrl() {
    return url;
  }
  public long getLastVisited() {
    return lastVisited;
  }
}

Save the preceding code as Link.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application. There is nothing tricky about the preceding code. The JavaBean provides a read-only method (for code outside the com.jspbook class) to get information about tracked links.

Another requirement for the LinkTracker class is a Comparator class. The Filter provides methods for getting a current list of either the request, referral, or response information. Each method converts the related Hashtable into a sorted array. For efficiency the array is sorted using Java's implementation of merge sort via the Collections object. In order to do the sorting, an implementation of the Comparator interface is required. Listing 8-6 illustrates the implementation.

Listing 8-6. LinkComparator.java
package com.jspbook;

import java.util.*;

public class LinkComparator implements java.util.Comparator {
  public int compare(Object o1, Object o2) {
    Link l1 = (Link)o1;
    Link l2 = (Link)o2;
    return l2.getCount() - l1.getCount();
  }

  public boolean equals(Object o1, Object o2) {
    Link l1 = (Link)o1;
    Link l2 = (Link)o2;
    if (l2.getCount() == l1.getCount()) {
      return true;
    }
    return false;
  }
}

Save the preceding code as LinkComparator.java in the /WEB-INF/classes/com/jspbook directory of the jspbook Web Application. The code provides two methods, compare() and equals(), as mandated by the Comparator interface, and provides a method to compare two instances of the Link object. Elaboration on the Comparator interface can be found in the Java documentation for java.util.Comparator.

LinkTracker.java, Link.java, and LinkComparator.java are the only classes required for the link tracking Filter, but the Filter is pointless without something to display its statistics. We need to build a Servlet or JSP that displays the link information tracked by the Filter. For simplicity we will use JSP and save Listing 8-7 as linktracker.jsp in the root directory of the jspbook Web Application.

Listing 8-7. linktracker.jsp
<%@ page import="com.jspbook.*"%>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<% Link[] links = LinkTrackerFilter.getRequests();
   Link[] responses = LinkTrackerFilter.getResponses(); 
   Link[] referers = LinkTrackerFilter.getReferers();
%>
<p class="h1">Requests</p>
<c:forEach var="r" begin="0" items="${requests}">
  <p class="nopad"><a href="${r.url}">${r.url}</a> ${r.count} ${r.lastVisited}</p>
</c:forEach>
<p class="h1">Responses</p>
<c:forEach var="r" begin="0" items="${responses}">
  <p class="nopad"><a href="${r.url}">${r.url}</a> ${r.count} ${r.lastVisited}</p>
</c:forEach>
<p class="h1">Referers</p>
<c:forEach var="r" begin="0" items="${referers}">
  <p class="nopad"><a href="${r.url}">${r.url}</a> ${r.count} ${r.lastVisited}</p>
</c:forEach>

The JSP uses three loops to display ordered information about requests, responses, and referrals. Information is obtained from the getRequests(), getResponses(), and getReferers() method, respectively, of the link tracking Filter. The end result is a single HTML page that displays the information tracked by the LinkTracker Filter.

Try out the link tracking Filter by compiling LinkTracker.java, Link.java, and LinkComparator.java, reloading the Web Application, and browsing to http://127.0.0.1/jspbook/linktracker.jsp. You will see a page displaying the link tracking statistics. Figure 8-5 provides a browser rendering of the results.

Figure 8-5. Browser Rendering of linktracker.jsp

graphics/08fig05.gif


Initially information is sparse; until you make HTTP requests for various resources in the Web Application, there is nothing to track. Try browsing around to various previous code examples. Upon returning to linktracker.jsp you will see their information added. Responses are only tracked if links are specially encoded for the link tracking Filter. Links to external resources need to be directed to/redirected (of the jspbook Web Application) with the URL parameter set with the outbound URL. For example, the outgoing link "http://google.com" needs to be changed to "/redirect?url=http://google.com". The technique is the same as the one used by the LinkTracker Servlet coded in Listing 2-6 in Chapter 2. If needed, review the Chapter 2 example for a full explanation and how it was used.

So what has been gained by this example? The example illustrates the intended, popular use of Filters: as a component that adds functionality to possibly the entire Web Application but need not affect underlying resources. The doFilter() method is responsible for this, and it is important to realize how powerful the functionality is. Imagine other layered services such as security, caching, or content compression, and you should easily see why Filters are a helpful addition to the Servlet specifications.

Filter Configuration

Filters can be configured in the same method as a Servlet. The Filter web.xml element may contain any number of init-param elements to define custom initialization parameters. Discussion of the init-param and child param-name and param-value elements is skipped because it was already explained in Chapter 2 under Servlet configuration.

Initial parameters defined for a Filter can be accessed via the FilterConfig object passed as a parameter to the init() method. The FilterConfig class has a striking resemblance to the ServletConfig class used with Servlets. The following are the methods provided by FilterConfig:

Filters and Request Dispatching

By default a chain of Filters is only constructed to handle a request made by a client. If a request is dispatched using either the forward() or include() methods of the RequestDispatcher object, Filters are not applied. However, this functionality can be configured via web.xml. A filter-mapping element may include any number of request dispatcher elements to configure how the Filter is applied to requests. The dispatcher element has four possible values: REQUEST, INCLUDE, FORWARD, and ERROR. The different values represent if the Filter should be applied to only client requests, only includes, only forwards, only errors, or any combination of the four.

For example, applying Listing 8-5 to only RequestDispatcher include() calls could be accomplished by:



<filter>
  <filter-name>LinkTracker</filter-name>
  <filter-class>com.jspbook.LinkTracker</filter-class>
</filter>
<filter-mapping>
  <filter-name>LinkTracker</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>INCLUDE</dispatcher>
</filter-mapping>

Note the use of the dispatcher element. If the preceding mapping is used, the Filter is no longer applied to incoming client requests, only RequestDispatcherinclude() calls. If the Filter needs to be applied to two or more types of requests, then more dispatcher elements can be used:



<filter-mapping>
  <filter-name>LinkTracker</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>INCLUDE</dispatcher>
  <dispatcher>REQUEST</dispatcher>
</filter-mapping>

Use of the dispatcher element is straightforward and helpful if you need to adjust what the Filter is applied to. By default Filters are configured to be ideal for uses such as caching and compression, where an entire page is optionally evaluated or not. You would not want a JSP to evaluate half a page, produce some text, then have an include produce the second half of the page as compressed text because of a Filter. You would want the Filter on the included page to be skipped. By default this is exactly what would happen. However, the opposite might also be desired. A security Filter might need to be applied to all requests, even attempts to include content. In this case the Filter could be configured to do exactly that. The greater point is that Filters can be used with request dispatching; use the functionality if it is needed.

[ directory ]Chapter8.        Filters Wrappers