<jsptutorial />

Fehler- und Ausnahmebehandlung


Das Standardverhalten

Wenn wir Anwendungen entwickeln, haben wir mit einer Menge möglicher Fehlerbedingungen zu tun. Das Netzwerk kann ausfallen, die Nutzerin unerwartete Eingaben vornehmen, Dateien nicht mehr existieren und vieles mehr. Und nicht vergessen sollten wir unsere eigenen Fehler, die - so sehr wir es uns auch anders wünschen mögen - am Ende häufiger vorkommen, als uns lieb ist. Aufgrund der Unvermeidbarkeit von Fehlern ist es um so wichtiger, Fehlersituationen schon im Vorfeld zu bedenken und auftretende Fehlerfälle abzufangen, so dass diese entweder noch behandelt werden können oder - wo dies nicht unmittelbar möglich ist - zumindest geloggt und der Nutzerin verständlich aufbereitet angezeigt werden.
Die Servlet- und JSP-Containerhersteller haben leider ein Standard-Verhalten implementiert, das dazu führt, dass Fehler, die nicht abgefangen werden, auf einer speziellen Fehlerseite angezeigt werden. Dies ist keine Vorschrift der Servlet-Spezifikation, mir ist aber kein Container-Hersteller bekannt, bei dem dies nicht standardmäßig so umgesetzt würde. Die resultiert bspw. im Falle von Glassfish in einer Seite, die wie folgt aussieht:
image_exceptionHandling_screenshot_uncatchedException.png
Dieses Standardverhalten ist während der Entwicklung ganz nützlich, aber auf keinen Fall das Verhalten, das wir uns für produktive Systeme wünschen. Denn zum einen ist die angezeigte Seite ausgesprochen hässlich und bombardiert die Nutzerin der Seite mit Informationen, die sie in aller Regel nicht interessieren. Zum anderen könnten ungefilterte Fehlermeldungen Informationen enthalten, die Angreiferinnen Hinweise über das System liefern. In obigem Screenshot bspw., dass PostgreSQL als Datenbank eingesetzt wird und dass die Anwendung auf einem Glassfish-Applikationsserver läuft.1 Wäre jetzt bspw. für eines der beteiligten System eine Sicherheitslücke bekannt, so könnte man versuchen, über diese das System zu kompromittieren.
Daher: In Produktivsystemen (und dies gilt auch für Seiten, die sich nur an Entwicklerinnen richten) dürfen niemals die Standard-Fehlerseiten genutzt werden!
In den folgenden Abschnitten behandeln wir zunächst die Möglichkeiten, Fehlerseiten oder Fehlerhandler-Servlets anzugeben, bevor wir auf die Methoden des HttpServletResponse-Objekts zu sprechen kommen, mit denen wir explizit Status- oder Fehlerbedingungen entsprechend dem HTTP-Protokoll setzen können. Am Schluss dieses Kapitels geben wir kurz ein paar allgemeine Empfehlungen zum Umgang mit Fehlersituationen.

Zum SeitenanfangZum Seitenanfang

Fehlerseiten in den JSPs konfigurieren

JSPs selbst können auf verschiedene Weise zu Fehlersituationen führen. Zum einen kann die Syntax der Seite fehlerhaft sein, weswegen diese erst gar nicht übersetzt wird. Des Weiteren können Bedingungen verletzt sein, die benutzte Tags und JSP-Standardaktionen erwarten. In dem Fall tritt der Fehler zumeist nur in bestimmten Situationen (bspw. wenn ein Attribut null ist oder wenn es zu NumberFormatExceptions kommt) auf. Schlussendlich können Fehler in der Nutzung der Expression Language oder in Skriptlets zu Exceptions führen. In all diesen Fällen möchte man in Produktivsystemen der Nutzerin mitteilen, dass es zu einem Fehler gekommen ist.
Daher ist es möglich, in JSPs mittels einer Page-Direktive eine Fehlerseite anzugeben, die im Falle einer Exception aufgerufen wird. Die Syntax ist simpel, es reicht die folgende Zeile aus:

<%@ page errorPage="/WEB-INF/error.jsp" %>

