<jsptutorial />

Servlet-Filter


Was sind Filter

Servlet-Filter bieten eine Möglichkeit, auf die Werte des Requests und der Response um eine Anfrage an eine Web-Ressource herum zuzugreifen. Dabei können mehrere Filter je nach Konfiguration eine Filterkette bilden, die der Request durchläuft. Dabei wird mittels Mapping-Regeln bestimmt, welche Filter für welche Requests zuständig sind und ggf. eine Kette bilden.
servletFilter.png
Es gibt zahlreiche Möglichkeiten, bei denen der Einsatz eines Filters sinnvoll sein kann. Der einfachste Anwendungsfall ist, einfach mitzuloggen, welche Ressource angesprochen wurde und wie lange die Bereitstellung der Ressource gedauert hat. Eine weitere Möglichkeit stellen wir im Kapitel Internationalisierung vor. Dort stellen wir anhand eines Servlet-Filters das korrekte Encoding für einen Request ein. Weitere Anwendungsfälle umfassen bspw. eine Entschlüsselung des Requests und die Verschlüsselung der Response, die Komprimierung der Response oder die Transformation von XML mittels XSLT.
Mitunter ist es sinnvoll, das eigentliche Request- oder Response-Objekt des Containers zu wrappen, weil man bspw. dessen Funktionalität erweitern möchte. Auch hierfür sind Servlet-Filter ideal geeignet. Die Servlet-API bietet bereits Wrapper-Klassen für ServletRequest- und ServletResponse-Objekte an, diese kann man mit einer eigenen Klasse geeignet erweitern und dann im Filter nutzen.

Zum SeitenanfangZum Seitenanfang

Wie funktioniert es

Zur Nutzung der Servlet-Filter stellt das Package javax.servlet drei Interfaces zur Verfügung: FilterConfig, FilterChain und Filter. Für die ersten beiden werden vom Containerhersteller Implementierungen geliefert und der Entwicklerin an geeigneter Stelle zur Verfügung gestellt. Das Interface Filter hingegen wird von der Entwicklerin des Filters implementiert.

java.servlet.FilterConfig

Jeder Filter wird im Deployment-Deskriptor konfiguriert. Die Details werden weiter unten im Abschnitt "Deployment-Deskriptor-Eintrag" beschrieben. Hier wird nur darauf eingegangen, dass man Filtern Initialisierungsparameter mitgeben kann. Bspw. wird in dem EncodingFilter im Kapitel Internationalisierung ein Parameter namens "encoding" verwendet, der das in der Anwendung verwendete Encoding für den Filter setzen kann. Die im Deployment-Deskriptor definierten Einträge werden von dem Container in einem Objekt vom Typ FilterConfig gekapselt und in der Init-Methode des Filters diesem zur Verfügung gestellt. Mit getInitParameter(String) bekommt man den entsprechenden Wert vom FilterConfig-Objekt geliefert. Neben dieser Methode kann man auch eine Enumeration über alle Initialisierungsparameter-Namen mit getInitParameterNames() bekommen. Zudem hat man mit getServletContext() Zugriff auf den ServletContext der Anwendung, zu der der Filter gehört.

javax.servlet.FilterChain

In der Einleitung des Kapitels haben wir anhand der Grafik gesehen, dass mehrere Filter hintereinander geschaltet werden können. Diese Filter bilden eine Filterkette. Filterketten werden durch Objekte vom Typ FilterChain abgebildet. Die einzige Methode dieses Interfaces ist die Methode doFilter(ServletRequest, ServletResponse). Diese Methode wird innerhalb der gleich benannten Methode doFilter(ServletRequest, ServletResponse, FilterChain) des Filter-Interfaces aufgerufen, um das nächste Element der Filterkette anzusprechen. Dies ist entweder der nächste folgende Filter oder aber die vom Client angeforderte Ressource. Die Reihenfolge der Filter wird durch die Einträge im Deployment-Deskriptor festgelegt. Dazu mehr im entsprechenden Abschnitt.

javax.servlet.Filter

