Java Servlet-Filter in der Webapplikation

Mehr als Kaffeefilter

Dapeng Wang

Filtering ist eine der wesentlichen Neuerungen in der Java Servlet-Spezifikation 2.4. Obwohl die API-Änderung minimal ist, stellt Servlet-Filtering ein mächtiges Konzept dar, mit dessen Hilfe sich viele Aufgaben einfach lösen lassen. In diesem Artikel werden zuerst Entwicklung und Deployment von Filtern in einer Webapplikation beschrieben, und später diskutiert, welche Rolle ein Filter in der Model-2-Architektur einer Webapplikation spielen könnte.

Filter-Konzept

Ein Filter ist laut Java Servlet-Spezifikation eine wiederverwendbare Komponente, welche den Inhalt oder die Headerinformationen von Servlet Request oder Response lesen und modifizieren kann. Ein Filter schaltet sich immer in die Verarbeitungskette eines Requests ein, sodass er entweder Pre-Processing vor der eigentlichen Ressource (Servlet, JSP bzw. andere statische Ressourcen) oder Post-Processing nach der Ressource betreiben kann. Während der Pre- oder Post-Processing-Phase hat ein Filter die Möglichkeit, den Inhalt oder Headerinformationen zu transformieren oder zu modifizieren.

Obwohl ein Filter auch wirklich Requests wegfiltern kann (eigentlich blockieren, wie später noch gezeigt wird), ist er vom Konzept her nichts anderes als ein Interceptor, der schon seit langem in der CORBA-Welt existiert. Ein Interceptor bzw. ein Filter erlaubt es, vertikale Dienste oder zusätzliche Funktionalität dynamisch zu der eigentlichen Request-Verarbeitung hinzuzufügen, ohne dass der Anwendungsentwickler was davon merkt. Diese vertikalen Dienste kümmern sich zumeist um bestimmte technische Aspekte, die nicht viel mit der Anwendungslogik zu tun haben, daher werden sie auch technische Dienste genannt. Typische Beispiele solcher technischen Diensten sind Logging, Performancemessung, Authentication und Authorization. Das Interceptor-Konzept hat sich schon seit langem in der Praxis bewährt, sodass viele Basisdienste in CORBA auf der Grundlage von Interceptor realisiert sind. Informationen über Transaktions- oder Securitykontext können z.B. mit Hilfe von Interceptoren transparent beim Remote-Aufruf übertragen werden, ohne dass ein Anwendungsentwickler explizit diese Informationen als Parameter aufnehmen muss.

Auch in der Servlet-Welt sind Interceptoren bekannt. Ab Tomcat 3 ist ein Interceptor-Konzept in der Architektur vorgesehen, sodass mitgelieferte oder benutzerdefinierte Interceptoren in die Request-Verarbeitungskette eingepluggt werden können. Auch in der Catalina-Architektur für die 4er-Versionen von Tomcat wird das Konzept unter neuem Namen (Valve) fortgesetzt. Ein Valve schaltet sich wie ein Filter immer in die Request-Verarbeitung ein und kann zusätzliche Aufgabe (wie Logging) übernehmen. Während dieser nur für den internen Gebrauch in Catalina vorgesehen ist, kann ein Anwendungsentwickler durch Filter alles erreichen, was mit einem Valve oder Interceptor möglich ist.

Das Filter-Konzept basiert auf dem Chain Of Responsibility-Pattern, dessen Idee darin liegt, den Sender und den Empfänger bei der Request-Verarbeitung durch mehrere Zwischenobjekte zu entkoppelt, sodass diese ebenfalls den Request bzw. Response verarbeiten oder modifizieren können. Es wird eine Verarbeitungskette zwischen Sender und Empfänger mit den Zwischenobjekten als Kettenglieder aufgebaut. Im Normalfall delegiert ein Kettenglied die Request-Verarbeitung weiter an das nächste, bis der Request letztlich beim Empfänger ankommt.

Wenn man das Chain Of Responsibility-Pattern auf die Servlet-Welt überträgt, ergibt sich daraus folgendes Bild 1.