Der Nachteil dieses Ansatzes ist, dass das angesprochene Benachrichtigen der Administratorinnen (und ggf. Entwicklerinnen) nur per Skriptlets (oder mittels eines speziellen Fehlerbehandlungs-Tags) möglich ist. Vor allem aber ist dieser Ansatz sehr statisch. So muss man die Fehlerseite in jede JSP einbauen, und die Gefahr ist groß, dass man dies bei neuen Seiten vergisst. Darüber hinaus kann man keine Fehlerseiten in Abhängigkeit von der konkreten Fehlersituation anzeigen. Ist in einem Reisebuchungssystem bspw. gerade der Webservice einer Fluglinie nicht erreichbar, so sollte man die Nutzerin darauf hinweisen, dass diese Verbindung nicht hergestellt werden kann. Der Rest der Anwendung kann in dem Fall weiterhin genutzt werden oder die geplante Buchung zur späteren Wiederaufnahme im Account der Nutzerin gespeichert werden. Ist hingegen die Verbindung zur Datenbank unterbrochen, sind die meisten Systeme schlichtweg nicht mehr funktionsfähig. Dann bleibt nur, der Nutzerin dies schonend beizubringen und zu hoffen, dass sie der Bitte, es später nochmal zu versuchen, auch wirklich folgt.
Natürlich kann man auch beide Fälle in einer Seite abhandeln, aber damit macht man die Seite größer, unübersichtlicher und komplexer als nötig. Der im folgenden Abschnitt vorgestellte Ansatz erlaubt ein wesentlich flexibleres Vorgehen.

Zum SeitenanfangZum Seitenanfang

Fehlerseiten per Deployment Deskriptor konfigurieren

Das Vorgehen, in den JSPs Fehlerseiten zu deklarieren, hat die schon dargestellten Nachteile, unflexibel zu sein und schnell vergessen zu werden. Hinzu kommt das Problem, dass bei Model 2-basierten Architekturen (s. dazu das Kapitel Anwendungsarchitektur) Fehler in Servlets oder im von Servlets aufgerufenem Code der Geschäftslogik auftreten können, bevor überhaupt unsere JSP angezogen wird. Da in diesem Fall die Direktive nicht zum Tragen kommt, würde die dort angegebene Fehlerseite auch nicht aufgerufen.
Es spricht also alles für eine zentrale Konfiguration des Fehlerverhaltens. Glücklicherweise sieht die Servlet-Spezifikation eine solche in der zentralen Konfigurationsdatei web.xml vor. Hierzu gibt es das error-page-Element innerhalb des einzigen Top-Level-Elementes <web-app>. Hier gibt man eine Fehlerbedingung an sowie die Seite, die aufgerufen werden soll, wenn die Fehlerbedingung zutrifft.
Bei den Fehlerbedingungen wird zwischen Exceptions und HTTP-Statuscodes unterschieden. Im ersten Fall wird ein voll spezifizierter Klassenname einer Exception-Klasse angegeben. Tritt diese Exception auf, wird zur Fehlerseite verzweigt. Da die service()-Methode des Interfaces javax.servlet.Servlet ebenso wie die doFilter()-Methode des javax.servlet.Filter lediglich java.io.IOException und javax.servlet.ServletException als werfbare Exceptions spezifizieren, können auch nur diese Exceptions oder RuntimeExceptions auftreten. Somit stellen lediglich diese Exception-Klassen sinnvolle Einträge im Deployment Deskriptor dar.
Die Angabe von Statuscodes als Fehlerbedingung macht ergänzend Sinn. Hier ist zu bedenken, dass bspw. bei Frameworks und zumeist auch bei selbst geschriebenen Servlets diese zumeist von javax.servlet.http.HttpServlet abgeleitet sind. Dieses Servlet beantwortet fehlerhafte Anfragen mit geeigneten Statuscodes. Vor allem aber reagiert der Servlet-Container auf etliche Fehlersituationen mit Statuscodes ungleich 200, bspw. bei der Anfrage nach einer nicht existenten Seite mit dem Statuscode 404.
Im folgenden zeigen wir beispielhaft für je eine Exception und einen Statuscode die entsprechende Konfiguration:

<!-- show a nicer Not Found-page -->
<error-page>
    <error-code>404</error-code>
    <location>/WEB-INF/jsp/jsp/errorHandler404.jsp</location>