Jeder Filter muss das Interface javax.servlet.Filter implementieren. Dieses bietet drei Methoden, die nicht zufällig den Lebenszyklus-Methoden des Servlet-Interfaces ähneln: init(FilterConfig), destroy und doFilter(ServletRequest, ServletResponse, FilterChain).
Auch ein Filter wird zunächst initialisiert und erhält ein FilterConfig-Objekt. Im Abschnitt zum FilterConfig-Objekt ist alles Notwendige zur Initialisierung eines Filters beschrieben.
Danach kann ein Filter seine eigentlich Aufgabe vornehmen. Wie aus der Grafik ersichtlich, werden aber nicht immer alle Filter einer Anwendung beim Zugriff auf eine Ressource, wie bspw. einem Servlet durchlaufen, sondern nur diejenigen, die für die entsprechende Anfrage konfiguriert sind. Diese Konfiguration wird im Deployment-Deskriptor vorgenommen. Hierzu mehr im entsprechenden Abschnitt weiter unten.
Trifft nun eine auf den Filter passende Anfrage ein, so wird dessen doFilter(ServletRequest, ServletResponse, FilterChain)-Methode aufgerufen.
In dieser kann der einkommende Request analysiert und ggf. verändert werden. Hier ist auch der geeignete Platz, um den Request und die Response in Wrapper-Objekten zu verpacken. Die Packages javax.servlet und javax.servlet.http bieten jeweilige Wrapper-Klassen an, von denen man ableiten kann.
Je nachdem, ob der Request den Erwartungen entsprochen hat oder nicht, kann man nun das nächste Element der Filterkette aufrufen, wie wir im vorigen Abschnitt beschrieben haben. Man kann aber an dieser Stelle auch die Kette abreißen lassen. Wird die Methode doFilter des übergebenen FilterChain-Objekts nicht aufgerufen, verwirft der Filter den Request. Es wird schlicht die Response so zurückgeliefert, wie sie am Ende der Methode vorliegt.1 Im Standardfall bekäme der Client somit eine leere Response, mit dem Status-Code 200, als wenn die Anfrage erfolgreich beendet worden wäre. Daher sollte man unbedingt entweder sendError(int, String) oder setStausCode(int) des HttpResponse-Objekts (Cast notwendig) aufrufen, um dem Client einen Hinweis darauf zu geben, warum der Request nicht weiter bearbeitet wurde.
Am Ende des Lebenszyklus eines Filters, also i.a.R. am Ende einer Anwendung wird die Methode destroy() aufgerufen. Ggf. kann man hier vom Filter gehaltene Ressourcen wieder freigeben. Dies dürfte im Allgemeinen für Filter aber eher ungewöhnlich sein und die Methode daher leer bleiben.
Der folgende Code zeigt einen Filter, der für jede Client-Anfrage einfach die Dauer der Bearbeitung loggt. Ein weiteres Beispiel für einen Filter findet sich im Kapitel zur Internationalisierung:
package org.jsptutorial.util.filters;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.logging.*;

public class DurationLogFilter implements Filter {

   private static Log logger;

   public void init(FilterConfig config) throws ServletException {
      // check whether a category has been
      // configured in the deployment descriptor
      String category = config.getInitParameter("log_category");
      if (category != null) {
         logger = LogFactory.getLog(category);
      }
      else {
         logger = LogFactory.getLog(DurationLogFilter.class);
      }
   }

   public void doFilter(ServletRequest request, ServletResponse response,
         FilterChain chain) throws IOException, ServletException {

      String url = null;
      if (request instanceof HttpServletRequest) {
         url = ((HttpServletRequest)request).getRequestURL().toString();
      }
      long duration, starttime = System.currentTimeMillis();

      // proceed along the chain
      chain.doFilter(request, response);

      // after response returns, calculate duration and log it
      duration = System.currentTimeMillis() - starttime;
      if (logger.isDebugEnabled()) {
         logger.debug("duration: " + duration + " - " + url);
      }
   }

   public void destroy() {
   }
}

Deployment-Deskriptor-Eintrag

Auch die Registrierung eines Filters im Deployment-Deskriptor2 ist denkbar einfach. Dazu ist zunächst einmal ein Servlet-Filter zu definieren, und dann muss ein Mapping angegeben werden, bei dem der Filter greifen soll. Beim Mapping gelten die gleichen Regeln wie bei Servlets. Um unseren Filter so zu konfigurieren, dass er alle Anfragen umfasst, reicht es einfach, folgendes Code-Fragment innerhalb des Root-Tags web-app zu platzieren3:
<!-- first define a named filter -->
<filter>
   <filter-name>DurationLogFilter</filter-name>
   <filter-class>org.jsptutorial.util.filters.DurationLogFilter</filter-class>
   <init-param>
      <param-name>log_category</param-name>
      <param-value>DurationLog</param-value>
   </init-param>
</filter>

<!-- now map this filter to a URL-pattern -->
<filter-mapping>
   <filter-name>DurationLogFilter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

