Java™ Servlet Technologie
zurück weiter
Java™ Servlet Technologie

Grundlagen


  1. Was ist ein Servlet?
  2. Lebenszyklus eines Servlets
  3. Grundstruktur eines HTTP-Servlets
  4. Datenbankanbindung über Servlets

Was ist ein Servlet?

Bei Servlets handelt es sich um Java-Klassen, die serverseitig ausgeführt werden. Sie bearbeiten dabei die Anfragen von Clients und liefern entsprechend der Anfordeung dynamisch erzeugte HTML-Seiten als Antwort zurück. So betrachtet ist die Java Servlet Technologie also das Java-Äquivalent zur ebenfalls sehr stark verbreiteten CGI-Programmierung.

Der Einsatz von Servlets macht überall dort Sinn, wo sich der Inhalt einer HTML-Seite häufig ändert, der Inhalt einer HTML-Seite von Nutzereingaben abhängig ist oder in Datenbanken vorhandene Daten über ein Web-Interface zugänglich sein sollen.

Servlets sind grundsätzlich protokollunabhängig und können somit Anforderungen der unterschiedlichsten Art verarbeiten. Am häufigsten werden sie aber für Web-Anwendungen eingesetzt, die das HTTP-Protokoll verwenden. Neben dem Package javax.servlet wird deshalb zusätzlich noch das Package javax.servlet.http von der Java™ Servlet API zur Verfügung gestellt, welches speziell HTTP-spezifische Methoden bereitstellt.

Die Beschreibung der Funktionalität der Java™ Servlet Technologie wird im Weiteren am HTTP-Servlet erklärt, da dieses die breiteste Anwendung findet und auch im Beispielprojekt des elektronischen Produktkataloges eingesetzt wird.

Lebenzyklus eines Servlets

Der Lebenszyklus eines Servlets beschreibt dessen Initialisierung, Ausführung und Beendigung. Verantwortlich für die Erledigung dieser Aufgaben ist der Web-Server, in dessen Java-Umgebung das Servlet ausgeführt wird.

Erhält ein solcher Web-Server die Anforderung eines Clients für ein bestimmtes Servlet, so prüft er zunächst, ob bereits eine Instanz dieses Servlets existiert. Sollte dies nicht der Fall sein, so wird die entsprechende Servlet-Klasse geladen, davon eine Instanz erzeugt und deren Initialisierung ausgeführt, bevor dann schließlich die eigentliche Service-Methode des Servlets aufgerufen werden kann, welche die Client-Anforderung bearbeitet und die dynamisch generierte HTML-Seite zurückgibt. Die Beendigung eines Servlets erfolgt ebenfalls duch den Web-Server, indem er die Finalisierungsmethode des Servlets aufruft.

Initialisierung eines Servlets

Nach Erzeugung der Instanz einer Servlet-Klasse wird vom Web-Server automatisch deren Methode init() aufgerufen. Die Methode init() kann vom Programmierer überschrieben werden, um bei der Initialisierung des Servlets beispielsweise Variablen mit Vorgabewerten zu belegen oder andere Aktionen auszuführen, die nur ein einziges Mal zu Beginn stattfinden sollen. Eine solche Aktion könnte beispielsweise auch der Anmeldevorgang bei einem Datenbanksystem sein.

Ein erneutes Aufrufen der Methode init() ist durch den WEB-Server nur nach erneutem Laden des Servlets möglich. Hierzu muß der Web-Server aber zunächst die Methode destroy() des Servlets aufrufen, um es zu beenden.

Hinweis: Beim Auftreten eines Fehlers während der Initialisierung sollte mit dem Auslösen einer UnavailableException reagiert werden.

Service-Methoden eines Servlets

Nach der Initialisierung eines Servlets ist es in der Lage, Anforderungen von Clients zu bearbeiten. Solche Anforderungen können für HTTP-Servlets entsprechend dem HTTP-Protokoll vom Typ Get, Post, Put, Delete,Trace oder Options sein.