Abb.1: Filter in der Request-Verarbeitung

Wenn ein Client, der nicht in Abpictureung 1 eingezeichnet ist, ein Request schickt, wird dieser nicht direkt an die Zielressource abgesetzt. Stattdessen wird zuerst vom Servlet-Container geprüft, ob bestimmte Filter für diesen Request definiert sind, der dann an den ersten Filter weitergeleitet wird. Nun hat dieser die Möglichkeit, die entsprechende Verarbeitung zu starten. Im normalen Fall leitet ein Filter nach seiner Verarbeitung den Request an den nächsten Filter weiter, bis die eigentliche Zielressource erreicht ist. Die Response durchläuft ebenfalls die Filterkette, bloß in umgekehrter Reihenfolge. Zu diesem Zeitpunkt haben die Filter nochmals die Möglichkeit, die Response zu modifizieren. Es ist aber durchaus möglich, dass ein Filter die Weiterleitung blockiert und selbst eine Response an den anfragenden Client schickt, sodass die Zielressource gar nicht erreicht wird.

Filter werden in erster Linie zur Lösung bestimmter wiederkehrender Aufgabe wie Logging, Sessionvalidierung (zu prüfen, ob die HTTP-Session noch gültig ist), Zugriffsberechtigung oder Performancemessung verwendet. Diese Aufgaben hängen nicht direkt mit der Anwendungslogik zusammen, sondern werden oft als zusätzliche technische Dienste benötigt. Statt dieselbe Funktion an mehreren Stellen (z.B. in mehreren Servlets) zu implementieren, kann sie in einem Filter gekapselt und mit den Servlets verknüpft werden. Typische Anwendungsszenarien sind:

  • Logging: Ein Filter ist ideal, um Zugriffe zu protokollieren.
  • Tracking: Ein Filter kann Benutzerinteraktionen protokollieren.
  • Authentication (oder Sign-On): Stellt sicher, dass der Benutzer angemeldet und die Session nicht abgelaufen ist.
  • Authorization: Kann die Zugriffsberechtigung prüfen und unberechtigte blockieren.
  • Performancemessung: Ein Filter kann Start- und Endzeitpunkt eines Request messen und so die Verarbeitungszeit für Requests ermitteln.
  • Datenkomprimierung: Ein Filter kann Daten komprimieren damit die Übertragung schneller abläuft.
  • Verschlüsselung: Ein Filter kann die ursprüngliche Response verschlüsseln.
  • XSLT-Transformation: Responses im XML-Format können innerhalb eines Filters durch XSLT-Stylesheets transformiert werden.

Jeder Filter muss das Interface javax.servlet.Filter implementieren, das in der Java Servlet-Spezifikation 2.3 eingeführt ist. Dieses Interface definiert drei Methoden, die ähnlich wie ein Servlet die drei Zustände im Lebenszyklus eines Filters kennzeichnen.

Die Methode init(FilterConfig config) ist für die Initialisierung von Filtern vorgesehen und wird einmalig im Lebenszyklus vom Servlet-Container aufgerufen. Als Parameter liefert der Servlet-Container ein javax.servlet.FilterConfig-Objekt mit, das Filterkonfiguration und Kontextinformationen kapselt. Ähnlich wie javax.servlet.ServletConfig im Falle von Servlet kann ein Filter in der Initialisierungsphase über ServletConfig die Initialparameter abfragen, die im Deployment-Descriptor spezifiziert sind. Dafür sind die Methoden String getInitParameter(String string) und Enumeration getInitParameterNames() vorgesehen. Darüber hinaus ermöglicht die Methode ServletContext getServletContext() den Zugriff auf den zugehörigen ServletContext, den man sich eventuell merken sollte, falls man später im Filter auf den Request Dispatching-Mechanismus vom Servlet zurückgreifen will.

