17.19 Der Lebenszyklus eines Servlets
Der Container für Servlets registriert eine Anfrage durch den Client und lädt das Servlet in den Speicher. Da Servlets normale Klassen sind, übernimmt ein spezieller Klassenlader diese Aufgabe. Die Abarbeitung findet anschließend in einem Thread statt, der die Methoden des Servlet-Objekts aufruft.
Wir wollen nun verfolgen, wie der Container die Arbeit an das Servlet delegiert. Über die Schnittstelle Servlet werden drei elementare Methoden für Initialisierung, Abarbeitung der Anfragen und Beendigung vorgeschrieben. Der Ablauf dieser Methoden nennt sich Lebenszyklus eines Servlets.
Die folgende Aufzählung zeigt alle Methoden, die die Schnittstelle Servlet für alle Java-Servlets vorschreibt.
interface javax.servlet.Servlet
|
|
void init( ServletConfig config )
Wird zu Anfang eines Dienstes aufgerufen. |
|
void service( ServletRequest req, ServletResponse res )
Der Container leitetet die Anfrage an das Servlet an diese Stelle. |
|
void destroy()
Wird am Ende eines Servlets vom Container genau einmal aufgerufen. |
|
ServletConfig getServletConfig()
Liefert ein ServletConfig-Objekt, welches Initialisierungs- und Startparameter kapselt. |
|
String getServletInfo()
Liefert Informationen über das Servlet wie Autor, Version und Copyright. |
Beispiel Ein Servlet implementiert getServletInfo(), um Informationen an den Servlet-Container zu geben.
public class FirstServlet extends GenericServlet
{
public void service( ServletRequest request, ServletResponse response )
{
}
public String getServletInfo()
{
return "Ich bin der erste Erguss seiner Servlet-Fähigkeiten";
}
}
|
17.19.1 Initialisierung in init()
Nachdem der Klassenlader genau ein Exemplar der Servlet-Klasse geladen hat, ruft der Container die init()-Methode des Servlets auf. Die Servlet-Spezifikation macht allerdings keine Aussage darüber, wann init() ausgeführt wird. Dies kann der Fall sein, wenn das Servlet geladen wird, aber noch keine Anfrage ansteht, wenn die erste Anfrage kommt oder aufgrund einer globalen Initialisierung. In init() kann das Servlet sich initialisieren und beispielsweise Netzwerk- oder Datenbankverbindungen aufbauen.
Neben init() existiert init(ServletConfig) mit einem Konfigurationsobjekt vom Typ der Schnittstelle ServletConfig. Die wichtigste Methode ist getServletContext(), die ein ServletContext-Objekt liefert, welches sich zum Beispiel zur Pfadumsetzung eignet. Wenn wir diese parametrisierte Methode überschreiben, dann dürfen wir es nicht versäumen, super() mit dem Parameter ServletConfig aufzurufen, und es somit weiterzuleiten:
public void init( ServletConfig config ) throws ServletException
{
super.init( config );
...
}
Die init()-Methode in der Oberklasse über super() anzusprechen, ist die einzige Möglichkeit, die uns Zugang zum Konfigurationsobjekt verschafft. Der Container rückt es nur an dieser Stelle heraus. Daher bleibt uns nichts anders übrig, als entweder init(Servlet-Config) mit super() oder init() ohne Parameter zu verwenden, um das Servlet-Config-Objekt zu bekommen.
Falls wir das ServletConfig-Objekt in init(ServletConfig) nicht benötigen, ist es einfacher, nur init() zu überschreiben. Das sieht dann in der Klasse GenericServlet (davon erbt HttpServlet) so aus:
public void init( ServletConfig config ) throws ServletException {
this.config = config;
log( "init" );
this.init();
}
Der Container ruft eigentlich nur init(ServletConfig) auf, und wie wir sehen, ruft die Methode dann selbstständig init() auf. Eine Referenz auf das Konfigurationsobjekt wird in GenericServlet gespeichert, auf die über getServletConfig() zugegriffen werden kann. Die Referenzvariable ist private, so wie es sich gehört. Somit sollte nicht init(ServletConig) überschrieben werden, sondern nur init().
Thread-Sicherheit
Während externe CGI-Programme Datenbankverbindungen für jede Anfrage neu aufbauen müssen, muss ein Servlet dies nicht. (Dadurch ergibt sich natürlich ein satter Geschwindigkeitsvorteil.) Dies funktioniert aber nur deswegen, da für jede Servlet-Instanz in ihrem Lebenszyklus nur einmal die init()-Methode aufgerufen wird. Alle Anfragen werden in einem eigenen Thread behandelt, und dieser ruft die Methode service() auf. init() muss nicht Thread-sicher sein. service() wird von mehreren Threads parallel aufgerufen und muss Thread-sicher sein.
Hinweis Obwohl in der Regel der Servlet-Server mehrere Threads initialisiert, die auf die init()-Methode zugreifen, kann die init()-Methode mehrfach aufgerufen werden, um mehrere Exemplare einer Servlet-Klasse zu erzeugen. Dann ist es ungünstig, Ressourcen wie Datenbankverbindungen immer neu anzufordern, obwohl vielleicht schon eine andere init()-Methode dies gemacht hat. Da es mehrere Exemplare der Servlet-Klasse im Servlet-Container geben kann, sollten statische Initialisierungen nicht in der init()-Methode durchgeführt werden.
|
Besser gelöst ist die Implementierung mit einer Fabrik-Methode seitens der Datenbank. Dann kümmert sie sich darum, dass es nur ein Exemplar gibt. Die Referenz wird dann ebenfalls von der Datenbank und nicht vom Servlet gespeichert.
Beispiel Wenn ein Servlet erwacht, wollen wir eine fiktive Datenbankverbindung für alle Servlets dieser Klasse aufbauen.
public class DBServlet extends GenericServlet
{
static DBConnection con;
public void init()
{
if ( con == null )
con = DB.getConnection();
}
public void service( ServletRequest request, ServletResponse response )
throws ServletException, IOException
{
...
}
}
|
17.19.2 Abfragen bei service()
In unserem ersten Beispiel haben wir uns der Klasse GenericServlet bedient, um eine service()-Methode zu überschreiben, die auf beliebige Anfragen des Clients antwortet. In der Regel wollen wir aber bei unterschiedlichen Anfragen auch unterschiedlich reagieren. Eine Anfrage ist zum Beispiel GET. Diese Form wird dann benutzt, wenn im Browser vom Benutzer eine URL eingetragen oder ein Verweis verfolgt wird. Neben GET-Anfragen gibt es noch POST-Anfragen, die für Formulare Verwendung finden.
Klasse HttpServlet
Damit wir auf GET-Anfragen anders reagieren können als auf POST-Anfragen, sind zwei Möglichkeiten denkbar: Entweder können wir die service()-Methode überschreiben, den Typ herausfinden und dann die Anfrage behandeln, oder wir verwenden eine andere Klasse, nämlich HttpServlet. Sie implementiert service() so, dass Anfragen an Methoden wie doGet(), doPost() und entsprechende Methoden weitergeleitet werden. Bei eigenen Anfragen wollen wir im Folgenden immer HttpServlet erweitern und service() nicht mehr direkt verwenden, sondern die entsprechenden doXXX()-Methoden.
Implementierung von service() in HttpServlet
Die service()-Methode der Klasse HttpServlet erfragt die verwendete Methode (GET oder POST) mit der Dienstmethode getMethod() vom aktuellen Request. Kurz skizziert hat sie folgendes Format:
protected void service( HttpServletRequest req, HttpServletResponse resp )
throws ServletException, IOException
{
String method = req.getMethod ();
if (method.equals(METHOD_GET)) {
...
} else if (method.equals (METHOD_HEAD)) {
...
} else if (method.equals (METHOD_POST)) {
doPost (req, resp);
} else {
...
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED,
errMsg);
}
}
Auf diese Weise testet service() zusätzlich auf METHOD_PUT, METHOD_DELETE, METHOD_OPTIONS und METHOD_TRACE.
Hinweis Die service()-Methode überschreiben bedeutet auch, dass wir uns um die Sicherheit Gedanken machen müssen. Wenn wir nur doGet() und doPost() gleich behandeln wollen, so implementieren wir zum Beispiel doGet() und leiten die Anfrage an doPost() weiter. Wenn wir alles in service() implementieren, dann kann Programmcode bei allen Anfragetypen ablaufen.
|
17.19.3 Mehrere Anfragen beim Servlet und die Thread-Sicherheit
In der Regel wird der Container pro Anfrage einen Thread erzeugen, der dann die service()-Methode des Servlet-Objekts betritt und die Anfrage bearbeitet. Es gibt demnach für mehrere Aufträge keine unterschiedlichen Exemplare des Servlets, sondern lediglich unterschiedliche Threads bei einem Servlet-Exemplar. Es ist aus diesem Grund zu bedenken, dass die Dienste seiteneffektfrei sein müssen. Eine einfache Möglichkeit, dies zu erzwingen, bietet eine synchronized-Methode. Die Entwickler haben noch eine zweite Möglichkeit vorgesehen, so dass sich der Entwickler nicht um die Nebenläufigkeit kümmern muss. Dazu implementiert das Servlet die Markierungsschnittstelle SingleThreadModel. Sie garantiert, dass der Dienst von maximal einem Thread zu jedem Zeitpunkt ausgeführt wird. Ein Nebeneffekt wäre, wenn ein Thread in einem Servlet Daten ändert, während ein anderer Thread im gleichen Servlet die gleichen Daten liest.
Im Server läuft dann etwa Folgendes ab:
Servlet servlet = getServlet();
if ( servlet instanceof SingleThreadModel )
{
synchronized ( servlet )
{
servlet.service();
}
}
else servlet.service();
Die Synchronisation wirkt sich natürlich auf die Ausführungsgeschwindigkeit nachteilig aus.
17.19.4 Das Ende eines Servlets
Wenn eine Servlet-Klasse nicht mehr benötigt wird und aus dem Speicher entfernt werden soll, ruft der Container zum Abschluss die Methode destroy() auf. Hier findet sich der Programmcode, der für die Freigabe sorgt und die aus init() bereitgestellten Ressourcen wieder freigibt. Häufig findet sich in init() und destroy() ein Mechanismus, der serialisierte Daten liest und schreibt. Doch was ist, wenn der Container vor destroy() den Geist aufgibt? Dies führt zu Problemen, wenn in destroy() Daten für die Persistenz serialisiert werden. Sie würden bei einem Absturz verloren gehen.
|