Eine Besonderheit gibt es beim Mapping. Wie bei Servlets gibt es auch hier ein URL-Mapping. Ein Mapping von "/*" wie im Code-Beispiel würde bspw. immer greifen. Hingegen würde "*.jsp" auf alle JSPs zutreffen. Es gibt aber nicht nur das URL-Mapping, sondern auch ein Servlet-Mapping. Das heißt, man könnte auch einen Filter immer einem bestimmten Servlet, wie bspw. dem FrontController bei Struts oder JSF, zuordnen. Dazu würde man anstelle des obigen <url-pattern> den Filter durch folgenden Eintrag einem Servlet namens "FrontController" zuordnen:

<servlet-name>FrontController</servlet-name>

Wichtig ist, dass man hier den im Deployment-Deskriptor definierten Servlet-Namen und nicht die Servlet-Klasse verwendet. Ein Servlet kann schließlich im Deployment-Deskriptor mehrfach konfiguriert sein.
Mappings können auf diese Weise sehr gezielt nur bei einzelnen Ressourcen greifen oder auf Anfragen nach verschiedenen, dem URL-Mapping entsprechenden Ressourcen zutreffen. In der Grafik oben haben wir bspw. einen Filter gezeigt, der sowohl auf Anfragen gegen das Servlet1 greift, als auch bei solchen gegen das Servlet2. Zudem jeweils Filter, die nur bei Anfragen gegen das jeweilig im Mapping definierte Servlet greifen. Welche Filter eine Kette bilden, hängt ausschließlich davon ab, welche Mappings bei der jeweiligen Anfrage zutreffen.
Bleibt die Frage, in welcher Reihenfolge die Filter angewendet werden. Dabei gilt zunächst einmal, dass alle mit einem URL-Mapping definierten Filter Vorrang vor solchen haben, die mit einem Servlet-Mapping konfiguriert wurden. Innerhalb der jeweiligen Gruppe entscheidet dann alleine die Reihenfolge der Einträge im Deployment-Deskriptor.
Die Reihenfolge ist insofern wichtig, als bspw. ein Filter, der Wrapper-Objekte um das Request- und/oder Response-Objekt legt, i.a.R. als erstes konfiguriert werden muss, ansonsten könnten schon Werte verloren gehen, auf die man u.U. zugreifen möchte. Die Reihenfolge legt dabei nicht der Entwickler des Filters, sondern der Assembler (übersetzt: Monteur) der Anwendung fest.4

Zum SeitenanfangZum Seitenanfang

Ein ähnliches Konzept: Tomcats Valves

Tomcat, eine der am weitesten verbreiteten Servlet-Engines, bietet mit seinen Valves ebenfalls eine Möglichkeit, sich in den Requestfluß einzuhängen und Vor- oder Nacharbeiten am Request und an der Response vorzunehmen.
Die Programmierung ist ähnlich zu Servlet-Filtern. Im Folgenden findet sich bspw. der Code für ein Valve, das wie unser Filter Anfragen und deren Dauer protokolliert:

package org.jsptutorial.util.filters;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.*;
import org.apache.commons.logging.*;

public class DurationLogValve implements Valve {

   private static Log logger = LogFactory.getLog(DurationLogValve.class);

   public String getInfo() {
      return "DurationLogValve";
   }

   // be aware: Request and Response are Tomcat-internal interfaces that
   // act as a facade to ServletRequest and ServletResponse:
   public void invoke(Request request, Response response, ValveContext context) 
         throws IOException, ServletException {
      
      long duration, starttime = System.currentTimeMillis();
      
      String url = null;
      
      // since the method getRequestURL is not provided by 
      // org.apache.catalina.HttpRequest we go to the object itself
      if (request instanceof HttpRequest) {
         HttpServletRequest sR = (HttpServletRequest)request.getRequest();
         url = sR.getRequestURL().toString();
      }

      // pass the request on to the next element in the pipeline
      context.invokeNext(request, response);

      duration = System.currentTimeMillis() - starttime;
      if (logger.isDebugEnabled()) {
         logger.debug("duration(valve): " + duration + " - " + url);
      }
   }

   public void setCategory(String category) {
      DurationLogValve.logger = LogFactory.getLog(category);
      logger.info("category: " + category);
   }
}

Eine Registrierung eines Valve erfolgt in der server.xml im Verzeichnis conf unterhalb des Root-Verzeichnisses der Tomcat-Installation.5 Um unseren DurationLogValve zu registrieren, packen wir bspw. die folgende Zeile unterhalb des <Engine>-Elements:
<Valve className="org.jsptutorial.util.filters.DurationLogValve" category="DurationLog"/>