Nach der Initialisierung ist der Filter bereit Requests zu filtern. Bei jedem Request, dem ein Filter zugeordnet ist, wird die doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)-Methode des Filters von Servlet-Container aufgerufen. Der Filter bekommt dabei sowohl das Request- als auch das Response-Objekt als Parameter, sodass er den Inhalt und Headerinformationen von beiden lesen und modifizieren kann. Ist der Filter für HTTP-Anfragen vorgesehen, muss der Filter diese Objekte zu den HTTP-Varianten downcasten, damit er auf die HTTP-spezifischen Informationen zugreifen kann.

Interessanter als das Request- und Response-Objekt ist der dritte Parameter vom Typ javax.servlet.FilterChain. Ein FilterChain ist in der Spezifikation als Interface definiert. Und stellt die Verarbeitungskette dar, die mehrere Filter enthalten kann. Durch Aufruf der Methode doFilter(ServletRequest request, ServletResponse response) in diesem Interface wird der Request in der Verarbeitungskette weitergeleitet. Jeder Servlet-Container, der Filter unterstützt, muss eine Implementierung für FilterChain bereitstellen.

Ein Filter kann eingesetzt werden, um Pre-Processing zu betreiben. In diesem Fall wird die filterspezifische Aufgabe vor der Weiterleitung der Request bearbeitet. So kann ein Filter den Request auch blockieren, wenn z.B. eine Session abgelaufen und eine neue Anmeldung notwendig ist. Beispiele von Pre-Processing-Filtern sind: Logging-Filter, Authentication-Filter, Authorization-Filter. Das Sequenzdiagramm eines Pre-Processing-Filters ist in Abb. 2 dargestellt. Zur Veranschaulichung wird in Listing 1 ein Logging-Filter gezeigt, der den Request-URL in die Logdatei des Kontexts schreibt.

Abb.2: Pre-Processing-Filter

Es ist auch möglich, dass ein Filter erst auf dem Rückweg der Verarbeitungskette aktiv wird. In diesem Fall wird der Request zuerst ohne zusätzliche Verarbeitung weitergeleitet. Wenn die Methode filterChain.doFilter zurückkehrt, kann der Filter noch das Response-Objekt nachträglich modifizieren oder ersetzen. Beispiele für Post-Processing-Filter sind Datenkomprimierungsfilter oder Verschlüsselungsfilter. Das Sequenzdiagramm eines Post-Processing-Filters ist in Abpictureung 3 dargestellt

Abb.3 Post-Processing-Filter

Natürlich kann ein Filter sowohl vor der Weiterleitung als auch danach Arbeit verrichten. Ein typisches Beispiel ist ein Filter, der die Verarbeitungszeit eines Requests ermittelt (Listing 2).

Listing 2: Performance-Filter

Abb. 4: Vererbungshierarchie von Wrappers

Innerhalb der doFilter-Methode hat ein Filter die Möglichkeit, das Request- und (oder) das Response-Objekt bei der Weiterleitung durch Wrappers zu ersetzen (siehe Listing 3).

Listing 3: Request- und ResponseWrapper

public class ModifiedServletRequestWrapper extends HttpServletRequestWrapper {
public ModifiedServletRequestWrapper(HttpServletRequest request) {
super(request);
}

public String getParameter(String parm) {
if(parm.equals("LIMIT"))
return "1000";
else
return super.getParameter(parm);
}

public String getHeader(String name) {
if(name.equalsIgnoreCase("if-modified-since"))
return null;
else
return super.getHeader(name);
}
}

Dagegen hat ein ResponseWrapper die Funktion, die ursprüngliche Response abzufangen, dieses zu transformieren, zu modifizieren oder zu komprimieren, und das Ergebnis als Response an den Client zu schicken. Daher überschreibt ein ResponseWrapper meistens die Methode getWriter() und getOutputStream(). So wird das ursprüngliche Response von der Zielressource nicht in das OutputStream der HTTP-Verbindung, sondern in das des Wrappers geschrieben, sodass der Filter die Daten nachträglich manipulieren kann, bevor diese an den Client weitergeschickt werden. Komprimierungsfilter, Verschlüsselungsfilter oder XSLT-Transformationsfilter arbeiten alle mit ResponseWrapper. Da sich das Endergebnis nach der Manipulation wahrscheinlich stark vom ursprünglichen Ergebnis unterscheidet, muss der Content Length Header im Filter neu gesetzt werden (Listing 5).