Die Hauptaufgabe der Service-Methoden besteht dabei unter anderem aus dem Extrahieren der Informationen aus der Client-Anforderung, dem Zugriff auf externe Ressourcen und der Generierung einer Antwort für den Client, wobei die Auswertung der Client-Anforderung und der Zugriff auf externe Ressourcen aber nicht zwingend erforderlich ist. Beispielsweise könnte ein Servlet als einzigen Service eine HTML-Seite generieren, deren Inhalt die aktuelle Server-Zeit darstellt. In solch einem Falle wäre ein Extrahieren der Informationen aus der Client-Anforderung unnötig, da zur Ermittlung der Server-Zeit keinerlei Informationen seitens des Client erforderlich sind. Der Aufruf der Service-Methoden des Servlets erfolgt durch den Web-Server nach entsprechender Anforderung eines Clients.

Beim HTTP-Servlet stehen folgende Service-Methoden zur Verfügung:

Das Überschreiben dieser Service-Methoden ermöglicht somit die Implementierung der Funktionalitäten, welche das Servlet zur Generierung der gewünschten HTML-Seite bereitstellen muß.

Beendigung eines Servlets

Servlets werden vom Web-Server beendet, wenn dieser beispielsweise Speicher freigeben will, oder aber der Web-Server selbst heruntergefahren wird. Dabei wird die Methode destroy() des Servlets aufgerufen. Durch Überschreiben dieser Methode ist es möglich, bei Beendigung des Servlets Aktionen durchzuführen, die beispielsweise der Freigabe von Speicher dienen oder die Abmeldung von einem Datenbanksystem zum Ziel haben.

Grundstruktur eines HTTP-Servlets

Der nachfolgende Quellcode zeigt das Grundgerüst für ein HTTP-Servlet, welches Client-Anforderungen vom Typ GET bearbeiten kann. Die dargestellten Methoden init() und destroy() sind optional und können ebenso gut auch weggelassen werden, wenn für sie kein Bedarf besteht.

    import java.io.*;
    import javax.servlet.*;
    import javax.servlet.http.*;

    public class SomeServlet extends HttpServlet {

      public void init()
      throws ServletException {
        // optional

        // Aktionen, die bei der Initialsierung ausgeführt werden
      }

      public void doGet(HttpServletRequest request,
                        HttpServletResponse response)
      throws ServletException, IOException {
        // aus "request" können HTTP-Header und HTML-Formulardaten
        // des Clients ausgelesen werden

        // über "response" können HTTP-Response-Daten ermittelt werden
        // (z.B. Content Type spezifizieren, Cookies).

        PrintWriter out = response.getWriter();
        // "out" dient der Ausgabe der zu generierenden HTML-Seite
        // an den Browser
      }

      public void destroy() {
        // optional

        // Aktionen, die beim Beenden ausgeführt werden
      }
    }
    
Zur Erzeugung eines Servlets, welches Anfragen über das HTTP-Protokoll bearbeiten soll, wird die Klasse HttpServlet erweitert. Desweiteren sind die Methoden doGet(HttpServletRequest req, HttpServletResponse resp) beziehungsweise doPost(HttpServletRequest req, HttpServletResponse resp) zu überschreiben. Das hängt davon ab, ob der Client die Anfragen mittels der Methoden GET oder POST übermittelt. In der Praxis werden häufig beide Methoden überschrieben, wobei die Methode doPost() die Methode doGet() intern aufruft. Somit können dann sowohl Anforderungen mittels GET als auch mittels POST vom Servlet bearbeitet werden, wobei die vom Servlet auszuführenden Anweisungen nur in der Methode doGet(HttpServletRequest req, HttpServletResponse resp) niederzuschreiben sind. Das nachfolgende Beispiel macht dies dann noch deutlich.

Sowohl die Methode doGet(HttpServletRequest req, HttpServletResponse resp) als auch die Methode doPost(HttpServletRequest req, HttpServletResponse resp) besitzen zwei Übergabeparameter vom Typ HttpServletRequest und HttpServletResponse. Während die übergebene Variable vom Typ HttpServletRequest Methoden zum Extrahieren der Informationen aus der Client-Anforderung bereitstellt, stellt die Variable vom Typ HttpServletResponse Methoden zur Verfügung, welche beispielsweise die Spezifizierung des Medientyps (content type) und des PrintWriter zur Ausgabe der generierten HTML-Seite an den Client erlauben.