</error-page>
<!-- ... -->
<!-- catch anything that is not already catched -->
<error-page>
    <exception-type>java.lang.Throwable</exception-type>
    <location>/WEB-INF/jsp/jsp/errorHandlerUnspecific.jsp</location>
</error-page>   

Zum SeitenanfangZum Seitenanfang

Vom Container bereitgestellte Informationen

Unabhängig davon, ob man die Weiterleitung an eine Fehlerseite im Deployment Deskriptor oder in der JSP per Page-Direktive definiert hat, stellt einem der Servlet-Container Informationen über das aufgetretene Problem zur Verfügung. Dazu werden vom Container die folgenden Attribute in den Request-Scope gesetzt (zu den Gültigkeitsbereichen siehe das Kapitel "Beans in JSPs"):

Attribut-KeyTyp des AttributsBedeutung des Attributs
javax.servlet.error.status_codejava.lang.IntegerDer Statuscode, aufgrund dessen zum Fehlerhandler verzweigt wurde (trifft zu bei StatusCode-Fehlerdeklarationen in der web.xml)
javax.servlet.error.exception_typejava.lang.ClassDas Class-Objekt der aufgetretenen Exception (im Falle einer StatusCode-Fehlerdeklaration ist der Wert null)
javax.servlet.error.messagejava.lang.StringDie Fehlermeldung
javax.servlet.error.exceptionjava.lang.ThrowableDie eigentliche Exception (im Faller einer StatusCode-Fehlerdeklaration ist der Wert null)
javax.servlet.error.request_urijava.lang.StringDie vom Client angeforderte URI
javax.servlet.error.servlet_namejava.lang.StringDas Servlet, das f�r die Beantwortung der angefordertetn URI zust�ndig ist


Im Falle von JSPs kann man am Einfachsten wie im folgenden Abschnitt beschrieben verfahren und das ErrorData-Objekt nutzen. Im Falle von Servlets kann man die obigen Attribute nutzen wie im Beispiel des ErrorHandlerServlets am Ende dieses Kapitels gezeigt.

Zum SeitenanfangZum Seitenanfang

JSPs für individuell gestaltete Fehlerseiten nutzen

Egal welche Methode man verwendet, am Ende wird eine Fehlerseite aufgerufen. Damit man Zugang zu dem Exception-Objekt hat, muss diese Seite die folgende Direktive enthalten:

<%@page isErrorPage="true" %>

Dadurch kann man in der JSP selber auf die ursächliche Exception und auf ein Objekt vom Typ javax.servlet.http.jsp.ErrorData zugreifen.
Das ErrorData-Objekt verfügt über die folgenden Methoden (und deckt sich damit weitgehend mit den oben aufgeführten Request-Attributen):

MethodeBedeutung
getRequestURI() Gibt die URI der Anfrage zurück, bei der die Ausnahme aufgetreten ist
getServletName() Gibt den Namen des aufgerufenen Servlets zurück
getStatusCode() Wenn durch einen Deployment Deskriptor-Eintrag Statuscodes auf Fehlerseiten gemappt werden und aufgrund dessen diese Fehlerseite angesprungen wurde, kann man hier den entsprechenden Statuscode auslesen
getThrowable() Hier erhält man die Angabe über den verursachenden Fehler - sofern die Seite nicht aufgrund eines Statuscode-Eintrags angesprungen wurde


Im folgenden zeigen wir eine mögliche Verwendung der Objekte in einer Fehlerseite:

<%-- using expression language --%>
Statuscode: ${pageContext.errorData.statusCode}
Request-URI: ${pageContext.errorData.requestURI}
Servletname: ${pageContext.errorData.servletName}
Exception: ${pageContext.errorData.throwable}

<%-- using scriptlet expressions --%>
Statuscode: <%= pageContext.getErrorData().getStatusCode() %>
Request-URI: <%= pageContext.getErrorData().getRequestURI() %>
Servletname: <%= pageContext.getErrorData().getServletName() %>
Exception: <%= exception %>

Zum SeitenanfangZum Seitenanfang

response.setStatus

