Servlets und der Servlet Lifecycle
Servlets sind Javaklassen, die nach den Vorgaben der Servletspezifikation implementiert sind und in einem Servlet-Container leben. Sie nehmen Anfragen von HTTP-Clients entgegen und beantworten diese in Einklang mit der HTTP-Spezifikation. Eigene Funktionalität wird in erster Linie durch das Überschreiben der Servicemethoden der Servlet-Stubklassen erreicht.
Ein Servlet wird einmalig beim Initialisieren der Webanwendung geladen und verbleibt als Singleton im Webccontainer, bis die Webanwendung beendet wird. Im Einzelnen:
Vom Container wird also per VM immer nur eine Instanz des Servlets erzeugt. Alle Aufrufe am Servlet oder an dessen Umgebung (ServletConfig) müssen demnach synchronisiert werden, da sie potenziell nebenläufig passieren können.
Implementiert ein Servlet das Interface SingleThreadModel signalisiert das dem Container, dass dieses Servlet nicht nebenläufig genutzt werden darf. Leider (besser: erwartungsgemäß) gibt es keinen verlässlichen, allgemein anwendbaren und gleichzeitig skalierenden Mechanismus für diese Anforderung. Aus diesem Grund muss auf die Verwendung von SingleThreadModel verzichtet werden, diese Funktion ist 'deprecated' und wird in aktuellen Spezifikationen nicht weiterentwickelt. Jedes Servlet ist konsequent für nebenläufige Verwendung zu designen.
Form-Parameter oder URL-Parameter vom Request lesen:
req.getParameter("name"); // liefert den Parameter diese Namens req.getParameterNames(); // liefert alle Parameternamen
In einer Form können jedoch auch mehrere Parameter mit dem gleichen Namen exisiteren, diese werden dann als String-Array geliefert:
req.getParameterValues("name"); // ein String[] mit allen Parametern dieses Namens req.getParameterMap(); // eine Map mit key-String und value-String[]
Ruft man req.getParameter("name") an einem Parameter-Array, so wird (ohne Warnung) der erste Parameterwert zurückgegeben.
Auslesen des Request Header:
req.getHeader("name"); req.getHeaderNames();
Auch hier kann zu einem Header mehr als ein Eintrag existieren, dann hilft
req.getHeaders("name"); // liefert eine Enumeration aller Headereinträge zu diesem Namen
Für ganze Zahlen und Datumsangaben gibt es typsichere Methoden zum Auslesen:
req.getIntHeader("name"); // liefert einen Header als int req.getDateHeader("name"); // liefert einen Header als Date
Header können im Response auch gesetzt werden. Einige davon, zum Beispiel 'Date', werden dabei automatisch gesetzt:
// setzt eine Wert im Header, der vorhandene Wert wird überschrieben resp.setHeader("name", "value"); resp.setDateHeader("name", 12345678); resp.setIntHeader("name", 666); // fügt einen Wert dem Header hinzu resp.addHeader("name", "value"); resp.addDateHeader("name", 12345678); resp.addIntHeader("name", 666);
Auslesen von Cookies aus dem Request
req.getCookies(); // liefert ein Cookie-Array mit allen Cookies des Requests .. cookie.getValue(name);
Setzen von Cookies im Response:
Cookie cookie = new Cookie(name, value); // Ein Cookie wird erzeugt cookie.setMaxAge(seconds); resp.addCookie(cookie); // ein Cookie wird dem Response hinzugefügt
Bei der Arbeit mit Cookies muss man sich allerdings an gewisse Limitierungen halten, hier ein paar konkrete Zahlen:
Der Content Type informiert den anfragenden Client über die Natur des Inhalts, der im Response geschickt wird. Setzen des Content Type am Response:
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("application/pdf"); // resp.setContentType("application/java"); // resp.setContentType("application/jar"); :
Responses können formatiert (characterbasiert) oder unformatiert (bytebasiert) erfolgen. Beispiel: Für einen Fileupload wird (1) zunächst ein PrintWriter besorgt. Als nächstes setzt man Contenttype (2), Cache-Optionen (3) und Filename (4). Dann wird ein char-Stream auf den PrintWriter geschrieben (5).
1 PrintWriter out = response.getWriter(); // für den Browser unbekannter Datentyp (verhindert ungewolltes Öffnen) 2 response.setContentType("application/x-download"); // Der Name, unter dem clientseitig abgespeichert werden soll 3 response.setHeader("Content-Disposition", "attachment; filename=myName.txt"); // siehe Cache-Kapitel 4 response.setDateHeader("Expires", System.currentTimeMillis( ) + someTimes); // 5 Reader in = new BufferedReader(new FileReader(myFile)); char[] buf = new char[4096]; int r; while ((r = in.read(buf)) != -1) { out.write(buf, 0, r); }
Für unformatierte Daten dann statt des PrintWriter ein OutputStream benutzen:
1 OutputStream out = response.getOutputStream(); // für den Browser hoffentlich bekannter Datentyp 2 response.setContentType("application/zip"); // Der Name, unter dem clientseitig abgespeichert werden soll 3 response.setHeader("Content-Disposition", "attachment; filename=myName.txt"); // siehe Cache-Kapitel 4 response.setDateHeader("Expires", System.currentTimeMillis() + someMoreMillis); // 5 InputStream in = getServletContext().getResourceAsStream("/foobar.zip"); int r = 0; byte[] chunk = new byte[1024]; while ((r = in.read(chunk)) != -1) { out.write(chunk, 0, r); }
Bei GET und DELETE Requests werden die URL und HTTP Header übertragen, dank HTTP Spezifikation müssen sich Client und Server dabei nicht über das verwendete Encoding dieser Daten austauschen, denn die HTTP Spec regelt das. Bei POST und PUT Requests jedoch gibt es neben der URL und dem Header noch Nutzdaten, deren Encoding der Client bestimmt. Dieses Encoding muss der Server beim Lesen dieser Daten (also beim Lesen der Request-Parameter) kennen, damit deren Inhalt korrekt interpretiert wird. Das Attribut enctype in der HTML Form regelt dies nicht. Es bestimmt lediglich wie die Inhalte der Form als POST Request verpackt werden und wird hier nicht diskutiert.
Intuitiv erwartet man also, dass ein HTTP Client verpflichtet ist, den Server über das Encoding der Request-Parameter zu informieren. In der HTTP Spezifikation ist nichts in dieser Richtung zu finden. Hier soll es deshalb darum gehen, wie Web Container mit diesem Problem umgehen und wie der Anwendungsentwickler den Ablauf steuern kann:
Da die HTTP Spezifikation keine Anforderung für das Senden des Request-Parameter Encoding stellt wundert es nicht, dass Webbrowser diese Information auch nicht liefern. Dabei wäre es doch so einfach, beispielsweise das Encoding der HTML Seite die die FORM beinhaltet als das Request-Parameter Encoding zu nutzen!
Der Anwendungsentwickler muss sich anders behelfen. Im ServletRequest kann er die Methode
servletRequest.getCharacterEncoding()rufen. Wenn diese null zurückgibt, dann hat der Container kein Encoding ermitteln können - der Client hat keinen Content-Type Header gesetzt. In diesem Fall kann man das Encoding mit
servletRequest.setCharacterEncoding("UTF-8")explizit setzen. Eingesetzt wird das gleiche Encoding, das auf der die FORM beinhaltende HTML Seite benutzt wurde. In den meisten Fällen kennt das der Entwickler, weil er es selbst bestimmt hat. Leider kann man das Encoding nicht in der FORM, zum Beispiel als Hidden Field, definieren: setCharacterEncoding muss VOR dem Lesen der Requestparameter erfolgen, sonst ist es wirkungslos. Nachdem man also ein hypothetisches Hidden Field mit dem Encoding Angaben gelesen hat, kann man das Encoding zum Lesen der Parameter nicht mehr ändern. Schade eigentlich.
Der RequestDispatcher - include und forward
Der RequestDispatcher repräsentiert das Handle auf eine Ressource des Webservers.
// Holt sich das Handle vom Request - hier sind relative und absolute Pfade gestattet RequestDispatcher handle = request.getRequestDispatcher("view.jsp"); RequestDispatcher handle = request.getRequestDispatcher("/xyz/view.jsp");alternativ:
// // Holt sich das Handle vom ServletContext - hier sind nur absolute Pfade gestattet RequestDispatcher handle = getServletContext().getRequestDispatcher("/xyz/view.jsp");Am Handle kann nun ein include oder forward gerufen werden. Ein include leitet den Request und den gesamten Context an die angegebene Ressource weiter. Danach wird im aufrufenden Servlet die Verarbeitung fortgesetzt:
Im Servlet:
: handle.include(request, response); :Mit Includes, sie können beliebig tief verschachtelt sein, lassen sich hervorragend modulare Webseiten realisieren. Hinweis: die JSP Include-Direktive leistet das nicht. Sie fügt zur JSP-Übersetzungszeit statische Textfragmente ein.
Ein forward übergibt komplett die Kontrolle an die gegebene Ressource. Dabei darf noch keinerlei Response an den Client gesendet worden sein (auch nicht Teile). Technisch gesehen durfte somit bis zu Forward also weder explizit noch implizit ein flush gerufen sein und response.isCommitted() liefert false:
: bis hier darf kein flush auf den Response gegeben sein, sonst Fehler, alles : bisher Geschriebene wird ansonsten verworfen if (!reponse.isCommitted()) { handle.forward(request, response); } : diese Codezeile wird nie erreicht
Includes und Forwards werden serverseitig abgewickelt, der Client bekommt vom Dispatching nichts mit. Das HTTP Protokoll bietet aber noch eine weitere Möglichkeit, auf eine andere Ressource zu verweisen. Der Client bekommt eine entsprechende Anweisung in Form eines HTTP Response-Status nebst einer Zieladresse geliefert. Diese Technik ist im Zusammenhang mit dem PRG-Pattern sogar unabdingbar. Servletseitig ist einfach ein entsprechender Status zu setzen und die Location des Verweises zu setzen:
resp.setHeader("Location", encodeRedirectURL(location)); resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); // Ende, keine Ausgaben auf den Response
Der HttpServletResponse bietet auch eine Hilfsmethode dafür an:
resp.sendRedirect(encodeRedirectURL(location));sendet der Server dem Browser die HTTP-Nachricht, die angegebene URL abzuarbeiten. Das darf nicht geschehen, nachdem auf den response irgendwas geschrieben wurde.
Die Location kann man relativ, Application-relativ (beginnend mit "/")oder absolut (beginnend mit "http..") angeben. Gesendet wird der HTTP Statuscode 302 (die Ressource ist vorübergehend an einer anderen Stelle zu finden). Der Client wird damit aufgefordert, auch alle zukünftigen Anfragen wieder an diese URL zu richten.
URI Bestandteile einer Webanwendung und relative URLs
Pfadangeben beim Umgang mit Servlets folgen einer strikten Grundregel:
requestURI = contextPath + servletPath + pathInfo
Pfadangaben im HTML können auch relativ erfolgen und funktionieren, da HTTP Clients diese Angaben nach festen Regeln auswerten:
Oftmals ist es notwendig, den vom Servlet generierten Response zu manipulieren. Ein populäres Beispiel ist die Kompression des Outputs bevor er zum Client gesendet wird. Das HttpServletResponse Objekt repräsentiert in letzter Instanz eine Strom auf einen Socket, es ist im allgemeinen dann aus Sicht des Filters zu spät für die Manipulation dieses Stroms. Es muss also eine eigene Implementierung des HttpServletResponse Interfaces übergeben werden, welches keinen Socket dekoriert. Diese Implementierung wird erleichtert Dank der Wrapperklassen:
HttpServletRequestWrapper ServletRequestWrapper HttpServletResponseWrapper ServletResponseWrapper
In ihrem Konstruktor übernehmen sie das originale Request/Response Objekt und delegieren alle Methoden an dieses "gewrappte" Objekt weiter. Sie funktionieren also exakt wie das Original (Decorator-Pattern). In einer eigenen Implementierung erbt man von einer Wrapperklasse und überschreibt eine oder mehrere Methoden.