Wie man sieht, kann man einfach beliebige Parameter als Attribute des XML-Elements Valve hinzufügen. In unserem Fall wird durch das Hinzufügen des Parameters category der Wert des Attributs an die Methode setCategory(String) unseres DurationLogValve-Objektes übergeben.
Damit Tomcat die Klassen auch findet, genügt es nicht, diese wie üblich in WEB-INF/lib als jar-Datei oder unter WEB-INF/classes als ausgepackte Dateien bereit zu stellen. Sie müssen vielmehr dem Server schon beim Laden der Engine zur Verfügung stehen. Dazu packt man diese in das Verzeichnis server/lib oder server/classes unterhalb des Root-Verzeichnisses der Tomcat-Installation. Das gilt natürlich auch für alle benötigten Bibliotheken und ggf. auch Konfigurationsdateien - in unserem Fall bspw. die commons-logging- und die log4j-Bibliotheken.
Tomcat bringt bereits einige Valves mit, die nur noch wie gerade beschrieben registriert werden müssen. Dabei ist für Standalone-Servlet-Engines das AccessLogValve vermutlich eines der meistgenutzten. Es erzeugt Access-Logs in einem der gängigen Log-Formate, um diese anschließend mit einem Logtool wie bspw. AWStats auswerten zu können.
Hier eine Beispiel-Konfiguration für einen AccessLogValve:
<Valve className="org.apache.catalina.valves.AccessLogValve"
      directory="logs"
      prefix="access_log."
      suffix=".log"
      pattern="combined"/>

Der Dateiname der Logdateien setzt sich dann aus dem Prefix, dem Datum in der Form YYYY-MM-DD und dem Suffix zusammen. Es wird somit für jeden Tag eine Log-Datei erzeugt. Diese kommt in das Verzeichnis "logs" und nutzt das "combined"-Format, das neben den Angaben zur eigentlichen Ressource und der Dauer der Anfrage auch noch den Referer (die Seite, von der aus die aktuelle Ressource verlinkt wurde) und den UserAgent enthält.
Zudem gibt es noch spezielle Filter-Valves, die bspw. genutzt werden können, um den Zugang zum Server auf bestimmte IP-Adressen zu beschränken oder Valves, die Debugging-Informationen bereitstellen.
Was Valves gegenüber den oben beschriebenen Servlet-Filtern mitunter geeigneter erscheinen lässt, ist die Möglichkeit, diese für den gesamten Tomcat-Server, einen virtuellen Host oder einem Servlet-Context zu registrieren. Bspw. wenn man alle Anfragen an einen bestimmtern virtuellen Host loggen will, registriert man das Valve im Host (praktisch gesprochen bedeutet das, man platziert das Valve-XML-Element unterhalb des Host-XML-Elements in der server.xml).
Die Trennung würde ich dort ziehen, wo man zwischen Web-Anwendung und Servlet-Engine unterscheiden will. Ein Access-Log gehört für mich zur Servlet-Engine und wird damit zu recht von Tomcat in Form eines Valves mitgeliefert. Der im Kapitel Internationalisierung beschriebene Filter zur Kodierung des einkommenden Requests ist spezifisch für die jeweilige Web-Anwendung und ist somit ein typischer Kandidat für einen Filter.

Zum SeitenanfangZum Seitenanfang

Anmerkungen:

1) Die Response kann freilich noch von Filtern, die in der Kette vor dem aktuellen Filter liegen, verändert werden. (zurück)

2) Der Deployment-Deskriptor ist die Datei web.xml im Verzeichnis WEB-INF der jeweiligen Web-Anwendung. S. dazu das Kapitel Verzeichnisstruktur von Java-Webanwendungen. (zurück)

3) Bis zur Servlet-Version 2.3 war die Reihenfolge der Elemente unterhalb von web-app durch die DTD genau vorgeschrieben. Mit dem Wechsel auf eine XML-Schema-basierte Definition der Deployment-Deskriptor-Elemente ist die Reihenfolge der Kind-Elemente von web-app nicht mehr festgelegt. (zurück)

4) Das Rollenmodell der Java Enterprise-Spezifikationen gilt so natürlich in erster Linie für große Firmen/Teams. Häufig genug wird mindestens eine Anwendungs-Entwicklerin des Entwicklungsteams am Ende auch die Rolle der Anwendungs-Assemblerin einnehmen. Auch wenn in der Praxis nicht unbedingt immer die Rollen so sauber getrennt sind, wie in den Specs beschrieben, so macht diese gedankliche Trennung doch Sinn und hilft den Entwicklungsprozess zu strukturieren. (zurück)

5) Alternativ ist für rein Kontext-bezogene Valves auch ein Eintrag in der Tomcat-spezifischen Datei context.xml im META-INF-Verzeichnis der Webanwendung möglich. (zurück)


www.jsptutorial.org
© 2005, 2006, 2007