Im Anhang I: HTTP-Grundlagen haben wir das Http-Protokoll beschrieben und erklärt, dass der Server jede Anfrage als erstes mit einer Statuszeile beantwortet, die neben einem kurzen Text vor allem den Status-Code enthält. Der bekannteste Statuscode ist der Code 404, weil man ihn des öfteren im Web antrifft - und früher häufig Seiten sah, die den Statuscode direkt mit ausgaben. Der verbreitetste ist freilich 200, der für den Erfolgsfall steht. Will man in seiner eigenen Anwendung einen spezifischen Statuscode setzen, bspw. weil eine GET-Methode verwandt wurde, aber nur POST genutzt werden soll (bspw. bei Uploads relevant), so kann man mit der Methode setStatus(int) des HttpServletResponse-Objekts gezielt einen Statuscode setzen, der zurückgegeben wird. Wichtig ist, dass zuvor noch keinerlei Content direkt an den Client weiter gereicht wurde, also der Puffer noch nicht durch ein Flush weggeschrieben wurde. Anderenfalls sind bereits sämtliche Response-Header gesetzt und können nicht mehr geändert werden. Der Status der Response kann jederzeit mit der Methode isCommitted() des HttpServletResponse-Objektes abgefragt werden.

Zum SeitenanfangZum Seitenanfang

response.sendError

Die Methode setStatus(int) setzt einen Status-Code, ermöglicht aber noch, eine normale Response auszugeben, Header zu setzen (bspw. notwendig, wenn man manuell Redirects setzt) und dergleichen.
Die Methode sendError(int) des HttpServletResponse-Objekts hingegen beendet den weiteren Ablauf. Sie funktioniert somit ähnlich, wie die obige Methode, allerdings wird durch Aufruf dieser Methode direkt der Statuscode gesetzt und gesendet. Nach Beendigung der Methode ist das HttpServletResponse-Objekt im Zustand committed und es darf in die Response nichts mehr geschrieben werden. Es existiert noch eine Variante, die als zweiten Parameter einen String erwartet. Mit diesem kann man den Text der Statuszeile der HTTP-Response festlegen. Ebenso wie bei der setStatus-Methode darf vor dem Aufruf der sendError-Methode noch kein Teil der Antwort an den Client geschrieben sein, der Status darf nicht "committed" sein!
Die Klasse javax.servlet.http.HttpServletResponse enthält eine Fülle von Konstanten für die verschiedenen möglichen HTTP-Statuscodes. Da diese Konstanten sprechender als die reinen Zahlen sind, sollte man in eigenem Interesse angesichts späterer Pflegearbeiten an der eigenen Anwendung immer die Konstanten nutzen.

Zum SeitenanfangZum Seitenanfang

Allgemeine Anregungen und Tipps