Listing 5: CharArrayServletResponseWrapper

public class ReplaceFilter extends HttpServlet implements Filter {
public void init(FilterConfig filterConfig) {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException{
HttpServletResponse resp = (HttpServletResponse)response;
CharArrayServletResponseWrapper wrapper = new CharArrayServletResponseWrapper(resp);
filterChain.doFilter(request, wrapper);
String responseString = wrapper.toString();
responseString = Utils.replace(responseString, "&", "&");
response.setContentLength(responseString.length());
response.getWriter().write(responseString);
}

public void destroy() {
}
}

Nachdem ein Filter codiert ist, muss er deployt werden, um zwischen Requests (bzw. Ressourcen) und Filtern Zuordnungen zu definieren, welche werden im Deployment-Descriptor WEB-INF/web.xml deklarativ festgelegt. So kann man flexibel bestimmen, ob technische Dienste (Z.B.: Logging, Performancemessung, Authorization) für eine Webapplikation aktiviert werden sollen und muss sie nicht im Code fest verdrahten.

Bevor ein Filter bestimmten Ressourcen zugeordnet werden kann, muss dieser zuerst unter einem eindeutigen Namen registriert werden (Listing 7).

Listing 7: Filterdefinition

textfiltercom.develop.filter.TextFiltertextHello World

Neben dem Namen muss bei der Deklaration der vollständige Klassenname des Filters angegeben werden. Darüber hinaus besteht die Möglichkeit, zusätzliche Initialparameter zu definieren, die der Filter zur Laufzeit mit Hilfe von FilterConfig auslesen kann.
Der nächste Schritt besteht darin, einen registrierten Filter mit Ressourcen zu verknüpfen. Dabei kann er sowohl mit einem Servlet als auch mit einer Gruppe von Ressourcen assoziiert werden.

Listing 8: Filter-Mapping

textfilterhelloworldservlettextfilter/*.jsp

Im Falle von Servlet muss der symbolische Name angegeben werden, unter dem ein Servlet in dem Deployment-Descriptor registriert ist. Alternativ kann man Filter mit einer Gruppe von Ressourcen “ die meistens über ein Erweiterungsmuster gekennzeichnet wird “ assoziieren. Filter können nicht nur dynamischen Ressourcen wie Servlets oder JSP, sondern auch statischen Ressourcen wie HTML-Dokumenten oder Image-Dateien zugeordnet werden. Ein URL-Pattern von /* gibt an, dass der Filter mit allen Ressourcen verknüpft ist (siehe Abb. 5).

Abb. 5: Filter-Mapping

Wenn eine Ressource mit mehreren Filtern assoziiert ist, muss die Reihenfolge des Aufbaus der Filterkette zur Laufzeit bekannt sein. Eine falsche Reihenfolge von Filtern kann Fehler produzieren: Wird z.B. ein Authorization-Filter bei beendeter Benutzersession vor einem Authentication (oder Session)-Filter aufgerufen, kann Letzterer nicht feststellen, dass eine neue Anmeldung notwendig ist, um den Benutzer zu identifizieren. Der Authorization-Filter verwendet stattdessen für die Prüfung der Zugriffsberechtigung die in der Session abgelegte Benutzerinformation. Die Reihenfolge, in der die Filter für einen Request zusammengekettet werden, ist durch die entsprechenden -Elemente für diesen Request in dem Deployment-Descriptor bestimmt. Im obigen Beispiel soll im Deployment-Descriptor das Mapping von Authentication-Filter vor dem Mapping von Authorization-Filter deklariert werden.

Filter in MVC

Ein Servlet wird in Java implementiert und ist dadurch gut geeignet, um Logik zu implementieren. JSP enthält in erster Linie HTML-Elemente und ist somit gut für den Aufbau der Präsentation geeignet. Für den Datenaustausch zwischen Servlets und JSP können Java Beans verwendet werden, die in einem Austauschscope abgelegt werden. Diese drei Komponenten zusammen pictureen die Model-2-Architektur für Webapplikationen, die auf dem MVC-Pattern (Model, View, Controller) basiert. In dieser Architektur agiert ein Servlet als Controller und Dispatcher, der meistens die Logikimplementierung an eine Action-Klasse delegiert. Während der Verarbeitung werden die Java Beans, die als Models fungieren modifiziert. Nach der Verarbeitung wird die Kontrolle an die View-Komponente, in diesem Fall eine JSP-Seite, weitergeleitet. Diese hat die Möglichkeit, den Inhalt von Java Beans in der Ergebnisseite darzustellen.

Diese Model-2-Architektur hat sich schnell der Beliebtheit der Entwickler erfreut. Es gibt mittlerweile mehrere Frameworks wie Struts oder Webworks, die auf dieser Architektur basieren. Trotzdem hat diese Architektur mit einigen Problemen zu kämpfen.

  • Das Dispatcher Servlet (in Struts ActionServlet und in J2EE-Pattern-Katalog Front-Controller genannt) muss alle benötigten Dienste fest verdrahten. Obwohl die Idee, nur ein einziges Servlet zu benutzen, darauf basiert, dass bestimmte sich wiederholende Aufgaben an einer Stelle einmal gelöst werden, müssen diese Funktionalitäten trotzdem in diesem Servlet fest kodiert werden. Neue Anforderungen (z.B. Logging, Authorization, Performancemessung usw.) haben sehr wahrscheinlich eine Code-Änderung zur Folge.
    Die Zuordnung zwischen Controller (Servlet oder Action) und View (JSP) ist schwer zu verwalten. Es ist oft nicht sofort erkennbar, welche JSP mit welchem Servlet (bzw. Action) verknüpft ist.
  • Wenn zwei Views zur selben Logik erforderlich sind (HTML- und XML-Präsentation), müssen zusätzliche Information im Request vorhanden sein, damit der Controller die richtige View (html.jsp oder xml.jsp) aussuchen kann. Eine andere Möglichkeit besteht darin, für jede Familie von Views einen separaten Controller im Deployment-Descriptor zu definieren, der dann nur eine Art von Views kennt. Dafür muss der Client immer den richtigen Controller ansprechen, damit er das korrekte Präsentationsformat zurückbekommt.
  • Weil JSP-Seiten nur die Views darstellen, die das Ergebnis nach der Logikverarbeitung anzeigen, ist ein direkter Aufruf von den JSP-Seiten oft sinnlos, da die für die Anzeige benötigten Model-Informationen noch gar nicht vorhanden oder aktuell sind. Trotzdem ist schwer zu vermeiden, dass ein Benutzer die URL eines JSP eingibt und die Seite direkt aufruft. Eine Lösung für diese Problematik liegt darin, alle JSP-Seiten unter WEB-INF/ abzulegen (damit sind die Seiten nicht direkt aufrufbar) und jede Seite explizit im Deployment-Descriptor als Servlet zu definieren, sodass der Controller über getNamedDispacther() die Kontrolle delegieren kann. Dies erfordert einen hohen Aufwand, weil jede JSP als Servlet unter einem Namen registriert werden muss.

Das erste der vier genannten Probleme ist etwas anders einzuordnen als die restlichen drei. Hierfür ist der Einsatz von Filtern ideal um fest verdrahtete Dienste zu vermeiden. Durch Bereitstellung von mehreren Filtern, die jeweils einen technischen Dienst kapseln, können alle diese Dienste dynamisch im Deployment-Descriptor deklarativ konfiguriert werden. Das Controller-Servlet kann sich an der Stelle auf seine Aufgabe als Dispatcher konzentrieren.

Die drei anderen Probleme hängen mehr oder weniger zusammen. Eigentlich stellt eine JSP-Seite den Endpunkt eines Requests dar, weil die JSP auch die Response liefert. Wünschenswert ist natürlich, diesen Endpunkt direkt anzusprechen. Wenn dies möglich wäre, d.h. dass der Client direkt HTML.jsp oder XML.jsp aufruft, wäre das dritte Problem gelöst. Das funktioniert aber bis jetzt nicht, weil man keine Logik in der JSP implementieren will. Daher wird immer ein Servlet vorgeschaltet, das die Logik implementiert und dann per RequestDispatcher die Kontrolle an JSP delegiert. An statt dessen kann auch ein Filter verwendet werden. Dieser kann wie das Servlet die Dispatching-Aufgabe an Action-Klassen übernehmen. Darüber hinaus ist keine explizite Delegation über RequestDispatcher notwendig. Ein Filter mit dem Aufruf chain.doFilter() kann als Servlet mit automatisch eingebautem Dispatching angesehen werden. Durch den Austausch von Servlet durch einen Filter kann jede JSP-Seite vom Benutzer direkt angesprochen werden. Auch wird die Zuordnungen zwischen Servlet und JSP überflüssig, sodass auch Problem Zwei gelöst ist. Weil der Filter, der hier in der Model-2-Architektur als Dispatcher fungiert, immer vor der eigentlichen Ressource (in diesem Fall, die JSP-Seite) angesprochen wird, kann er vor der Weiterleitung die Logikimplementierung aufzurufen, sodass die JSP-Seite als View-Komponente immer Zugriff auf die richtigen Models hat.

Durch den Einsatz eines Filters an Stelle eines Servlets können die oben genannten Probleme in der Model-2-Architektur elegant gelöst werden. Der Benutzer kann immer direkt die JSP-Seiten als Endpunkte aufrufen. Trotzdem sind Filter kein Ersatz für Servlets. Obwohl beide Requests verarbeiten und Response produzieren können, kann ein Servlet als Endpunkt für die Beantwortung eines Requests benutzt werden. Es ist zwar theoretisch auch möglich, dass ein Filter bestimmte Requests immer ohne Weiterleitung beantwortet, aber das entspricht nicht dem ursprünglichen Konzept. Filter ist nach wie vor am besten geeignet, ein Zwischenglied in der Request-Verarbeitungskette zu spielen, sei es ein Interceptor oder ein Dispatcher.

Filter in der Zukunft

Das Filter-Konzept hat noch eine entscheidende Schwäche. Es können nur vom Client stammende Requests gefiltert werden. Es ist nicht möglich, Filter in die Weiterleitung (forward) oder Einbeziehung (include) des Request-Dispatchers zu schalten. Da aber der Request-Dispatching-Mechanismus in den Webapplikationen so gerne eingesetzt wird, ist es oft nicht leicht zu unterscheiden, ob ein Zugriff von Client stammt oder von einer anderen Ressource im selben Servlet-Container. Daher ist es für forward und include mehr als sinnvoll, ebenfalls auf Filter zurückgreifen zu können.

Diese Schwäche haben auch die Leute erkannt, die die nächste Servlet-Version spezifizieren. So wird in der Servlet-Spezifikation 2.4, die im Moment noch als Proposed Final Draft vorliegt, der Filter-Mapping-Mechanismus erweitert.

Ein Subelement für das -Element ist eingeführt, das eines der drei Werte einnehmen kann: Request, Forward, Include. Um die Abwärtskompatibilität zu gewährleisten, bedeutet ein -Element ohne das -Subelement, dass dieser Zuordnung nur für Client Requests gilt. Ansonsten kann ein Filter so gemappt sein, dass er sowohl für Client Requests, als auch für forward und include durch Request Dispatcher aktiviert wird.

Listing 9: Filter-Mapping in Servlet 2.4

textfilterhelloworldservletREQUESTFORWARDINCLUDE

Mit den Filtern wird ein erprobtes Konzept in die Servlet-Welt eingeführt. Durch die Beschränkung der API-Neuerung auf drei Klassen kann man mit minimalem Lernaufwand viele Aufgaben in einer Webapplikation elegant erledigen. Durch den eingebauten Dispatching-Mechanismus können Filter an vielen Stellen Servlets ersetzen. Vor allem der Einsatz von Filter kann einige vorhandene Probleme in der jetzigen Model-2-Architektur lösen.

Geschrieben von
Dapeng Wang
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.