Servlet-Filter

(Auszug aus "Java und XSLT" von Eric M. Burke)

Die Version 2.3 der Java Servlet-Spezifikation führt das neue Feature filter ein. Ein Filter ist ein Objekt, das Anfragen an ein Servlet, eine JSP oder eine statische Datei einer Webanwendung abhört. Er hat die Möglichkeit, Anfragen und Antworten zu ändern, bevor sie an die zugrundeliegende Ressource bzw. den Client weitergereicht werden. Da Filter im Anwendungsdeskriptor deklariert werden, können sie ohne Modifikationen an bestehendem Code einer Webanwendung integriert werden.

Überblick

Servlet-Filter können für viele Zwecke wie Protokollierung, Nutzer-Authentifizierung, Datenkompression, Verschlüsselung und XSLT-Transformationen eingesetzt werden. Es können viele Filter verkettet werden, wobei jeder eine spezielle Aufgabe erfüllt. Für dieses Buch ist der Einsatz für XSLT-Transformationen interessant. Die folgende Abbildung illustriert die allgemeine Filterarchitektur.

Servlet-Filter

Abbildung: Servlet-Filter

Das Interface javax.servlet.Filter muß von allen Filtern implementiert werden. Es definiert die folgenden Methoden:

void init(FilterConfig config)
void destroy( )
void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

Die Methoden init( ) und destroy( ) sind identisch zu den Methoden init( ) und destroy( ) von Servlets. init( ) wird aufgerufen, wenn der Filter geladen wird, wobei der Parameter FilterConfig den Zugriff auf den ServletContext und auf eine Liste von Initialisierungsparametern ermöglicht. Der Code des Beispiels Ein Servlet-Filter für XSLT-Transformationen demonstriert diese Eigenschaft. Die Methode destroy( ) wird beim Beenden des Filters aufgerufen, so daß er die Möglichkeit bekommt, belegte Ressourcen freizugeben.

Die Methode doFilter( ) wird beim Eingang einer Client-Anfrage ausgeführt. Der Filter ist Teil einer FilterChain, die vom Servlet-Container eingerichtet wird und die Verkettung mehrerer Filter erlaubt. Wenn ein Filter eine Anfrage zurückweisen will, reicht es, wenn er einfach nur nichts tut. Ansonsten muß er die Kontrolle an das nächste Element in der Kette weitergeben:

chain.doFilter(req, res);

Dieses nächste Element kann ein weiterer Filter, ein Servlet oder eine JSP sein. Der Filter selbst muß darüber nichts wissen.

Der Aufruf von doFilter(req, res) gibt die Kontrolle an das nächste Element der Kette weiter. Um eine Anfrage oder Antwort zu ändern, muß der Filter das Objekt ServletRequest bzw. ServletResponse modifizieren. Leider handelt es sich bei beiden um Interfaces, und ihre Implementierungen sind für jeden Servlet-Container unterschiedlich. Außerdem lassen die Interfaces keine Änderungen ihrer Werte zu.

Um dieses Problem zu lösen, werden in der Version 2.3 der Servlet-API Wrapper-Klassen eingeführt, die Änderungen an Anfragen und Antworten ermöglichen. Es handelt sich dabei um die folgenden Klassen:

  • javax.servlet.ServletRequestWrapper
  • javax.servlet.ServletResponseWrapper
  • javax.servlet.http.HttpServletRequestWrapper
  • javax.servlet.http.HttpServletResponseWrapper

Jede dieser Klassen umschließt eine Anfrage oder Antwort, und alle Methoden werden an das umschlossene Objekt weitergereicht. Um das Verhalten zu ändern, muß von einer dieser Klassen abgeleitet und eine oder mehrere Methoden überschrieben werden. So könnte ein selbstgeschriebener Filter aussehen:

public class MyFilter implements Filter {
     public void doFilter (ServletRequest req, ServletResponse res,
              FilterChain chain) throws IOException, ServletException {
         // umschließe die Anfrage und die Antwort
         MyRequestWrapper reqWrap = new MyRequestWrapper(req);
         MyResponseWrapper resWrap = new MyResponseWrapper(res);
                
         // gib die Wrapper an das nächste Element weiter
         chain.doFilter(reqWrap, resWrap);
     }
}