Zur Vorbereitung der eigentlichen Fehlerseite ist ein ErrorHandler-Servlet empfehlenswert. Dieses loggt die aufgetretene Exception und alle Ursachen, die dieser zugrunde liegen. Danach verzweigt das Servlet zu einer JSP mit einem geeigneten Hinweis für die Nutzerin.
Wie oben beschrieben, kann man Exceptions per Deployment Deskriptor Fehlerseiten (oder Servlets) zuordnen. Dabei empfiehlt es sich als Fallback-Exception den Typ java.lang.Throwable zu verwenden, wodurch sicher gestellt ist, dass nichts übersehen wurde. Dadurch wird zudem versucht, auf Fälle, in denen ein von java.lang.Error abgeleiteter Fehler geworfen wurde, angemessen zu reagieren.2 Etwaige auftretende Fehler auf der Fehlerbeseitigungsseite oder dem ErrorHandler-Servlet selbst sollten unbedingt abgefangen werden. Ansonsten bekommt die Nutzerin am Ende doch wieder den Stacktrace zu sehen, den man eigentlich verhindern wollte.
Fehler, die bis zur Servlet-Ebene gelangen oder dort auftreten, sollten idealerweise einen Automatismus anstoßen, der Administratorinnen und ggf. Entwicklerinnen der Anwendung über diese Fehlerfälle informiert. Hat man Angst, mit Exceptions bombardiert zu werden und dadurch die wirklich schwerwiegenden Fehler zu übersehen, dann hat man ein grundlegendes Problem mit dem Design der Anwendung. Exceptions sollten schließlich die Ausnahme sein. Dabei sollte man die Notification natürlich so gestalten, dass bspw. der Wegfall einer Ressource genau einmal mitgeteilt wird,. Ebenso ist es je nach Art der Anwendung hilfreich, automatisch mitzuteilen, dass die Ressource wieder vorhanden ist.
Während Exceptions zu einer Benachrichtigung führen sollten, gilt dies nicht für Fehlerbedingungen, die auf Statuscodes beruhen. Die meisten davon kommen in aller Regel vom Statuscode 404. Weiterhin häufig sind Statuscodes, die der Browser direkt verarbeiten kann (bspw. bei Redirects die Statuscodes 301 und 302). Uns interessiert nicht, wenn eine Nutzerin eine falsche Adresse eingetippt hat oder versucht, sich ein von uns unterbundenes Directory-Listing anzeigen zu lassen. Auch wissen wir ohnehin, wo wir Redirects in der Anwendung verwenden und würden in einer Benachrichtigungsflut untergehen, würden wir darüber jedes mal eine Warnung erhalten. Allerdings sollte man in den HTTP-Access-Logfiles des Servers darauf achten, ob einzelne Adressen zu unerwartet häufigen 404-Fehlern führen. Dies könnte ein Hinweis auf einen falschen Link sein (durch einen selbst auf der eigenen Seite oder durch - möglicherweise veraltete - Links von externen Seiten). In dem Fall sollte man aktiv werden und seine eigenen Links überarbeiten bzw. die Betreiberinnen der externen Seiten um eine Korrektur der Links bitten.
Wenn man Exceptions fängt und aufgrund dessen neue Exceptions generieren muss, empfiehlt es sich, immer die ursächliche Exception beim Erzeugen mitzugeben. Nur so kann man sicher stellen, dass wichtige Informationen über die eigentliche Ursache des Problems nicht verloren gehen. Legt man eine eigene Exception-Hierarchie an, so sollte man daher immer überladene Konstruktoren anbieten, die von java.lang.Throwable abgeleitete Klassen entgegen nehmen und die entsprechenden Super-Konstruktoren von java.lang.Exception aufrufen. Dadurch wird es möglich, wie im ErrorHandlerServlet weiter unten gezeigt, über alle Ursachen zu iterieren und die Stacktraces zu loggen.
Das Fangen von Exception und Weiterleiten in eigenen spezifischen Fehlerklassen macht nur dort Sinn, wo nach außen hin keine Exceptions tieferer Schichten sichtbar sein sollen. Die Java Persistence API-Klassen bspw. werfen keine SQLExceptions sondern eigene von java.lang.RuntimeException abgeleitete Exceptions. IllegalArgumentExceptions oder ArrayIndexOutOfBoundsExceptions abzufangen und wieder in eigene Exceptions einzupacken, wäre hingegen wenig sinnvoll. Diese deuten ohnehin auf Programmierfehler hin und sollten erst auf der Präsentationsschicht zur Anzeige geeigneter Fehlermeldungen gefangen werden.
Generell sollte das ExceptionHandling möglichst zentral vorgenommen werden. Übermäßig häufiges Fangen und das Neuerzeugen eigener Exceptions, die dann weiter geworfen werden, ist zeitaufwändig (das Erzeugen eines Stacktraces ist nicht gerade die kostengünstigste Operation) und führt zu einem schlecht lesbaren Code. Daher sollten Exceptions, so denn es sich nicht um Situationen handelt, auf die vom Programm aus angemessen reagiert werden kann, möglichst bis zu den Grenzen der Komponenten und Schichten weiter gereicht werden.
Diskussionswürdig ist, wie weit diese Zentralisierung gehen soll. Sollen Fehler der Geschäftslogik einfach weiter gereicht werden oder nicht? Als Daumenregel empfehle ich hier, alle Fehler in RuntimeExceptions weiter zureichen, sofern man in der jeweiligen Schicht nicht sinnvoll darauf reagieren kann. Eine javax.persistence.OptimisticLockException kann durch das erneute Absetzen einer Transaktion meist erfolgreich behoben werden. Eine java.net.ConnectException könnte bei mehreren Backup-Systemen evtl. erfolgreich durch die Auswahl eines neuen Kommunikationsendpunktes behoben werden. Stehen solche nicht zur Verfügung oder treten erneut Exceptions auf, sollte man diese Exceptions weiter reichen. Idealerweise in einer RuntimeException verpackt, damit keine unnötigen Abhängigkeiten vom Package java.net entstehen.
Beim Fangen und Weiterwerfen von Exceptions empfiehlt es sich, den Stacktrace nur am Ende der Kette auszugeben und nicht auf jeder Ebene. Ansonsten ist das Log voller Exceptions, und man macht sich die Analyse der Ursache unnötig schwer. Auch hier zeigt es sich, dass möglichst zentralisiertes ErrorHandling von Vorteil ist und einem hilft, an dieser Stelle sauber zu entwickeln.
Einige weitere nützliche Hinweise zum Thema ExceptionHandling gibt es in den englischsprachigen Artikeln von TimMcCune, Jim Cushing und Bob Lee der auch ansonsten empfehlenswerten Seite java.net. Der Artikel "Effective Java Exceptions" von Barry Ruzek führt mit seiner Unterscheidung von "Faults" (Fehler) und "Contingencies" (Möglichkeit) zudem eine nützliche Kategorisierung von Exceptions ein.
Wie versprochen zum Abschluss ein verkürztes Beispiel für ein ErrorHandlerServlet:

package org.jsptutorial.examples.servlets;

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

/**
 * This servlet handles requests which were forwarded by the servlet 
 * container based on exception handling entries in the deployment 
 * descriptor. This servlet is for exceptions only and not for 
 * status-code-based error-pages.
 */
public class ErrorHandlerServlet extends HttpServlet {
   
   /** The logger. We use commons-logging of http://jakarta.apache.org/commons */
   Log logger = LogFactory.getLog(ErrorHandlerServlet.class);

   public void doPost(HttpServletRequest request, HttpServletResponse response) {
      doGet(request, response);
   }

   public void doGet(HttpServletRequest request, HttpServletResponse response) {
      try {
         Throwable t = (Throwable)request.getAttribute("javax.servlet.error.exception");
         if (t == null) {
            // servlet has not been called as an error handler
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
         }
         String message = (String)request.getAttribute("javax.servlet.error.message");
         String uri = (String)request.getAttribute("javax.servlet.error.request_uri");
         String servletName = (String)request.getAttribute("javax.servlet.error.servlet_name");
         
         // you might want to add some more information - e.g. request parameters
         String completeMessage = "Problem encountered while serving " + uri +
                 "\nservlet involved: " + servletName +
                 "\nmessage: " + message +
                 "\n" +
                 getCompleteStack(t);
         
         // log problem; you could also trigger some kind of notification mechanism
         logger.error(completeMessage);
         
         // finally forward to customer friendly error page
         if (!response.isCommitted()) {
            RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/errorPage.jsp");
            dispatcher.forward(request, response);
         }
         else {
            // most probably a buffer is not big enough; try to adjust it...
            // you should put some kind of notification in here, so that you
            // know about having to adjust the buffer
         }
      } catch (Exception ex) {
         // DO NOT SWALLOW! If the above code causes any exception to be thrown
         // it is a hint that s.th. went seriously awry; most probably 
         // caused by io problems! 
      }
   }
     
   private String getCompleteStack(Throwable t) {
      StringWriter writer = new StringWriter();
      PrintWriter out = new PrintWriter(writer);
      while (t != null) {
         t.printStackTrace(out);
         if (t instanceof ServletException) {
            t = ((ServletException)t).getRootCause();
         }
         else {
            t = t.getCause();
         }
         if (t != null) {
            out.print("Caused by: ");
         }
      }
      out.flush();
      out.close();
      return writer.toString();
   }
}

Zum SeitenanfangZum Seitenanfang

Anmerkungen:

1) Glassfish gibt sich standardmäßig als Sun Java System Application Server zu erkennen. (zurück)

2) Das wird aufgrund der schweren Bedingungen, die einem Error zugrunde liegen, allerdings häufig nicht funktionieren - immerhin bekommt man aber den OutOfMemoryError auf diese Weise gepackt. (zurück)


www.jsptutorial.org
© 2005, 2006, 2007