Das folgende Beispiel demonstriert ein HTTP-Servlet, welches eine HTML-Seite generiert, die die aktuelle Serverzeit und den Hostnamen des Client zum Inhalt hat.


    import java.io.*;
    import java.util.Date;
    import javax.servlet.*;
    import javax.servlet.http.*;


    public class ServerTime extends HttpServlet {

        public void doGet(HttpServletRequest request,
                          HttpServletResponse response)
        throws IOException, ServletException
        {
            // Medientyp festlegen und PrintWriter spezifizieren
            response.setContentType("text/html");
            PrintWriter out = response.getWriter();
            // Generierung der HTML-Seite
            out.println("<HTML><HEAD></HEAD><BODY>");
            out.println("Serverzeit: " + new Date() + "<BR>");
            // Ermitteln der Client-URL aus dem Request
            out.println("URL des Client: " + request.getRemoteHost());
            out.println("</BODY></HTML>");
        }

        public void doPost(HttpServletRequest request,
                           HttpServletResponse response)
        throws IOException, ServletException
        {
            // Übergabe an doGet(), falls Anforderung mittels POST
            doGet(request, response);
        }
    }
    
Die von diesem Beispiel generierte HTML-Seite können Sie hier anfordern.

Datenbankanbindung über Servlets

Ein wichtiges Anwendungsgebiet für Servlets ist der Zugriff auf Datenbanken über ein Web-Interface. Hierzu muß das Servlet eine Verbindung zur Datenbank aufbauen, Abfragen und Manipulationen an der Datenbank vornehmen sowie die Verbindung zur Datenbank schließen können. Desweiteren muß auch eine Ausnahmebehandlung bei auftretenden Fehlern berücksichtigt werden.

Zum Zugriff auf relationale Datenbanken wird die JDBC™ 2.x API verwendet. Diese stellt Klassen und Methoden für den Zugriff auf eine sehr große Anzahl unterschiedlicher Datenbanksysteme zur Verfügung.

Verbindungsaufbau

Zum Verbindungsaufbau zur Datenbank werden aus der JDBC™ API das Interface Connection und die Klasse DriverManager verwendet. Hierbei muß zunächst der Treiber für die Zieldatenbank definiert werden, bevor der eigentliche Verbindungsaufbau durchgeführt werden kann. Das folgende Beispiel demonstriert den Verbindungsaufbau zu einer Oracle-Datenbank namens IMNLehre.
    
    import java.sql.*;
    ...

    // Objekt für Datenbankzugriff
    private Connection conn = null;
    ...

    // Definition des Treibers
    DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
    // Connect-String definieren
    String cstr = "jdbc:oracle:thin:user/passwd@141.57.9.9:1521:IMNLehre"
    // Verbindungsaufbau herstellen
    conn = DriverManager.getConnection(cstr);

    ...
    
Wie aus dem Beispiel ersichtlich wird, ist zum Verbindungsaufbau außerdem ein sogenannter Connect-String zu definieren, der sich aus drei Teilen zusammensetzt.
  1. Treiber: Im vorliegenden Beispiel wird mittels jdbc:oracle:thin der Thin-Treiber von Oracle™ angesprochen, welcher den Datenbankzugriff über das Internet gestattet.
  2. Nutzername/Passwort: Für user und passwd sind Nutzername und Passwort anzugeben, um sich an der Zieldatenbank erfolgreich anmelden zu können.
  3. Name der Datenbank: Mit 141.57.9.9:1521:IMNLehre wird eine Datenbank namens IMNLehre angesprochen, die sich auf dem Host mit der IP 141.57.9.9 befindet und dort über Port 1521 erreichbar ist. Wird das Servlet auf dem gleichen Host ausgeführt, auf dem sich auch die Zieldatenbank befindet, so reicht an dieser Stelle die Angabe des Datenbanknamens. Die Angabe von Host-IP und Port-Nummer ist also nur dann erforderlich, wenn Servlet und Datenbank auf unterschiedlichen Hosts laufen.

SELECT