In diesem Fall werden die Anfrage und die Antwort von MyRequestWrapper bzw. MyResponseWrapper modifiziert. Das ist für viele einfache Filter ausreichend, es wird jedoch komplizierter, wenn der Inhalt der Antwort geändert werden muß. Um dies zu verdeutlichen, schauen wir uns die Methode getOutputStream( ) von javax.servlet.ServletResponse an:

public interface ServletResponse {
  ServletOutputStream getOutputStream( ) throws IOException;
  ... weitere Methoden
}

javax.servlet.ServletResponseWrapper definiert dieselbe Methode wie folgt:

public class ServletResponseWrapper implements ServletResponse {
  private ServletResponse response;
  
  public ServletResponseWrapper(ServletResponse response) {
    this.response = response;
  }
  
  // die Default-Implementierung verwendet die umschlossene Antwort
  public ServletOutputStream getOutputStream( ) throws IOException {
    return this.response.getOutputStream( );
  }
  
  ... weitere Methoden verhalten sich entsprechend
}

Um die Antwort, die zum Clientbrowser geschickt wird, zu ändern, muß die Wrapper-Unterklasse die Methode getOutputStream( ) überschreiben:

public class MyResponseWrapper extends ServletResponseWrapper {
  
  public ServletOutputStream getOutputStream( ) throws IOException {
    
    // Der ServletOutputStream der Basisklasse kann nicht zurückgegeben
    // werden, weil wir nicht auf die Ausgabe zugreifen dürfen. Deshalb wird
    // eine eigene Unterklasse von ServletOutputStream zurückgegeben:
    return new MyServletOutputStream( );
  }
}

ServletOutputStream ist eine abstrakte Klasse, die keine Methoden zum Ändern ihres Inhalts bereitstellt. Statt dessen müssen Unterklassen von ServletOutputStream geschrieben werden, bei denen man auf die Ausgabe zugreifen und nötige Änderungen vornehmen kann. Das macht die Modifikation der Servlet-Antwort so kompliziert.

Filter für XSLT-Transformationen

Bis jetzt wurden einige Konzepte von Servlet-Filtern vorgestellt, ohne zu sehr ins Detail zu gehen. Als nächstes wird ein Beispiel vorgestellt, in dem ein Filter für XSLT-Transformationen eingesetzt wird. Dadurch werden die behandelten Themen bestimmt etwas klarer.

Das Ziel ist, einen Servlet-Filter zu erstellen, der XSLT-Transformationen ausführt. Die XML-Daten für die Transformation kommen von einem Servlet, einer JSP oder einer statischen XML-Datei. Der Filter soll die XML-Daten abfangen, bevor sie zum Clientbrowser geschickt werden, und eine XSLT-Transformation durchführen. Das Ergebnis wird dann zum Browser gesendet.

Das folgende Beispiel zeigt die erste der drei Klassen dieses Beispiels. Es handelt sich um eine Unterklasse von ServletOutputStream, die die Ausgabe abfängt und in einem Byte-Feld puffert. Die XML-Daten werden in diesem Puffer zwischengespeichert, bevor sie transformiert werden.

Beispiel: BufferedServletOutputStream.java

package com.oreilly.javaxslt.util;

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

/**
 * Ein selbstgeschriebener ServletOutputStream, der die Daten in einem Puffer
 * zwischenspeichert und sie nicht gleich an den Client weiterleitet.
 *
 * @author Eric M. Burke
 */
public class BufferedServletOutputStream extends ServletOutputStream {
  // der Puffer
  private ByteArrayOutputStream bos = new ByteArrayOutputStream( );
  
  /**
   * @return den Inhalt des Puffers.
   */
  public byte[] getBuffer( ) {
    return this.bos.toByteArray( );
  }
  
  /**
   * Diese Methode muß für eigene ServletOutputStreams definiert werden.
   */
  public void write(int data) {
    this.bos.write(data);
  }
  
  // BufferedHttpResponseWrapper ruft diese Methode auf
  public void reset( ) {
    this.bos.reset( );
  }
  
  // BufferedHttpResponseWrapper ruft diese Methode auf
  public void setBufferSize(int size) {
    // es gibt keine Möglichkeit, die Größe eines ByteArrayOutputStream zu ändern
    this.bos = new ByteArrayOutputStream(size);
  }
}

Die Klasse BufferedServletOutputStream ist von ServletOutputStream abgeleitet. Die einzige abstrakte Methode in ServletOutputStream ist write( ). Deshalb muß unsere Klasse diese Methode definieren. Anstatt die Daten aber zum Client zu schicken, werden sie in einen ByteArrayOutputStream geschrieben. Die restlichen Methoden, reset( ) und setBufferSize( ), werden von der Klasse aus dem folgenden Beispiel benötigt.

