Kapitel 3 Servlet-Grundlagen Kapitel 78 3 Wie bereits in Kapitel 1 erwähnt wurde, sind Servlets Java-Programme, die auf einem Web- oder Anwendungsserver ausgeführt werden. Sie fungieren als Zwischenebene zwischen den Anfragen, die von den Webbrowsern oder anderen HTTP-Clients kommen, und den Datenbanken oder Anwendungen auf dem HTTP-Server, siehe Abbildung 3.1. Datenbank Legacy-Anwendung Java-Anwendung Client (Endanwender) Webserver (Servlets/JSP) Webdienst Abbildung 3.1: Die Aufgabe der Web-Zwischenebene (Middleware) 1. Die vom Client gesendeten, expliziten Daten lesen. Diese Daten werden normalerweise vom Benutzer in ein HTML-Formular auf einer Webseite eingegeben. Sie können aber auch von einem Applet oder einem selbst geschriebenen HTTP-Clientprogramm kommen. 2. Die vom Browser implizit mit der HTTP-Anfrage gesendeten Daten lesen. In Abbildung 3.1 sehen Sie nur einen Pfeil vom Client zum Webserver (der Ebene, auf der Servlets und JSP ausgeführt werden). Genau genommen gibt es aber zwei Arten von Daten: die expliziten Daten, die der Benutzer in ein Formular eingibt, und die impliziten oder auch verborgenen HTTP-Informationen. Beide Arten sind für die effektive Entwicklung wichtig. Zu den HTTP-Informationen gehören Cookies, Informationen zu den vom Browser unterstützten Medientypen und Komprimierungsformaten, und so weiter. Näheres hierzu erfahren Sie in Kapitel 5. 3. Ergebnisse generieren. Hierzu kann es erforderlich sein, mit einer Datenbank zu kommunizieren, einen RMI- oder CORBA-Aufruf zu tätigen, einen Webdienst aufzurufen oder die Antwort direkt zu berechnen. Unter Umständen stehen Ihre Daten in einer relationalen Datenbank. Gut. Doch Ihre Datenbank spricht wahrscheinlich kein HTTP und gibt auch keine Ergebnisse in HTML zurück, sodass der Webbrowser nicht direkt mit der Datenbank kommunizieren kann. Doch selbst wenn dies möglich wäre, würden Sie dies aus Sicherheitsgründen wahrscheinlich ablehnen. Das gleiche Argument trifft auch für die meisten anderen Anwendungen zu. Deshalb wird eine Zwischenebene im Webserver benötigt, die die eingehenden Daten aus dem HTTP-Stream herauszieht, mit der Anwendung kommuniziert und die Ergebnisse in ein Dokument einbettet. 4. Die konkreten Daten (d.h. das Dokument) an den Client senden. Das Dokument kann in verschiedenen Formaten gesendet werden, unter anderem als Text (HTML oder XML), binär (GIF-Bilder) oder sogar in einem komprimierten Format wie gzip, das irgend einem zugrunde liegenden Format übergestülpt wurde. 5. Die impliziten HTTP-Antwortdaten senden. In Abbildung 3.1 weist genau ein Pfeil von der Zwischenebene des Webservers (dem Servlet oder der JSP-Seite) auf den Client. Genau genommen gibt es aber zwei Arten von gesendeten Daten: das Dokument selbst und die impliziten beziehungsweise verborgenen HTTP-Informationen. Und auch hier gilt, dass beide Arten für die effektive Entwicklung wichtig sind. Anhand der gesendeten HTTP-Antwortparameter wird dem Client (z.B. Browser) mit- Servlet-Grundlagen 79 geteilt, welche Art Dokument zurückgeliefert wird (z.B. HTML), werden Cookies und CachingParameter gesetzt und ähnliche Aufgaben ausgeführt. Eine ausführliche Beschreibung dieser Aufgaben finden Sie in den Kapiteln 6 und 7. Prinzipiell sind Servlets nicht auf Web- oder Anwendungsserver zur Behandlung von HTTP-Anfragen beschränkt, sondern eignen sich auch für andere Typen von Servern. So könnte man z.B. Servlets in Mail- oder FTP-Server einbetten, um deren Funktionalität zu erweitern. In der Praxis haben sich diese Einsatzmöglichkeiten für Servlets jedoch noch nicht durchgesetzt, sodass hier ausschließlich auf HTTPServlets eingegangen werden soll. 3.1 Grundstruktur von Servlets Listing 3.1 skizziert ein einfaches Servlet zur Behandlung von GET-Anfragen. Für Leser, die mit HTTP nicht so vertraut sind: GET-Anfragen sind der übliche Anfragetyp, den Browser zum Abrufen von Webseiten verwenden. Ein Browser generiert diese Anfragen, wenn der Benutzer einen URL in die Adresszeile eintippt, einen Link auf einer Webseite anklickt oder ein HTML-Formular abschickt, das kein METHOD-Attribut oder METHOD="GET" spezifiziert. Servlets können aber auch POST-Anfragen handhaben, die erzeugt werden, wenn jemand ein HTML-Formular abschickt, in dem METHOD="POST" spezifiziert ist. Einzelheiten zur Verwendung von HTML-Formularen und den Unterschied zwischen GET und POST finden Sie in Kapitel 19. Listing 3.1: ServletTemplate.java import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ServletTemplate extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Verwenden Sie "request", um eingehende HTTP-Header // (z.B. cookies) und HTML-Formulardaten zu lesen. // Verwenden Sie "response", um den HTTP// Antwortstatuscode und die Antwort-Header anzugeben // (z.B. Inhaltstyp, Cookies). PrintWriter out = response.getWriter(); // Verwenden Sie "out", um den Inhalt an den Browser zu // senden. } } Servlets werden normalerweise von HttpServlet abgeleitet und überschreiben doGet beziehungsweise doPost, je nachdem, ob die Daten mit GET oder POST übermittelt werden. Wenn Sie sowohl GET als auch POST von demselben Servlet mit derselben Aktion behandeln lassen möchten, können Sie einfach doGet veranlassen, doPost aufzurufen oder umgekehrt. Kapitel 80 3 Beide Methoden nehmen zwei Parameter entgegen: HttpServletRequest und HttpServletResponse. Mit HttpServletRequest haben Sie Zugriff auf alle eingehenden Daten. Die Klasse verfügt über Methoden, mit denen Sie feststellen können, ob Informationen wie z.B. Formulardaten (Anfrage), HTTP-Anfrage-Header oder der Hostname des Clients eingehen. Mit HttpServletResponse können Sie ausgehende Informationen, wie HTTP-Statuscode (200, 404 etc.) und Antwort-Header (Content-Type, Set-Cookie etc.), spezifizieren. Was jedoch am wichtigsten ist: HttpServletResponse verschafft Ihnen einen PrintWriter, mit dem Sie den Dokumentinhalt an den Client zurücksenden können. Einfache Servlets verwenden die meiste Mühe auf println-Anweisungen, die die gewünschte Seite generieren. Formulardaten, HTTP-AnfrageHeader, HTTP-Antworten und Cookies werden in den nächsten Kapiteln detailliert behandelt. Da doGet und doPost zwei Ausnahmen (ServletException und IOException) auslösen, müssen Sie diese in die Methodendeklaration aufnehmen. Zum Schluss müssen Sie noch die Klassen aus java.io (für PrintWriter etc.), javax.servlet (für HttpServlet etc.) und javax.servlet.http (für HttpServletRequest und HttpServletResponse) importieren. Es besteht jedoch keine Notwendigkeit, sich die Methodensignatur und die Importanweisungen zu merken. Laden Sie einfach die obige Vorlage von der Buch-CD oder aus dem Quellcode-Archiv unter http:/ /www.coreservlets.com/ herunter und verwenden Sie diese als Ausgangsbasis für Ihre Servlets. 3.2 Ein Servlet, das einfachen Text generiert Listing 3.2 zeigt ein einfaches Servlet, das normalen Text generiert. Die Ausgabe des Servlets ist in Abbildung 3.2 zu sehen. Bevor wir jetzt weitermachen, wollen wir uns jedoch etwas Zeit nehmen und die Installation, Kompilierung und Ausführung dieses einfachen Servlets durchspielen. (Siehe auch Kapitel 2 finden für ausführliche Hinweise zur Installation.) Als Erstes müssen Sie sicherstellen: • dass Ihr Server wie in Kapitel 2.3 beschrieben konfiguriert ist. • dass Ihr Entwicklungs-CLASSPATH wie in Kapitel 2.7 beschrieben auf die notwendigen drei Einträge verweist: die Servlet-JAR-Datei, Ihr oberstes Entwicklungsverzeichnis, und ».«. • dass alle Tests aus Kapitel 2.8 erfolgreich durchgeführt werden können. Zweitens: Geben Sie javac HelloWorld.java ein oder teilen Sie Ihrer Entwicklungsumgebung mit, das Servlet zu kompilieren (z.B. durch Anklicken von BUILD in Ihrer IDE oder durch Auswählen des COMPILE-Befehls aus dem emacs JDE-Menü). Damit kompilieren Sie Ihr Servlet in eine Datei namens HelloWorld.class. Drittens: Verschieben Sie HelloWorld.class in das Verzeichnis, in dem Ihr Server Servlets speichert, die in die Standardwebanwendung gehören. Die genaue Position hängt vom Server ab, lautet aber in der Regel install_dir/.../WEB-INF/classes (siehe Kapitel 2.10 für Einzelheiten). Für Tomcat verwenden Sie zum Beispiel install_dir/webapps/ROOT/WEB-INF/classes, für JRun install_dir/servers/default/defaultear/default-war/WEB-INF/classes und für Resin install_dir/doc/WEB-INF/classes. Alternativ können Sie aber auch eine der in Kapitel 2.9 vorgestellten Techniken einsetzen, um die Klassendateien automatisch an die gewünschte Position zu verschieben. Servlet-Grundlagen 81 Zum Schluss rufen Sie das Servlet auf. Hier kommt entweder der Standard-URL http://host/servlet/ServletName oder ein eigener, in der web.xml-Datei definierter URL ins Spiel. Eine genaue Beschreibung finden Sie in Kapitel 2.11. Zu Beginn eines Projekts werden Sie es sicherlich bequemer finden, mit dem Standard-URL zu arbeiten, sodass Sie die web.xml-Datei nicht jedes Mal beim Testen eines neuen Servlets bearbeiten müssen. Wenn Sie allerdings konkrete Anwendungen installieren, werden Sie fast immer den Standard-URL deaktivieren und in der web.xml-Datei explizite URLs zuweisen (siehe Kapitel 2.11). Im Übrigen unterstützen nicht alle Server die Verwendung eines Standard-URLs, BEA WebLogic verzichtet beispielsweise darauf. Abbildung 3.2 zeigt ein Servlet, auf das über den Standard-URL zugegriffen wird, wobei der Server auf der lokalen Maschine läuft. Listing 3.2: HelloWorld.java import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class HelloWorld extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { PrintWriter out = response.getWriter(); out.println("Hello World"); } } Abbildung 3.2: Ausgabe von http://localhost/servlet/HelloWorld. 3.3 Ein Servlet, das HTML generiert Die meisten Servlets generieren HTML und keinen einfachen Text wie im vorigen Beispiel. Um HTML zu erstellen, sind drei zusätzliche Schritte erforderlich: 1. Sie teilen dem Browser mit, dass sie ihm HTML zuschicken. 2. Sie modifizieren die println-Anweisungen so, dass sie eine gültige Webseite aufbauen. 3. Sie prüfen mithilfe eines HTML-Validierers, ob Ihre HTML-Dokumente formale Syntaxfehler enthalten. 82 Kapitel 3 Für Punkt 1 setzen Sie den HTTP-Antwort-Header Content-Type auf text/html. Im Allgemeinen setzt die setHeader-Methode von HttpServletResponse die Header, aber es ist eine so häufige Aufgabe, den Inhaltstyp zu setzen, dass es hierfür eine spezielle setContentType-Methode gibt. Am besten kennzeichnet man HTML durch den Typ text/html, sodass der Code folgendermaßen aussieht: response.setContentType("text/html"); HTML ist zwar der gebräuchlichste Dokumenttyp, den Servlets erstellen, aber es ist nicht unüblich, auch andere Dokumenttypen zu erzeugen. So kommt es z.B. recht häufig vor, dass man mit Servlets ExcelTabellen (Inhaltstyp application/vnd.ms-excel – siehe Kapitel 7.3), JPEG-Bilder (Inhaltstyp image/jpeg – siehe Kapitel 7.5) und XML-Dokumente (Inhaltstyp text/xml) erzeugt. Außerdem verwenden Sie Servlets nur selten dazu, HTML-Seiten zu erzeugen, die ein relativ festes Format aufweisen (d.h. deren Layout sich bei jeder Anfrage nur wenig ändert). Für solche Fälle ist JSP normalerweise die bessere Wahl. JSP wird in Teil II dieses Buches, der mit Kapitel 10 beginnt, behandelt. Keine Sorge, wenn Sie mit HTTP-Antwort-Headern noch nicht vertraut sind; sie werden in Kapitel 7 noch detailliert behandelt. Beachten Sie, dass Sie die Antwort-Header setzen, bevor Sie irgendwelche Inhalte über den PrintWriter zurückgeben. HTTP-Antworten bestehen nämlich aus einer Statuszeile, einem oder mehreren Headern, einer leeren Zeile und dem eigentlichen Dokument. und zwar in genau dieser Reihenfolge. Da die Header in jeder beliebigen Reihenfolge stehen können und Servlets die Header im Puffer speichern, bis sie alle auf einmal gesendet werden, ist es zulässig, den Statuscode (ein Teil der ersten zurückgegebenen Zeile) auch noch nach den Headern zu setzen. Servlets speichern aber nicht notwendigerweise das gesamte Dokument, da die Benutzer unter Umständen für lange Seiten auch Teilresultate sehen möchten. Servlet-Engines dürfen die Ausgabe zum Teil im Puffer speichern, aber die Größe dieses Puffers ist nicht angegeben. Sie können sie mit der HttpServletResponse-Methode getBufferSize ermitteln oder mit setBufferSize einstellen. Sie können so lange Header setzen, bis der Puffer voll ist und tatsächlich an den Client gesendet wird. Wenn Sie unsicher sind, ob der Puffer bereits gesendet wurde, können Sie dies mit der Methode isCommitted nachprüfen. Trotz allem ist es am besten, wenn Sie einfach die Zeile setContentType vor allen anderen Zeilen, die PrintWriter verwenden, stellen. Warnung Setzen Sie den Inhaltstyp immer, bevor Sie das eigentliche Dokument übermitteln. Der zweite Schritt besteht darin, println-Anweisungen aufzusetzen, die HTML statt eines einfachen Textes ausgeben. Listing 3.3 zeigt die Datei HelloServlet.java, das Beispiel-Servlet aus Kapitel 2.8, mit dem getestet wurde, ob der Server ordnungsgemäß funktioniert. Wie Abbildung 3.3 belegt, formatiert der Browser das Ergebnis als HTML und nicht als einfachen Text. Listing 3.3: HelloServlet.java import java.io.*; import javax.servlet.*; import javax.servlet.http.*; Servlet-Grundlagen 83 /** Einfaches Servlet zum Testen des Servers. */ public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); String docType = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 " + "Transitional//EN\">\n"; out.println(docType + "<HTML>\n" + "<HEAD><TITLE>Hello</TITLE></HEAD>\n" + "<BODY BGCOLOR=\"#FDF5E6\">\n" + "<H1>Hello</H1>\n" + "</BODY></HTML>"); } } Abbildung 3.3: Ausgabe von http://localhost/servlet/HelloServlet. Der letzte Schritt besteht darin, sicherzustellen, dass Ihr HTML keine Syntaxfehler aufweist, die zu unvorhersehbaren Ergebnissen auf den verschiedenen Browsern führen könnten. In Abschnitt 3.5 werden HTML-Validierer besprochen. 3.4 Servlet-Pakete In einer Produktionsumgebung erstellen eventuell mehrere Programmierer Servlets für denselben Server. Wenn man alle diese Servlets in dasselbe Verzeichnis legen würde, ließe sich die unüberschaubare Sammlung von Klassen kaum noch verwalten und es käme zu Namenskonflikten, wenn zufällig zwei Entwickler den gleichen Namen für Servlets oder Hilfsklassen verwenden. Mit Hilfe von Webanwendungen (siehe Kapitel 2.11) lässt sich dieses Problem einfach lösen, indem Sie alles auf getrennte Unterverzeichnisse, jedes mit seinem eigenen Satz an Servlets, Hilfsklassen, JSP-Seiten und HTML-Dateien verteilen. Da jedoch auch einzelne Webanwendungen recht umfangreich werden können, benötigen Sie trotzdem noch Javas Standardlösung zur Vermeidung von Namenskonflikten: die Pakete. Darüber hinaus müssen, wie Sie später noch sehen werden, selbst definierte Klassen, die von JSP-Seiten verwendet werden, immer in Paketen abgelegt werden. Gewöhnen Sie sich also möglichst früh an den Gebrauch von Paketen. 84 Kapitel 3 Um Servlets in Pakete zu packen, sind zwei Schritte erforderlich: 1. Sie verschieben die Dateien in ein Unterverzeichnis, das mit dem gewünschten Paketnamen übereinstimmt. Wir werden z.B. für die meisten Servlets, die in diesem Buch noch vorkommen, das Paket coreservlets verwenden. Die Klassendateien gehören folglich in ein Unterverzeichnis mit dem Namen coreservlets. Denken Sie an die Groß- und Kleinschreibung. Sie ist sowohl für Paket- als auch für Verzeichnisnamen wichtig, unabhängig davon, welches Betriebssystem Sie verwenden. 2. Sie fügen eine package-Anweisung in die Klassendatei ein. Damit z.B. eine Klasse einem Paket namens einPaket angehört, muss die Klassendatei in dem Verzeichnis einPaket liegen und die erste Zeile der Datei, die kein Kommentar ist, muss lauten: package einPaket; Listing 3.4 definiert z.B. eine Variante der Klasse HelloServlet, die sich im Paket coreservlets und damit im Verzeichnis coreservlets befindet. Wie bereits in Kapitel 2.8 besprochen wurde, sollte die Klassendatei für Tomcat in install_dir/webapps/ROOT/WEB-INF/classes/coreservlets, für JRun in install_dir/servers/default/default-ear/default-war/WEB-INF/classes/coreservlets und für Resin in install_dir/doc/WEB-INF/classes/coreservlets stehen. Andere Server verfügen über ähnlich lautende Standardverzeichnisse. Abbildung 3.4 zeigt das Servlet, auf das mittels des Standard-URLs zugegriffen wird. Listing 3.4: coreservlets/HelloServlet2.java package coreservlets; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /** Einfaches Servlet zum Testen von Paketen. */ public class HelloServlet2 extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); String docType = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 " + "Transitional//EN\">\n"; out.println(docType + "<HTML>\n" + "<HEAD><TITLE>Hello (2)</TITLE></HEAD>\n" + "<BODY BGCOLOR=\"#FDF5E6\">\n" + "<H1>Hello (2)</H1>\n" + "</BODY></HTML>"); } } Servlet-Grundlagen 85 Abbildung 3.4: Ausgabe von http://localhost/servlet/coreservlets.HelloServlet2. 3.5 Einfache Hilfsklassen zum Erstellen von HTML Wie Sie wahrscheinlich bereits wissen, besitzen HTML-Dokumente folgenden Aufbau: <!DOCTYPE ...> <HTML> <HEAD><TITLE>...</TITLE>...</HEAD> <BODY ...>...</BODY> </HTML> Wenn Sie jetzt beginnen, HTML mit Servlets erzeugen, sind Sie vielleicht in Versuchung, Teile dieser Grundstruktur – insbesondere die DOCTYPE-Zeile – wegzulassen, da diese Zeile, obwohl von der HTMLSpezifikation vorgeschrieben, von fast allen wichtigen Browsern ignoriert wird. Davon können wir nur entschieden abraten. Die DOCTYPE-Zeile wird benötigt, um HTML-Validierern die von Ihnen benutzte HTML-Version mitzuteilen. So wissen die Validierer, anhand welcher Spezifikation sie Ihr Dokument prüfen müssen. Validierer sind wertvolle Debug-Programme, die Ihnen helfen, Syntaxfehler aufzuspüren, die Ihr eigener Browser womöglich kompensiert, die anderen Browsern aber Schwierigkeiten bereiten können. Die beiden am weitesten verbreiteten Online-Validierer sind die Validierer des World Wide Web Consortiums (http://validator.w3.org) und der Web Design Group (http://www.htmlhelp.com/tools/ validator/). Diesen können Sie einen URL senden, damit sie die Seite aufrufen, die Syntax an Hand der formalen HTML-Spezifikation überprüfen und Ihnen die Fehler zurückmelden. Da ein Servlet, das HTML generiert, für den Client wie eine normale Webseite aussieht, kann es in der gleichen Weise validiert werden – solange es nicht auf POST-Daten angewiesen ist. Anders Servlets, die GET-Daten verarbeiten. Da GET-Daten an den URL angehängt werden, können Sie den URL mit den GET-Daten an den Validierer übergeben. Wenn das Servlet nur innerhalb Ihre Corporate Firewall verfügbar ist, führen Sie es einfach aus, sichern Sie den erzeugten HTML-Code auf Platte und wählen Sie die Validierer-Option FILE UPLOAD. Hinweis Prüfen Sie die Syntax der von Ihren Servlets generierten Seiten mit einem HTML-Validierer. 86 Kapitel 3 Zugegebenermaßen ist es manchmal ein bisschen umständlich, HTML mit println-Anweisungen zu generieren, besonders bei so langen Zeilen wie der DOCTYPE-Deklaration. Manche Entwickler lösen dieses Problem, indem sie in Java umfangreiche Hilfsprogramme schreiben, die HTML generieren und die sie dann in ihren Servlets einsetzen. Uns scheint die Nützlichkeit einer solchen Programmbibliothek jedoch zweifelhaft. Zum einem ist die umständliche HTML-Generierung mittels Programmen ja gerade eines der Hauptprobleme zu deren Lösung die JavaServer Pages entwickelt wurden (siehe Kapitel 10). Zweitens sind Routinen zum Generieren von HTML oft sperrig und unterstützen selten alle HTML-Attribute (CLASS und ID für Stylesheets, JavaScript-Ereignisbehandungscode, Hintergrundfarben für Tabellenzellen und so weiter). Trotz des fragwürdigen Werts einer voll ausgereiften Bibliothek zur Erzeugung von HTML-Code spricht nichts gegen die Implementierung einzelner Hilfsklassen: Wenn Sie beispielsweise feststellen, dass Sie immer wieder dieselben Konstrukte ausgeben, können Sie hierfür ebenso gut eine einfache Hilfsklasse schreiben. Schließlich arbeiten Sie mit der Programmiersprache Java; denken Sie also die an das Grundprinzip der objektorientierten Programmierung, das Code wieder verwendet und nicht wiederholt werden sollte. Die Wiederholung identischen oder beinahe identischen Codes bedeutet, dass Sie den Code an vielen Stellen ändern müssen, wenn Sie später Ihren Ansatz ändern. Zwei Teile der von normalen Servlets erzeugten Webseiten ändern sich in der Regel kaum: DOCTYPE und HEAD. Es bietet sich daher an, für diese eine einfache Hilfsprogrammdatei zu schreiben. Den Quelltext dieser Datei sehen Sie in Listing 3.5. Eine Variante der Klasse HelloServlet, die diese Hilfsklasse nutzt, finden Sie in Listing 3.6. Im Laufe des Buchs werden wir noch einige weitere Hilfsklassen erstellen. Listing 3.5: coreservlets/ServletUtilities.java package coreservlets; import javax.servlet.*; import javax.servlet.http.*; /** Einige einfache Konstrukte, zumeist statische Methoden, die Ihnen Zeit sparen können. */ public class ServletUtilities { public static final String DOCTYPE = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 " + "Transitional//EN\">"; public static String headWithTitle(String title) { return(DOCTYPE + "\n" + "<HTML>\n" + "<HEAD><TITLE>" + title + "</TITLE></HEAD>\n"); } ... } Servlet-Grundlagen 87 Listing 3.6: coreservlets/HelloServlet3.java package coreservlets; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /** Einfaches Servlet, mit dem der Einsatz von * Paketen und Hilfsklassen desselben Pakets * getestet werden kann. */ public class HelloServlet3 extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); String title = "Hello (3)"; out.println(ServletUtilities.headWithTitle(title) + "<BODY BGCOLOR=\"#FDF5E6\">\n" + "<H1>" + title + "</H1>\n" + "</BODY></HTML>"); } } Nachdem Sie HelloServlet3.java kompiliert haben (wodurch ServletUtilities.java automatisch mitkompiliert wird), müssen Sie die zwei Klassendateien in das Unterverzeichnis coreservlets des verwendeten Deployment-Verzeichnisses des Servers (.../WEB-INF/classes; siehe Kapitel 2.8 für nähere Einzelheiten) verschieben. Wenn Sie beim Kompilieren von HelloServlet3.java eine »Unresolved Error«-Fehlermeldung erhalten, prüfen Sie noch einmal Ihre CLASSPATH-Einstellungen (siehe Kapitel 2.7 ), besonders die Angabe des obersten Entwicklungsverzeichnis. In Abbildung 3.5 sehen Sie das Ergebnis, wenn das Servlet mit dem Standard-URL aufgerufen wird. Abbildung 3.5: Ausgabe von http://localhost/servlet/coreservlets.HelloServlet3. Kapitel 88 3.6 3 Lebenszyklus von Servlets In Kapitel 1.4 wurde bereits angesprochen, dass immer nur eine einzige Instanz eines Servlets erzeugt wird und für jede Benutzeranfrage ein neuer Thread gestartet wird, der je nach Anfragetyp doGet oder doPost ausführt. Nun soll etwas genauer erläutert werden, wie Servlets erzeugt und aufgelöst werden, und wie und wann die verschiedenen Methoden aufgerufen werden. Wir beginnen mit einer kurzen Zusammenfassung, in den nächsten Unterabschnitten folgen dann die Einzelheiten. Wenn ein Servlet erzeugt wird, wird seine init-Methode aufgerufen. In diese schreiben Sie folglich den Setup-Code, der nur einmal ausgeführt werden soll. Danach erzeugt jede Benutzeranfrage einen Thread, der die service-Methode der zuvor erzeugten Instanz aufruft. Mehrere, zeitgleiche Anfragen erzeugen in der Regel mehrere Threads, die service parallel aufrufen (Ihr Servlet kann aber auch eine spezielle Schnittstelle (SingleThreadModel) implementieren, die festlegt, dass jeweils nur ein einziger Thread gleichzeitig ausgeführt werden darf). Danach ruft die service-Methode je nach der der von ihr empfangenen HTTPAnfrage doGet, doPost oder eine andere doXxx-Methode auf. Wenn schließlich der Server beschließt, ein Servlet aus dem Speicher zu entfernen, ruft er als Erstes die destroy-Methode dieses Servlets auf. 3.6.1 Die service-Methode Immer wenn der Server die Anfrage nach einem Servlet empfängt, erzeugt er einen neuen Thread und ruft service auf. Die Methode service stellt den HTTP-Anfragetyp fest (GET, POST, PUT, DELETE etc.) und ruft entsprechend doGet, doPost, doPut, doDelete etc. auf. GET-Anfragen werden gesendet, wenn ein normaler URL angefordert oder ein HTML-Formular, für das kein METHOD-Attribut spezifiziert wurde, abgeschickt wird. POST-Anfragen werden für HTML-Formulare erzeugt, die als METHOD explizit POST angeben. Andere HTTP-Anfragen werden nur von selbst definierten Clients erzeugt. Wenn Sie mit HTML-Formularen noch nicht so vertraut sind, lesen Sie Kapitel 19. Für Servlet, die POST- und GET-Anfragen identisch behandeln, könnten Sie vielleicht in Versuchung geraten, service direkt zu überschreiben, anstatt doGet und doPost zu implementieren. Das ist keine gute Idee. Sorgen Sie stattdessen dafür, dass doPost die Methode doGet aufruft (oder umgekehrt): public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Servlet-Code } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } Dieser Ansatz erfordert zwar ein paar Codezeilen mehr, aber er hat gegenüber dem Überschreiben von service mehrere Vorteile. Erstens können Sie später noch Unterstützung für andere HTTP-Anfragemethoden hinzufügen, indem Sie – vielleicht in einer abgeleiteten Klasse – doPut, doTrace etc. definieren. Wenn Sie service direkt überschreiben, ist diese Möglichkeit ausgeschlossen. Zweitens können Sie das Änderungsdatum verarbeiten, indem Sie eine getLastModified-Methode, wie in Listing 3.7 zu sehen, hin- Servlet-Grundlagen 89 zufügen. Da getLastModified von der ursprünglichen service-Methode aufgerufen wird, berauben Sie sich durch Überschreiben von service dieser Option. Und schließlich erhalten Sie durch service automatisch Unterstützung für HEAD-, OPTION- und TRACE-Anfragen. Hinweis Wenn Ihr Servlet GET und POST identisch behandeln muss, lassen Sie Ihre doPost-Methode doGet aufrufen oder umgekehrt. Überschreiben Sie auf keinen Fall die Methode service. 3.6.2 Die Methoden doGet, doPost und doXxx Diese Methoden enthalten die eigentliche Substanz Ihres Servlets. 99 Prozent der Zeit werden Sie sich nur über GET oder POST-Anfragen und die Überschreibung der Methoden doGet und/oder doPost Gedanken machen. Wenn Sie möchten, können Sie aber auch doDelete für DELETE-Anfragen, doPut für PUT-Anfragen, doOptions für OPTIONS-Anfragen und doTrace für TRACE-Anfragen überschreiben. Denken Sie jedoch daran, dass Sie automatische Unterstützung für OPTIONS und TRACE haben. Normalerweise müssen Sie doHead nicht implementieren, um HEAD-Anfragen zu behandeln (HEAD-Anfragen fordern, dass der Server die normalen HTTP-Header ohne dazugehöriges Dokument zurückliefert). Das liegt daran, dass das System zum Beantworten von HEAD-Anfragen automatisch die Statuszeile und Header-Einstellungen von doGet benutzt. Manchmal kann es jedoch nützlich sein, doHead zu implementieren, damit Sie schneller Antworten auf HEAD-Anfragen erzeugen können (d.h. auf Anfragen von selbst geschriebenen Clients, die nur die HTTP-Header und nicht das eigentliche Dokument anfordern) – ohne das eigentliche Dokument zur Ausgabe mit erzeugen zu müssen. 3.6.3 Die init-Methode In den meisten Fällen verarbeiten Ihre Servlets allein die Daten der Anfrage, und die einzigen Methoden aus Lebenszyklus des Servlets, die Sie benötigen, sind doGet und doPost. Gelegentlich kann es jedoch vorkommen, dass Sie beim ersten Laden des Servlets komplexere Initialisierungen durchführen möchten, die später nicht für jede Anfrage wiederholt werden sollen. Für diese Fälle wurde die init-Methode entworfen. Sie wird nur zu Beginn des Lebenszyklus aufgerufen, wenn das Servlet erzeugt wird; für nachfolgende Benutzeranfragen wird sie nicht mehr erneut aufgerufen. Sie wird also, genau wie die initMethode der Applets, für einmalige Initialisierungen eingesetzt. Normalerweise wird ein Servlet erzeugt, wenn irgendein Benutzer den URL, der zu dem Servlet gehört, zum ersten Mal aufruft. Sie können aber auch vorgeben, dass das Servlet geladen wird, wenn der Server das erste Mal gestartet wird (mithilfe der Datei web.xml). Die Definition der init-Methode sieht folgendermaßen aus: public void init() throws ServletException { // Initialisierungscode... } Die init-Methode führt zwei Arten der Initialisierung aus: allgemeine Initialisierungen und Initialisierungen, die von Initialisierungsparameter gesteuert werden. 90 Kapitel 3 Allgemeine Initialisierung Bei der ersten Form der Initialisierung erzeugt oder lädt init einfach einige Daten, die während der Lebensdauer des Servlets verwendet werden, oder es führt bestimmte einmalige Berechnungen durch. Man kann dies mit einem Applet vergleichen, das getImage aufruft, um Bilddateien über das Netzwerk zu laden: Die Operation muss nur einmal durchgeführt werden und wird deshalb in init angestoßen. Einmalige Servlet-typische Aufgaben wären das Einrichten eines Datenbankverbindungspool für Anfragen, die das Servlet behandelt, oder das Laden einer Datendatei in eine HashMap. Listing 3.7 zeigt ein Servlet, das init für zweierlei Dinge verwendet. Zum einen baut es ein Array von 10 ganzen Zahlen auf. Da diese Zahlen das Ergebnis komplexer Berechnungen sind, sollen diese Berechnungen nicht für jede Anfrage wiederholt werden. Deshalb schaut doGet die von init berechneten Werte nach, anstatt diese Werte immer neu zu generieren. Abbildung 3.6 zeigt die Ergebnisse dieser Technik. Des Weiteren speichert das Servlet in init das Datum der letzten Überarbeitung, welches später von der getLastModified-Methode benutzt wird, und nutzt dabei den Umstand, dass sich seine Ausgabe nur ändert, wenn der Server neu hochgefahren wird. Die getLastModified-Methode sollte eine Änderungszeit zurückgeben, die gemäß dem Standard für Datumsangaben in Java in Millisekunden seit 1970 ausgedrückt ist. Diese Zeit wird automatisch in das für den Last-Modified-Header geeignete GMT-Datum umgerechnet. Was jedoch noch wichtiger ist: Wenn der Server eine bedingte GET-Anfrage empfängt (die spezifiziert, dass der Client nur Seiten möchte, die seit einem bestimmten Datum geändert wurden und mit If-Modified-Since gekennzeichnet sind), vergleicht das System das angegebene Datum mit dem von getLastModified zurückgegebenen Datum und liefert dem Client die Seite nur dann, wenn diese nach dem angegebenen Datum geändert worden ist. Da Browser oft solche bedingten Anfragen nach Seiten senden, die in ihren Caches gespeichert sind, kommt eine Unterstützung der bedingten Anfrage letztlich Ihren Benutzern zugute (sie erhalten schnellere Ergebnisse) und reduziert die Serverlast (Sie senden weniger vollständige Dokumente). Weil die Header Last-Modified und If-Modified-Since nur ganze Sekunden berücksichtigen, sollte die getLastModified-Methode die Zeitangaben auf die nächste Sekunde runden. Listing 3.7: coreservlets/LotteryNumbers.java package coreservlets; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /** Beispiel mit Servlet-Initialisierung und der Methode * getLastModified. */ public class LotteryNumbers extends HttpServlet { private long modTime; private int[] numbers = new int[10]; /** Die init-Methode wird nur beim erstmaligen Laden des * Servlets aufgerufen, bevor die erste Anfrage * verarbeitet wird. */ Servlet-Grundlagen public void init() throws ServletException { // Runde auf Sekunde auf (d. h. 1000 Millisekunden) modTime = System.currentTimeMillis()/1000*1000; for(int i=0; i<numbers.length; i++) { numbers[i] = randomNum(); } } /** Liefere die Liste der in init berechneten Zahlen zurück. */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); String title = "Your Lottery Numbers"; String docType = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 " + "Transitional//EN\">\n"; out.println(docType + "<HTML>\n" + "<HEAD><TITLE>"+title+"</TITLE></HEAD>\n" + "<BODY BGCOLOR=\"#FDF5E6\">\n" + "<H1 ALIGN=CENTER>" + title + "</H1>\n" + "<B>Based upon extensive research of " + "astro-illogical trends, psychic farces, " + "and detailed statistical claptrap, " + "we have chosen the " + numbers.length + " best lottery numbers for you.</B>" + "<OL>"); for(int i=0; i<numbers.length; i++) { out.println(" <LI>" + numbers[i]); } out.println("</OL>" + "</BODY></HTML>"); } /** * * * * * * * * * */ Die originale service-Methode vergleicht dieses Datum mit dem Datum aus dem If-Modified-Since-AnfrageHeader. Wenn das getLastModified-Datum neuer oder kein If-Modified-Since-Header vorhanden ist, wird die doGet-Methode normal aufgerufen. Ist jedoch das getLastModified-Datum gleich oder liegt es früher, so sendet die service-Methode eine 304er-Antwort (Not Modified) zurück und ruft doGet <B>nicht</B> auf. Der Browser soll in diesem Fall seine im Cache befindliche Version der Seite verwenden. public long getLastModified(HttpServletRequest request) { 91 92 Kapitel 3 return(modTime); } // Eine Zufallszahl vom Typ int zwischen 0 und 99. private int randomNum() { return((int)(Math.random() * 100)); } } Abbildung 3.6: Ausgabe des Servlets LotteryNumbers. Abbildung 3.7 und Abbildung 3.8 zeigen das Ergebnis von Anfragen mit zwei leicht voneinander abweichenden If-Modified-Since-Datumsangaben an dasselbe Servlet. Um die Anfrage-Header zu setzen und die Antwort-Header zu sehen, wurde WebClient verwendet, eine Java-Anwendung, mit der Sie HTTPAnfragen interaktiv einrichten, absenden und die Ergebnisse anzeigen lassen können. Der Code für WebClient ist im Quellcode-Archiv auf der Homepage zu diesem Buch zu finden (http://www.coreservlets.com/). Initialisierungen, die durch Initialisierungsparameter gesteuert sind In dem obigen Beispiel berechnete die init-Methode verschiedene Daten, die von den doGet- und getLastModified-Methoden verwendet wurden. Obwohl diese Art der allgemeinen Initialisierung weit verbreitet ist, findet man ebenso oft Code, bei dem die Initialisierung durch die Verwendung von Initialisierungsparametern gesteuert wird. Um verstehen zu können, warum Initialisierungsparameter eingesetzt werden, müssen Sie wissen, wer daran Interesse haben könnte, die Funktionsweise eines Servlets oder einer JSP-Seite zu beeinflussen. Es gibt insgesamt drei Gruppen: Servlet-Grundlagen 93 Abbildung 3.7: Wenn man auf das Servlet LotteryNumbers mit einer un-bedingten GET-Anfrage oder mit einer bedingten Anfrage, die ein vor der Servlet-Initialisierung liegendes Datum angibt, zugreift, erhält man die normale Webseite zurück. Der Code für das WebClient-Programm, mit dem hier interaktiv eine Verbindung zum Server hergestellt wurde) ist im Quellcode-Archiv zu diesem Buch unter http://www.coreservlets.com/ zu finden. 1. Entwickler. 2. Endnutzer. 3. Administratoren, die für die korrekte Installation verantwortlich sind. Entwickler ändern das Verhalten eines Servlets, indem Sie den Code verändern. Endnutzer ändern das Verhalten eines Servlets, indem sie Daten in ein HTML-Formular eingeben (vorausgesetzt der Entwickler hat vorgesehen, dass das Servlet nach diesen Daten Ausschau hält). Doch wie sieht es mit den Administratoren aus? Es muss auch für Administratoren eine Möglichkeit geben, Servlets von einer Maschine zu einer anderen zu verschieben und dabei bestimmte Parameter ändern zu können (z.B. die Adresse einer Datenbank, die Größe eines Verbindungspools oder den Speicherort einer Datendatei), ohne dafür gleich den Quellcode des Servlets anpassen zu müssen. Zu diesem Zwecke gibt es die Initialisierungsparameter. 94 Kapitel 3 Abbildung 3.8: Wenn man auf das Servlet LotteryNumbers mit einer bedingten GET-Anfrage zugreift, die ein Datum nach der Servlet-Initialisierung angibt, erhält man die Antwort 304 (Not Modified). Da die Verwendung von Initialisierungsparametern bei Servlets im starken Maße von dem DeploymentDeskriptor (web.xml) abhängt, dessen ausführliche Behandlung außerhalb der Möglichkeiten dieses Buches liegt, beschränken wir uns hier mit einem kurzen Überblick: 1. Verwenden Sie das web.xml-Element servlet, um Ihrem Servlet einen Namen zu geben. 2. Verwenden Sie das web.xml-Element servlet-mapping, um Ihrem Servlet einen eigenen URL zuzuweisen. Verwenden Sie niemals Standard-URLs der Form http://.../servlet/ServletName im Zusammenhang mit Initialisierungsparametern. (Tatsächlich werden diese Standard-URLs, so bequem sie zu Beginn eines neuen Projekts sein mögen, so gut wie nie für die konkrete Installation auf dem Zielserver verwendet. 3. Ergänzen Sie das web.xml-Element servlet um init-param-Unterelemente, um die Initialisierungsparameter mit Namen und Werten auszustatten. 4. Rufen Sie in der init-Methode Ihres Servlets getServletConfig auf, um eine Referenz auf das ServletConfig-Objekt zu erhalten. Servlet-Grundlagen 95 5. Rufen Sie die getInitParameter-Methode von ServletConfig mit dem Namen des Initialisierungsparameters auf. Der Rückgabewert ist der Wert des Initialisierungsparameters oder null, wenn in der web.xml-Datei kein solcher Initialisierungsparameter gefunden wird. 3.6.4 Die destroy-Methode Unter Umständen entscheidet sich der Server, eine zuvor geladene Instanz eines Servlets zu entfernen, vielleicht, weil der Server-Administrator dies explizit verlangt, vielleicht aber auch, weil das Servlet sehr lange untätig geblieben ist. Bevor er dies jedoch macht, ruft er die destroy-Methode des Servlets auf. Diese Methode gibt Ihrem Servlet eine Chance, Datenbankverbindungen zu schließen, im Hintergrund ablaufende Threads anzuhalten, Cookie-Listen oder Zugriffszählungen auf die Festplatte zu schreiben und andere vergleichbare Abschlussarbeiten vorzunehmen. Sie müssen jedoch immer auch damit rechnen, dass der Webserver abstürzen kann (denken Sie an die weltweit grassierenden Stromausfälle). Verlassen Sie sich also nicht auf destroy als einzigen Mechanismus, um den Zustand auf der Festplatte zu speichern. Auch Aktivitäten wie ein Zugriffszähler oder angesammelte Listen von Cookie-Werten, die speziellen Zugriff anzeigen, sollten ihren Zustand vorsorglich in regelmäßigen Abständen auf die Festplatte schreiben. 3.7 Die Schnittstelle SingleThreadModel Normalerweise erzeugt das System eine einzige Instanz Ihres Servlets und erstellt dann für jede Benutzeranfrage einen neuen Thread, wobei mehrere Threads parallel ablaufen, wenn eine neue Anfrage eintrifft und eine vorangegangene Anfrage noch nicht abgearbeitet ist. Folglich müssen Ihre Methoden doGet und doPost darauf achten, dass sie den Zugriff auf Felder und andere gemeinsam genutzte Daten (falls vorhanden) synchronisieren, da eventuell mehrere Threads gleichzeitig versuchen, auf diese Daten zuzugreifen. (Lokale Variablen werden nicht von mehreren Threads gemeinsam genutzt und benötigen deshalb keinen besonderen Schutz.) Wenn Sie den gleichzeitigen Zugriff durch mehrere Threads verhindern möchten, können Sie Ihr Servlet wie unten dargestellt die Schnittstelle SingleThreadModel implementieren lassen. public class YourServlet extends HttpServlet implements SingleThreadModel { ... } Wenn Sie diese Schnittstelle implementieren, gewährleistet das System, dass nie mehr als ein AnfrageThread auf eine einzelne Instanz Ihres Servlets zugreift. Dazu stellt es meistens alle Anfragen in eine Schlange und übergibt sie nacheinander einer einzigen Instanz des Servlets. Allerdings darf der Server auch einen Pool mit mehreren Instanzen erzeugen, die jeweils immer nur eine Anfrage behandeln. In beiden Fällen brauchen Sie sich nicht um gleichzeitige Zugriffe auf reguläre Felder (Instanzvariablen) des Servlets zu sorgen. Deswegen müssen Sie aber immer noch den Zugriff auf Klassenvariablen (als static deklarierte Felder) oder auf gemeinsam genutzte Daten außerhalb des Servlets synchronisieren. Doch auch wenn SingleThreadModel den zeitgleichen Zugriff vom Prinzip her verhindert, gibt es aus praktischer Sicht zwei Gründe, warum man darauf verzichten sollte. 96 Kapitel 3 Zum einen kann der synchronisierte Zugriff auf Ihre Servlets die Leistung stark beeinträchtigen (Latenz), wenn Ihr Servlet extrem stark frequentiert wird. Wenn ein Servlet auf E/A wartet, kann der Server anstehende Anfragen für dasselbe Servlet nicht entgegennehmen. Denken Sie also lieber zweimal nach, bevor Sie den SingleThreadModel-Ansatz wählen. Besser ist es, nur den Teil des Codes zu synchronisieren, der die gemeinsam genutzten Daten manipuliert. Ein weiteres Problem der SingleThreadModel-Schnittstelle ergibt sich dadurch, dass die Spezifikation es den Servern gestattet, Instanzen-Pools zu verwenden, anstatt die Anfragen in eine Warteschlange zu stellen und an eine einzelne Instanz zu übergeben. Solange jede Instanz nur eine Anfrage zurzeit bearbeitet, genügt der Instanzen-Pool-Ansatz den Anforderungen der Spezifikation. Aber auch er ist keine gute Lösung. Nehmen wir an, dass wir reguläre, nicht als static deklarierte Instanzvariablen (Felder) verwenden, um auf gemeinsam verwendete Daten zuzugreifen. Zwar verhindert die SingleThreadModel-Schnittstelle den gleichzeitigen Zugriff, aber dazu schüttet sie auch gleich das Baby mit dem Wasser aus, denn jede Servlet-Instanz besitzt eine eigene Kopie der Instanzvariablen, sodass man nicht mehr von echter gemeinsamer Nutzung der Daten sprechen kann. Wie sieht es dagegen aus, wenn wir als static deklarierte Instanzvariablen verwenden, um auf gemeinsam verwendete Daten zuzugreifen. In diesem Fall bietet der Instanzen-Pool-Ansatz zu SingleThreadModel keinerlei Vorteil. Immer noch können mehrere Anfragen (unter Verwendung verschiedener Instanzen) zeitgleich auf die statischen Daten zugreifen. Und doch ist die SingleThreadModel-Schnittstelle gelegentlich durchaus nützlich. Zum Beispiel kann sie eingesetzt werden, wenn die Instanzvariablen für jede Anfrage neu initialisiert werden (wenn sie z.B. nur dazu verwendet werden, die Kommunikation zwischen den Methoden zu vereinfachen). Im Großen und Ganzen sind die Probleme mit der Schnittstelle SingleThreadModel jedoch so gravierend, dass sie in der Servlet-Spezifikation 2.4 (JSP 2.0) als veraltet eingestuft wurde. Wir empfehlen Ihnen, stattdessen lieber die expliziten synchronized-Blöcke zu verwenden. Warnung Vermeiden Sie die Implementierung von SingleThreadModel für stark frequentierte Servlets. Verwenden Sie die Schnittstelle auch sonst nur mit größter Vorsicht. Für die endgültigen Versionen Ihrer Servlets ist die explizite Codesynchronisation fast immer die bessere Wahl. Ab Version 2.4 der Servlet-Spezifikation ist SingleThreadModel veraltet. Betrachten wir beispielsweise das Servlet aus Listing 3.8, das versucht jedem Client eine eindeutige Benutzer-ID zuzuweisen (eindeutig, bis der Server neu startet). Es verwendet eine Instanzvariable (Feld) namens nextID, um zu verfolgen, welche ID als Nächstes zugewiesen werden soll, und verwendet den folgenden Code, um die ID auszugeben. String id = "User-ID-" + nextID; out.println("<H2>" + id + "</H2>"); nextID = nextID + 1; Servlet-Grundlagen 97 Nehmen wir einmal an, Sie wären sehr vorsichtig beim Testen dieses Servlets. Sie würden es in ein Unterverzeichnis namens coreservlets ablegen, es kompilieren und das coreservlets-Verzeichnis in das Verzeichnis WEB-INF/classes der Standardwebanwendung kopieren (siehe Abschnitt 2.10). Anschließend würden Sie den Server starten und mehrmals mit http://localhost/servlet/coreservlets.UserIDs auf das Servlet zugreifen. Bei jedem Zugriff erhielten Sie einen anderen Wert (Abbildung 3.9) Also ist Ihr Code korrekt, nicht wahr? Falsch! Das Problem tritt erst auf, wenn es mehrere gleichzeitige Zugriffe auf das Servlet gibt. Und auch dann nur sporadisch. Aber es kann passieren, dass in einigen Fällen der erste Client das nextID-Feld liest und dann auf Grund des präemptiven Multitasking die Kontrolle über den Thread entzogen bekommt, bevor er das Feld inkrementiert hat. Anschließend könnte ein zweiter Client das Feld lesen und erhielte den gleichen Wert wie der erste Client. Das ist schlecht! So hat es zum Beispiel tatsächlich E-Commerce-Anwendungen gegeben, in denen Kundenkäufe gelegentlich auf einer falschen Kundenkreditkarte abgerechnet wurden, und Schuld waren genau die hier beschriebenen Konkurrenzsituationen bei der Erzeugung von Benutzer-IDs. Wenn Sie sich in der Multithread-Programmierung auskennen, ist Ihnen das Problem sicher gleich aufgefallen. Die Frage ist nur, welches ist die geeignete Lösung? Sehen Sie im Folgenden drei Möglichkeiten: 1. Verkürzen Sie die Konkurrenzsituation. Löschen Sie die dritte Zeile des Codefragments und ändern sie die erste Zeile in: String id = "User-ID-" + nextID++; Puuh! Durch diesen Ansatz ist die Wahrscheinlichkeit einer falschen Antwort zwar geringer, aber die Möglichkeit besteht immer noch. In vielen Szenarien ist die Reduzierung der Wahrscheinlichkeit einer falschen Antwort eher von Nachteil denn von Vorteil: Es bedeutet lediglich, dass das Problem beim Testen schlechter aufzuspüren ist und wahrscheinlich genau dann auftritt, wenn die Testphase vorbei ist. 2. Verwenden Sie SingleThreadModel. Ändern sie die Servlet-Klassendefinition wie folgt. public class UserIDs extends HttpServlet implements SingleThreadModel { Funktioniert das? Wenn der Server SingleThreadModel implementiert, indem er alle Anfragen in eine Warteschlange stellt, dann mag das funktionieren. Allerdings auf Kosten der Leistung, wenn es viele gleichzeitige Zugriffe gibt. Und was noch schlimmer ist, wenn der Server SingleThreadModel implementiert, indem er einen Servletinstanzen-Pool einrichtet, ist dieser Ansatz gänzlich unbrauchbar, da jede Instanz sein eigenes nextID-Feld hat. Beide Serverimplementierungsansätze sind gültig, sodass diese »Lösung« keine Lösung darstellt. 3. Synchronisieren Sie den Code explizit. Verwenden Sie die in Java übliche Standardkonstruktion zur Synchronisierung. Beginnen Sie den synchronized-Block direkt vor dem ersten Zugriff auf die gemeinsam genutzten Daten und beenden Sie den Block direkt nach der letzten Aktualisierung der Daten: synchronized(this) { String id = "User-ID-" + nextID; out.println("<H2>" + id + "</H2>"); nextID = nextID + 1; } 98 Kapitel 3 Damit teilen Sie dem System mit, dass sobald ein Thread in den obigen Codeblock (oder einen anderen synchronisierten Abschnitt, der mit der gleichen Objektreferenz gekennzeichnet wurde) eingetreten ist, kein anderer Thread Zugriff hat, bis der erste Thread den Block verlässt. Dies ist die Lösung, die immer in Java verfolgt wird. Sie ist auch hier die richtige Wahl. Vergessen Sie fehleranfällige und leistungsschwache SingleThreadModel-Abkürzungen. Beheben Sie Konkurrenzsituationen ordnungsgemäß. Listing 3.8: coreservlets/UserIDs.java package coreservlets; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /** * * * * */ Servlet, das versucht, jedem Benutzer eine eindeutige Benutzer-ID zuzuteilen. Da es jedoch versäumt, den Zugriff auf das nextID-Feld zu synchronisieren, kommt es zu Konkurrenzsituationen zwischen den Threads. Folge: Zwei Benutzer können die gleiche ID zugewiesen bekommen. public class UserIDs extends HttpServlet { private int nextID = 0; public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); String title = "Your ID"; String docType = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 " + "Transitional//EN\">\n"; out.println(docType + "<HTML>\n" + "<HEAD><TITLE>"+title+"</TITLE></HEAD>\n" + "<CENTER>\n" + "<BODY BGCOLOR=\"#FDF5E6\">\n" + "<H1>" + title + "</H1>\n"); String id = "User-ID-" + nextID; out.println("<H2>" + id + "</H2>"); nextID = nextID + 1; out.println("</BODY></HTML>"); } } Servlet-Grundlagen 99 Abbildung 3.9: Ausgabe des Servlets UserIDs. 3.8 Servlets debuggen Wenn Sie ein Servlet schreiben, machen Sie natürlich nie Fehler. Doch der eine oder andere Ihrer Kollegen macht vielleicht gelegentlich einen Fehler und dann können Sie ihm die folgenden Ratschläge geben. Nein, ernsthaft, das Debuggen von Servlets kann knifflig werden, weil Sie sie nicht direkt ausführen. Stattdessen starten Sie sie mit einer HTML-Anfrage und der Webserver führt sie aus. Diese Remote-Ausführung erschwert es, Haltepunkte zu setzen oder Debug-Meldungen und Stack-Traces zu lesen. Daher unterscheiden sich die Ansätze beim Debuggen von Servlets etwas von denjenigen, die bei einer normalen Entwicklung eingesetzt werden. Hier sind zehn allgemeine Strategien, die Ihnen das Leben erleichtern können. 1. Verwenden Sie print-Anweisungen. Die Server der meisten Anbieter blenden während der Ausführung des Servers auf dem Desktop ein Fenster für die Standardausgabe ein (d.h. die Ausgabe der System.out.println-Anweisungen). »Was?« werden Sie sagen, »Sie wollen doch sicherlich nicht auf etwas so Veraltetes wie print-Anweisungen zurückgreifen?«. Wir geben zu, dass es ohne Zweifel anspruchsvollere Debug-Techniken gibt, und wenn Sie damit vertraut sind, sollten Sie sie unbedingt anwenden. Aber es wird Sie überraschen, festzustellen, wie nützlich allein das Einholen von grundlegenden Informationen zu der Arbeitsweise Ihres Programms ist. Sie haben den Eindruck, die init-Methode arbeitet nicht ordnungsgemäß? Fügen Sie eine print-Anweisung ein, starten Sie den Server neu und prüfen Sie, ob die print-Anweisung im Standardausgabefenster angezeigt wird. Vielleicht haben Sie init nicht korrekt deklariert, sodass Ihre Version nicht aufgerufen wird? Sie erhalten eine Ausnahme vom Typ NullPointerException? Fügen Sie einfach einige print-Anweisungen ein, um festzustellen, in welcher Codezeile der Fehler erzeugt wird und welches Objekt auf der Zeile null war. Falls Sie Zweifel haben, holen Sie sich einfach mehr Informationen ein. 2. Verwenden Sie den in Ihre IDE integrierten Debugger. Viele integrierte Entwicklungsumgebungen (IDEs) verfügen über ausgereifte Debug-Tools, die in Ihre Servlet- und JSP-Container integriert werden können. Mit den Enterprise Editions der IDEs wie Borland JBuilder, Oracle JDeveloper, IBM WebSphere Studio, Eclipse, BEA WebLogic Studio, Sun ONE Studio, etc. können Sie beispielsweise Haltepunkte setzen, Methodenaufrufe verfolgen und so weiter. Manche IDEs ermöglichen es sogar, eine Verbindung zu einem Server herzustellen, der auf einem Remote-System ausgeführt wird. 100 Kapitel 3 3. Nutzen Sie die Protokolldatei. Die Klasse HttpServlet enthält eine Methode namens log, mit der Sie Informationen in eine Protokolldatei auf dem Server schreiben können. Es ist zwar etwas unpraktischer, Debug-Meldungen in der Protokolldatei zu lesen, anstatt sie wie bei den beiden vorangehenden Ansätzen direkt aus einem Fenster zu lesen, aber die Verwendung der Protokolldatei ist eine Option, auch wenn sie auf einem Remote-Server ausgeführt wird. In einer solchen Situation sind print-Anweisungen selten nützlich und nur die fortgeschritteneren IDEs unterstützen Remote-Debugging. Die log-Methode gibt es in zwei Ausführungen: Eine, die einen String übernimmt und eine andere, die einen String und eine Throwable-Instanz (Throwable ist eine Basisklasse von Exception) übernimmt. Der genaue Speicherort der Protokolldatei ist zwar serverspezifisch, aber im Allgemeinen klar dokumentiert oder in Unterverzeichnissen des Server-Installationsverzeichnisses zu finden. 4. Verwenden Sie Apache Log4J. Log4J ist ein Paket des Apache Jakarta Project – des Projekts, das auch Tomcat (einen der in diesem Buch verwendeten Beispielserver) und Struts (ein spezielles MVC-Framework) verwaltet. Mit Log4J fügen Sie beständige Debug-Anweisungen in Ihren Code ein und verwenden eine XML-basierte Konfigurationsdatei, um zu steuern, welche Anweisungen während der Abarbeitung einer Anfrage ausgeführt werden. Log4J ist schnell, flexibel, bequem und wird jeden Tag populärer. Nähere Einzelheiten hierzu finden Sie unter http://jakarta.apache.org/log4j/. 5. Setzen Sie separate Klassen auf. Eines der wichtigsten Prinzipien beim Entwerfen von Software ist es, häufig verwendeten Code in separate Funktionen oder Klassen kapseln, sodass Sie diesen Code nicht immer wieder neu schreiben müssen. Dieses Prinzip gewinnt bei Servlets noch mehr an Bedeutung, da diese separate Klassen oft unabhängig vom Server getestet werden können. Sie können sogar eine Testroutine mit einer main-Funktion schreiben, die dazu verwendet werden kann, Hunderte oder sogar Tausende von Testfällen zu generieren und zu prüfen – was Sie wahrscheinlich nicht tun würden, wenn Sie jeden Testfall dem Browser von Hand übermitteln müssten. 6. Treffen Sie Vorkehrungen für fehlende und fehlerhafte Daten. Lesen Sie Formulardaten von einem Client ein (Kapitel 4)? Denken Sie daran, zu prüfen, ob diese null oder ein leerer String sind. Verarbeiten Sie HTTP-Anfrage-Header (Kapitel 5)? Denken Sie daran, dass Header optional sind und in manchen Anfragen null sein können. Jedes Mal, wenn Sie Daten verarbeiten, die direkt oder indirekt von einem Client kommen, sollten Sie die Möglichkeit in Betracht ziehen, dass die Daten falsch oder zum Teil gar nicht eingegeben wurden. 7. Schauen Sie in den HTML-Quellcode. Wenn Ihr Browser seltsame Ergebnisse anzeigt, sollten Sie im Browser-Menü QUELLTEXT ANZEIGEN aufrufen. Manchmal führt ein kleiner Fehler wie z.B. <TABLE> anstelle von </TABLE> dazu, dass ein Großteil der Seite nicht angezeigt wird. Noch besser ist es, wenn Sie die Ausgabe des Servlets mit einem formalen HTML-Validierer überprüfen. Dieser Ansatz ist in Abschnitt 3.5 beschrieben. 8. Schauen Sie sich die Anfragedaten separat an. Servlets lesen Daten aus der HTTP-Anfrage, konstruieren eine Antwort und senden diese an den Client zurück. Wenn in diesem Prozess etwas schief geht, sollten Sie ermitteln, ob es daran liegt, dass der Client verkehrte Daten gesendet hat, oder ob das Servlet die Daten verkehrt verarbeitet. Mit der in Kapitel 19 vorgestellten Klasse EchoServer können Sie HTML-Formulare senden und erhalten Servlet-Grundlagen 101 ein Ergebnis, das genau zeigt, wie die Daten auf dem Server eingetroffen sind. Diese Klasse ist nichts weiter als ein einfacher HTTP-Server, der für alle Anfragen eine HTML-Seite aufbaut, die anzeigt, was gesendet wurde. Den vollständigen Quellcode erhalten Sie online unter http://www. coreservlets.com/. 9. Schauen Sie sich die Antwortdaten separat an. Wenn Sie schon die Anfragedaten separat untersuchen, sollten Sie das auch mit den Antwortdaten machen. Mit Hilfe der Klasse WebClient, die wir im Zusammenhang mit dem init-Beispiel in Abschnitt 3.6 vorgestellt haben, können Sie sich interaktiv mit dem Server verbinden, eigene HTTPAnfragedaten senden und einschließlich HTTP-Antwort-Headern alles sehen, was zurückkommt. Auch diesen Quellcode können Sie von http://www.coreservlets.com/ herunterladen. 10. Halten sie den Server an und starten Sie ihn neu. Die meisten Webserver sollten die Servlets zwischen den Anfragen im Speicher behalten und nicht bei jeder Ausführung neu laden. Die meisten Server unterstützen darüber hinaus einen Entwicklungsmodus, in dem Servlets automatisch neu geladen werden sollen, wenn sich ihre Klassendatei ändert. Manchmal gerät jedoch der ein oder andere Server durcheinander, besonders, wenn Sie nur eine einzige Änderung in einer tiefer geschachtelten Klasse und nicht in der obersten Servlet-Klasse vorgenommen haben. Wenn Sie also den Eindruck haben, dass das Verhalten Ihrer Servlets die von Ihnen gemachten Änderungen nicht widerspiegelt, sollten Sie den Server neu starten. Denken Sie in diesem Zusammenhang auch daran, dass die init-Methode nur ausgeführt wird, wenn ein Servlet das erste Mal geladen wird, die web.xml-Datei (siehe Kapitel 2.11) nur gelesen wird, wenn eine Webanwendung das erste Mal geladen wird (obwohl viele Server über Möglichkeiten zum Neuladen verfügen), und gewisse Webanwendungs-Listener nur ausgelöst werden, wenn der Server das erste Mal gestartet wird. Das Neustarten des Servers vereinfacht das Debuggen in allen drei Situationen.