Für das Ausführen von Queries auf Datenbanken stellt die JDBC™ 2.x API die Interfaces Statement und ResultSet zur Verfügung. Während Statement unter anderem eine Methode zur Ausführung einer Query bereitstellt, wird von ResultSet eine Vielzahl von Methoden zur Verarbeitung der Ergebnismenge der gestellten Abfrage angeboten. Das nachfolgende Beispiel demonstriert die Verwendung dieser Interfaces für eine einfache Abfrage, bei der die Attribute ename und eid für alle Datensätze der Tabelle emp abgefragt und in einem Vector-Objekt gespeichert werden.
    
    Vector result = new Vector();
    // Formulierung der Abfrage
    String sql = "SELECT ename, eid FROM emp";
    // Initialisierung des Statement-Objektes
    Statement sment = conn.createStatement();
    // Ausführung der Query und Zuweisung der Ergebnismenge
    ResultSet rs = sment.executeQuery(sql);
    // alle Zeilen der Ergebnismenge abarbeiten
    while (rs.next()) do {
        // Auslesen der Ergebnismenge
        result.addElement(rs.getString("ename"));
        result.addElement(rs.getString("pid"));
    }
    // Schließen der beiden Objekte
    rs.close();
    sment.close();
    
Wie aus dem Beispiel ersichtlich wird, muß der Inhalt der Query zunächst in Form einer Zeichenkette formuliert werden. Außerdem muß unter Verwendung der Methode createStatement() des Connection-Objektes conn ein Statement-Objekt erzeugt werden, bevor dann schließlich mittels der Methode executeQuery(String sql) die eigentliche SQL-Anweisung auf der Datenbank ausgeführt werden kann. Der Methode executeQuery(String sql) wird dabei als Parameter die zuvor in der Zeichenkette formulierte SQL-Anweisung übergeben. Als Ergebnis wird von dieser Methode das anfangs beschriebene ResultSet-Objekt zurückgegeben.

Zum Verarbeiten der einzelnen Datensätze der Ergebnismenge stellt das ResultSet-Objekt zahlreiche Methoden zur Verfügung. Im Beispiel wird mittels next() der Satzzeiger der Ergebnismenge um den Wert 1 erhöht. Dabei ist unbedingt zu beachten, daß der Satzzeiger zu Beginn vor dem ersten Datensatz der Ergebnismenge steht. Es muß also immer zuerst die next()-Methode aufgerufen werden, bevor auf den ersten Datensatz zugegriffen werden kann. Sollte die Ergebnismenge leer sein oder der Satzzeiger hinter dem letzen Datensatz positioniert sein, so liefert next() den Wahrheitswert FALSE zurück.

Der Zugriff auf den Inhalt der Datensätze des ResultSet-Objektes erfolgt im Beispiel über die Methode getString(String columnName). Hierbei wird der Wert des angegebenen Attributes als Zeichenkette zurückgegeben. Die Methoden zur Rückgabe anderer Datentypen werden hier ausführlich aufgelistet. Neben der dargestellten Möglichkeit, auf die Attribute eines Datensatzes über ihren Namen zuzugreifen, können diese auch über ihren Index angesprochen werden. Hierbei ist aber zu beachten, daß dieser Spaltenindex bei 1 beginnt, und nicht etwa bei 0. Für unser Beispiel wäre dann die Methode getString(int columnIndex) zu verwenden.

Nach Verarbeitung der Ergebnismenge sollten die Statement- und ResultSet-Objekte mittels der Methode close() wie im Beispiel geschlossen werden, um nicht mehr benötigte Ressourcen für andere Operationen freizugeben.

INSERT / DELETE / UPDATE

Die Manipulation von Daten mittels INSERT, UPDATE oder DELETE gestaltet sich ähnlich der zuvor beschriebenen Ausführung von SELECT-Anweisungen. Das nachfolgende Beispiel macht dies deutlich.

    // Formulierung der Anweisung
    String sql = "DELETE FROM emp WHERE ename LIKE 'Maier'";
    // Initialisierung des Statement-Objektes
    Statement sment = conn.createStatement();
    // Ausführung der Anweisung
    int cnt = sment.executeUpdate(sql);
    // Schließen des Statement-Objektes
    sment.close();
    
