Core Servlets und JavaServer Pages

Werbung
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.
Herunterladen