Beispiel: BufferedHttpResponseWrapper.java

package com.oreilly.javaxslt.util;

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

/**
 * Ein Response-Wrapper, der die Ausgabe in einem Puffer speichert.
 */
public class BufferedHttpResponseWrapper extends HttpServletResponseWrapper {
  private BufferedServletOutputStream bufferedServletOut
    = new BufferedServletOutputStream( );
  
  private PrintWriter printWriter = null;
  private ServletOutputStream outputStream = null;
  
  public BufferedHttpResponseWrapper(HttpServletResponse origResponse) {
    super(origResponse);
  }
  
  public byte[] getBuffer( ) {
    return this.bufferedServletOut.getBuffer( );
  }
  
  public PrintWriter getWriter( ) throws IOException {
    if (this.outputStream != null) {
      throw new IllegalStateException(
        "Die Servlet-API verbietet den Aufruf von getWriter( )"
        + "nachdem getOutputStream( ) aufgerufen wurde.");
    }
    
    if (this.printWriter == null) {
      this.printWriter = new PrintWriter(this.bufferedServletOut);
    }
    return this.printWriter;
  }

  public ServletOutputStream getOutputStream( ) throws IOException {
    if (this.printWriter != null) {
      throw new IllegalStateException(
        "Die Servlet-API verbietet den Aufruf von getOutputStream( )"
        + "nachdem getWriter( ) aufgerufen wurde.");
    }
    
    if (this.outputStream == null) {
      this.outputStream = this.bufferedServletOut;
    }
    return this.outputStream;
  }

  // überschreibe die Methoden, die mit dem Antwort-Puffer zu tun haben

  public void flushBuffer( ) throws IOException {
    if (this.outputStream != null) {
      this.outputStream.flush( );
    } else if (this.printWriter != null) {
      this.printWriter.flush( );
    }
  }

  public int getBufferSize( ) {
    return this.bufferedServletOut.getBuffer( ).length;
  }

  public void reset( ) {
    this.bufferedServletOut.reset( );
  }

  public void resetBuffer( ) {
    this.bufferedServletOut.reset( );
  }

  public void setBufferSize(int size) {
    this.bufferedServletOut.setBufferSize(size);
  }
}

BufferedHttpResponseWrapper ist eine Erweiterung von HttpServletResponseWrapper und überschreibt alle Methoden, die den Writer oder OutputStream zum Client beeinflussen. Dadurch erhalten wir die volle Kontrolle über die Antwort, bevor sie zum Clientbrowser zurückgeschickt wird.

Entsprechend der Servlet-API können entweder getWriter( ) oder getOutputStream( ) aufgerufen werden, aber nicht beide. In der Wrapper-Klasse für die Antwort müssen beide Methoden unterstützt werden, da man nicht weiß, welche benötigt wird. Das ist ein Punkt der Servlet-Filter-API, der noch verbesserungswürdig scheint.

Warnung:
In der Servlet-Spezifikation ist hiervon noch sehr wenig dokumentiert. Das hat sich hoffentlich bis zur Veröffentlichung dieses Buches geändert. Es gibt bis jetzt nur wenige Beispiele, die demonstrieren, wie eine Antwort abgefangen und modifiziert wird. Das ändert sich bestimmt, sobald mehrere Servlet-Container die Servlet-Spezifikation 2.3 unterstützen.

Die wichtigste Klasse dieses Beispiels ist im folgenden Beispiel zu sehen. Es handelt sich um den eigentlichen Filter, der die XSLT-Transformationen ausführt.

Beispiel: Ein Servlet-Filter für XSLT-Transformationen

package com.oreilly.javaxslt.util;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
/**
 * Eine Hilfsklasse, die die Servlet-Filter-API 2.3 verwendet,
 * um ein XSLT-Stylesheet auf eine Servlet-Antwort anzuwenden.
 *
 * @author Eric M. Burke
 */
public class StylesheetFilter implements Filter {
  private FilterConfig filterConfig;
  private String xsltFileName;
  