Gegenüber der Methode executeQuery(String sql) liefert die hier verwendete Methode executeUpdate(String sql) einen ganzzahligen Wert zurück. Dieser gibt an, wieviel Datensätze von der formulierten SQL-Anweisung betroffen worden sind. Im Beispiel würde dieser Rückgabewert also die Anzahl der gelöschten Datensätze repräsentieren, welche durch Ausführung der SQL-Anweisung aus der Datenbank entfernt wurden.

Im Beispiel ist außerdem zu erkennen, daß in einer SQL-Anweisung vorkommende Zeichenketten wie in Oracle™ üblich in Hochkommata eingeschlossen werden müssen.

Fehlerbehandlung

Wie bei vielen anderen Operationen können auch bei Datenbankoperationen Fehler auftreten. Diese müssen in Java™ durch die Verwendung einer geeigneten Ausnahmebehandlungsroutine abgefangen werden. Hierzu gibt es zwei Möglichkeiten. Die erste Variante besteht darin, jede Methode, die eine solche Datenbankoperation enthält, in ihrer Definition mit der Anweisung throws SQLException zu versehen. Die entsprechende Syntax wird im nachfolgenden Beispiel dargestellt.

    public void Methodenname()
    throws SQLException {
      // Definition von Datenbankoperationen
    }
    
Die zweite Variante besteht darin, die Datenbankoperationen durch die try{}-Anweisung zu kapseln. Gegenüber der ersten Variante bietet diese Vorgehensweise bei Ausführung mehrerer Datenbankoperationen innerhalb einer Methode den Vorteil, daß man gezielt auf eintretende Fehler einzelner Operationen eingehen kann, indem man diese jeweils in einem eigenen try{}-Block kapselt.

    try {
        // Definition von Datenbankoperationen
    }
    catch (SQLException e) {
        // Anweisungen zur Fehlerbehandlung
    }
    
Der in der catch{}-Anweisung vorkommende Parameter e vom Typ SQLException bietet zudem die Möglichkeit, über die Funktionen toString() beziehungsweise getMessage() die Fehlermeldung in Form einer Zeichenkette abzufragen.

Transaktionen

Um die Konsistenz einer Datenbank bei Ausführung mehrerer voneinander abhängiger Schreib-Operationen sicherstellen zu können, stellen Datenbanksysteme Mechanismen zur Kapselung solcher Gruppen von Operationen durch sogenannte Transaktionen zur Verfügung. Um diese Fähigkeit nutzen zu können, muß die Bestätigung von Schreiboperationen manuell ausgeführt werden, da der JDBC™-Treiber standardmäßig nach jeder einzelnen Schreiboperation eine Bestätigung mittels COMMIT veranlaßt.

Zu diesem Zweck muß unbedingt mittels der Methode setAutoCommit(boolean autoCommit) des Connection-Objektes die Eigenschaft der automatischen Transaktionsbestätigung deaktiviert werden. Die Bestätigung einer Transaktion beziehungsweise deren Rücknahme beim Auftreten eines Fehlers erfolgt über die Methoden commit() beziehungsweise rollback(). Die Syntax für den Transaktionsbetrieb wird nachfolgend dargestellt.


    try {
      // automatische Transaktionsbestätigung deaktivieren
      conn.setAutoCommit(false);

      // mehrere voneinander abhängige Schreiboperationen

      //Bestätigung der vorgenommenen Änderungen
      conn.commit();
    }
    catch (SQLException e) {
      // Rücknahme der bisherigen Änderungen
      conn.rollback();

      // optional weitere Behandlung des Fehlers
    }
    
Es ist unbedingt zu beachten, daß nach Deaktivierung der automatischen Transaktionsbestätigung nach jeder Schreiboperation manuell eine Bestätigung oder Rücknahme der Transaktion erfolgen muß. Ansonsten werden die Veränderungen an der Datenbank nicht wirksam. Durch Zuweisung des Wahrheitswertes TRUE mittels der Methode setAutoCommit(boolean autoCommit) kann die automatische Transaktionsbestätigung jederzeit wieder aktiviert werden.

Verbindungsabbau

Zum Abbau der Verbindung zur Datenbank bietet das Interface Connection die Methode close() an. Im vorliegenden Beispiel müßte man also lediglich folgenden Aufruf ausgeführen:

    conn.close();
    

zurück weiter