  /**
   * Diese Methode wird einmalig beim Laden des Filters aufgerufen.
   */
  public void init(FilterConfig filterConfig) throws ServletException {
    this.filterConfig = filterConfig;
    
    // xsltPath könnte z.B. den String "/WEB-INF/xslt/a.xslt" beinhalten
    String xsltPath = filterConfig.getInitParameter("xsltPath");
    if (xsltPath == null) {
      throw new UnavailableException(
        "der Parameter xsltPath muß angegeben werden."
        + "Bitte prüfen Sie den Anwendungsdeskriptor.");
    }
  
    // konvertiere den relativen Pfad in einen physischen Pfad
    this.xsltFileName = filterConfig.getServletContext( )
      .getRealPath(xsltPath);
  
    // prüfe, ob die Datei existiert
    if (this.xsltFileName == null ||
        !new File(this.xsltFileName).exists( )) {
      throw new UnavailableException(
        "Kann Stylesheet nicht finden: " + this.xsltFileName, 30);
    }
  }

  public void doFilter (ServletRequest req, ServletResponse res,
      FilterChain chain) throws IOException, ServletException {
        
    if (!(res instanceof HttpServletResponse)) {
      throw new ServletException("Dieser Filter unterstützt nur HTTP");
    }
        
    BufferedHttpResponseWrapper responseWrapper =
      new BufferedHttpResponseWrapper((HttpServletResponse) res);
    chain.doFilter(req, responseWrapper);
        
    // Tomcat 4.0 benutzt manchmal gepufferte Instanzen seiner Implementierung
    // von HttpServletResponse. Dies geschieht z.B., wenn der reload( )-Button
    // des Webbrowsers wiederholt betätigt wird. In diesem Fall werden
    // die Ausgaben jedoch nie in den OutputStream von
    // BufferedHttpResponseWrapper geschrieben, d.h. das
    // XML-Ausgabefeld bleibt leer. Der folgende Code behebt diesen
    // Mangel:
        
    byte[] origXML = responseWrapper.getBuffer( );
    if (origXML == null || origXML.length == 0) {
      // Tomcat soll seine zwischengespeicherten Daten zum Client schicken
      chain.doFilter(req, res);
      return;
    }

    try {
      // führe die XSLT-Transformation aus
      Transformer trans = StylesheetCache.newTransformer(
        this.xsltFileName);
        
      ByteArrayInputStream origXMLIn = new ByteArrayInputStream(origXML);
      Source xmlSource = new StreamSource(origXMLIn);
      
      ByteArrayOutputStream resultBuf = new ByteArrayOutputStream( );
      trans.transform(xmlSource, new StreamResult(resultBuf));
      
      res.setContentLength(resultBuf.size( ));
      res.setContentType("text/html");
      res.getOutputStream().write(resultBuf.toByteArray( ));
      res.flushBuffer( );
    } catch (TransformerException te) {
      throw new ServletException(te);
    }
  }
  /**
   * Das Gegenstück zur Methode init( )
   */
  public void destroy( ) {
    this.filterConfig = null;
  }
}

Der Filter benötigt den Namen des XSLT-Stylesheets als Initialisierungsparameter. Die folgende Code-Zeile ermittelt den Parameter aus dem Anwendungsdeskriptor:

String xsltPath = filterConfig.getInitParameter("xsltPath");

Indem das Stylesheet als Parameter übergeben wird, kann der Filter jede beliebige XSLT-Transformation ausführen. Da der Filter auf ein Servlet, eine JSP oder eine statische Datei angewendet werden kann, sind auch die XML-Daten nicht fest vorgegeben.

Die Methode doFilter( ) illustriert eine weitere Schwäche der aktuellen Filter-API:

if (!(res instanceof HttpServletResponse)) {
    throw new ServletException("Dieser Filter unterstützt nur HTTP");
}

Da kein HTTP-spezifisches Filter-Interface existiert, müssen eigene Filter den Operator instanceof und Typcasts verwenden, um sicherzustellen, daß nur HTTP-Anfragen gefiltert werden.

Als nächstes erzeugt unser Filter den Wrapper für die gepufferte Antwort und leitet ihn an das nächste Element in der Kette weiter:

BufferedHttpResponseWrapper responseWrapper =
        new BufferedHttpResponseWrapper((HttpServletResponse) res);
chain.doFilter(req, responseWrapper);

Dadurch wird die XML-Ausgabe abgefangen und die XSLT-Transformation ermöglicht. Vor dem Ausführen der Transformation ist noch ein Trick nötig, damit das Beispiel mit Tomcat 4.0 läuft:

byte[] origXML = responseWrapper.getBuffer( );
if (origXML == null || origXML.length == 0) {
   // veranlasse Tomcat, seine zwischengespeicherten Daten zum Client zu schicken
   chain.doFilter(req, res);
   return;
}

Eine ausführliche Beschreibung ist dem Kommentar im Beispiel Ein Servlet-Filter für XSLT-Transformationen zu entnehmen. Tomcat speichert seine Antwort zwischen, wenn der Nutzer dieselbe statische Datei mehrmals zu laden versucht. Ohne diese Prüfung wäre das Byte-Feld origXML in diesem Falle leer. (Das war nur schwer herauszufinden. Da die Servlet-Spezifikation in diesem Punkt nicht eindeutig ist, können sich andere Servlet-Container durchaus unterschiedlich verhalten.)

Der Filter verwendet JAXP, um schließlich die XSLT-Transformation durchzuführen und das Ergebnis an die ursprüngliche Servlet-Antwort zu senden.

Der zugehörige Anwendungsdeskriptor ist im folgenden Beispiel zu sehen.

Beispiel: Anwendungsdeskriptor für den Filter

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/j2ee/dtds/web-app_2.3.dtd">
<web-app>
    <filter>
        <filter-name>xsltFilter</filter-name>
        <filter-class>com.oreilly.javaxslt.util.StylesheetFilter</filter-class>
        <init-param>
            <param-name>xsltPath</param-name>
            <param-value>/WEB-INF/xslt/templatePage.xslt</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>xsltFilter</filter-name>
        <url-pattern>/home.xml</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>xsltFilter</filter-name>
        <url-pattern>/company.xml</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>xsltFilter</filter-name>
        <url-pattern>/jobs.xml</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>xsltFilter</filter-name>
        <url-pattern>/products.xml</url-pattern>
    </filter-mapping>
</web-app>  

Wie aus den ersten Zeilen des Anwendungsdeskriptors ersichtlich wird, benötigen Filter die Version 2.3 der Webanwendungs-DTD.

Dann wird der Filter-Initialisierungsparameter innerhalb des Elements <filter> spezifiziert. Er definiert den Namen des XSLT-Stylesheets für diese spezielle Instanz des Filters. Es ist auch möglich, mehrere <filter>-Elemente mit derselben Filterklasse und unterschiedlichen Filternamen zu spezifizieren. Eine Webanwendung kann so einen einzigen Filter mit verschiedenen Konfigurationen nutzen.

Der Anwendungsdeskriptor definiert dann einige explizite Zuordnungen für diesen Filter. In dem gezeigten Beispiel wird er auf statische XML-Dateien angewendet. Er könnte jedoch genauso einfach auf ein Servlet oder eine JSP angewendet werden.

Abschließende Bemerkungen zu Filtern

Die Verwendung von Filtern für XSLT-Transformationen bietet die Möglichkeit, verschiedene Stylesheets auf XML-Daten unterschiedlichster Herkunft anzuwenden. Um ein anderes Stylesheet zu verwenden, muß lediglich der Anwendungsdeskriptor der Webanwendung geändert werden. Ein interessanter Ansatz ist es, mit JSP reine XML-Daten zu erzeugen, um diese mit einem Filter in XHTML-Daten für den Client zu transformieren.

Filter haben aber auch Nachteile und sind oft nicht die beste Lösung für ein Problem. Zunächst einmal ist die Filter-API erst ab Version 2.3 der Servlet-Spezifikation verfügbar, und viele existierende Servlet-Container unterstützen gar keine Filter. Bei XSLT-Transformationen muß ein ServletOutputStream geschrieben werden, um die Antwort abzufangen, und da keine HTTP-spezifische Filterklasse existiert, muß auf Downcasts zurückgegriffen werden. Da einige Servlet-Container die Antwort aus Performance-Gründen puffern könnten, müssen für eine verläßliche Implementierung provisorische Lösungen gefunden werden.

Schließlich ist dieser Ansatz langsamer als andere. Die XML-Daten müssen in Text konvertiert und im Speicher gepuffert werden, bevor die XSLT-Transformation durchgeführt werden kann. Das ist natürlich langsamer, als direkt SAX-Ereignisse oder einen DOM-Baum an den XSLT-Prozessor zu senden. Das Erzeugen der XML-Daten und die Ausführung der XSLT-Transformation in einem Servlet vermeidet den zusätzlichen Konvertierungsaufwand, der bei einem Filter nötig ist.

   

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Java & XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Java und XSLT" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de