16 Weiterführende Themen

Wir wollen in diesem abschließenden Kapitel einige weiterführende Java-Themen ansprechen. Ob der Komplexität dieser Themen (jedes für sich könnte ein ganzes Buch füllen) können wir sie in diesem Rahmen nicht erschöpfend behandeln. Es soll Ihnen dessen ungeachtet ein Einblick in die jeweilige Thematik vermittelt werden. Wir werden folgende Themen ansprechen:

16.1 Reflection und Serialization

Zu Beginn der weiterführenden Themen wollen wir - zugegeben recht knapp - zwei Begriffe besprechen, die Grundlage für einige der nachfolgend intensiver behandelten Punkte sind. Es handelt sich einmal um die so genannte Reflection. Dies ist ein Verfahren in Java, um in Java-Code Informationen über Felder, Methoden und Konstruktoren von geladenen Klassen verfügbar zu machen. Die Technik der JavaBeans nutzt beispielsweise diese so verfügbar gemachten Informationen, um gegenüber Entwicklungsumgebungen Instrumente wie einen Inspektor nutzbar zu machen. Das zweite Grundlagenthema ist die so genannte Object Serialization oder einfach nur Serialization bzw. Serialisierung, die auf der Reflection aufbaut. Dieser Begriff beschreibt ein Java-Verfahren, um Objekte in einem Strom über die Laufzeit einer Java-Applikation hinaus zur Verfügung zu haben. Dazu wird es ermöglicht, dass der Inhalt eines Objekts in einen Stream gespeichert werden kann. Dieser Strom kann z.B. eine beliebige Datei sein. Ein Objekt kann somit in einem Stream zwischengespeichert und zu einem späteren Zeitpunkt daraus wieder aufgebaut werden. Die Lebensdauer eines Objekts kann also über die eigentliche Laufzeit eines Programms hinaus verlängert werden. Hauptanwendung hierfür ist das Versenden von Objekten über das Netzwerk im Zusammenhang mit dem RMI-Konzept, CORBA oder aber der Socket-Kommunikation, was wir dann auch nachfolgend in der Praxis zeigen werden.

16.1.1 Reflection

Das Konzept der Reflection entstand daraus, dass es in Java-Varianten bis zum JDK 1.1 massive Probleme gab, wenn man ein Objekt anlegen oder auf Teile davon zugreifen wollte und noch nicht zu Kompilierzeit bekannt war, wie es genau aussah. Die statische Struktur von Klassen und Objekten machte das extrem schwer. Das ist zwar im Allgemeinen kaum ein Problem, aber wenn man diese Informationen externen Debuggern oder visuellen Entwicklungstools - wie es Visual Basic mit seinen Komponenten für Basic darstellt - unter Java bereitstellen will, ist es ziemlich unbequem. Um das Problem zu beseitigen, wurde ein Konzept entwickelt, mit dem Klassen geladen und instanziert werden konnten, ohne dass bereits zu Kompilierzeit die konkreten Namen bekannt sein mussten. Die Lösung bestand in der Bereitstellung eines Reflection-APIs, das diese normalerweise vom Compiler geforderten Eigenschaften des Laufzeitsystems anderen Anwendungen in einer Proxyform bereitstellt. Im JDK 1.1 wurde das API in Form des Packages java.lang.reflect eingeführt und es wird auch im JDK 1.3 nahezu unverändert so bereitgestellt. Das API stellt die Schnittstellen InvocationHandler (wird vom Aufrufhandler einer Proxyinstanz eingebunden) und Member (Reflektion eindeutig identifizierender Informationen über ein einzelnes Mitglied - das kann ein Feld, eine Methode oder ein Konstruktor sein) bereit. Dazu kommen die Klassen AccessibleObject (Basisklasse von Feldern, Methoden und Konstruktoren), Array (Unterstützung von statischen Methoden für die dynamische Erstellung von Java-Arrays und den Zugriff darauf), Constructor (Informationen über den Konstruktor einer Klasse und Unterstützung des Zugriffs darauf), Field (Informationen über die Felder einer Klasse oder Schnittstelle und Unterstützung des Zugriffs darauf), Method (Informationen über die Methoden einer Klasse oder Schnittstelle und Unterstützung des Zugriffs darauf), Modifier (Unterstützung in Form von statischen Methoden und Konstanten zum Dekodieren der Zugriffsmodifier von Klassen und deren Mitgliedern), Proxy (die Superklasse aller dynamischen Proxyklassen mit Unterstützung in Form von statischen Methoden zum Generieren von dynamischen Proxyklassen und -instanzen) und ReflectPermission (allgemeine reflektive Operationen). Die Klassen in diesem Package bilden zusammen mit der Klasse java.lang.Class1 die Basis für Applikationen wie Debugger, Interpreter, Objektinspektoren, Klassenbrowser, aber auch Diensten wie der Object Serialization und JavaBeans. Letztere benötigen ja den Zugriff auf die mit public deklarierten Mitglieder von einem Zielobjekt bereits vor der Laufzeit. Denken Sie etwa bei visuellen Entwicklungsumgebungen an den grafischen Erstellungsmodus von Komponenten. Deren Eigenschaften und Methoden werden bereits während des Design-Modus benötigt.

Wir werden Reflection gleich im Rahmen der JavaBeans noch genauer verfolgen. Auch bei den darauf folgenden Datenbankzugriffen wird die Technik eine Rolle spielen.

16.1.2 Serialization

Serialization ist eine Erweiterung der Core-Java-Input/Output-Klassen um die Unterstützung von Objekten. Sie werden in einen Strom von Bytes gespeichert und können daraus komplett rekonstruiert werden. Serialization wird beispielsweise verwendet, um Objekte persistent zu machen (das dauerhafte Speichern von Objekt-Daten auf externen Datenträgern), für die Kommunikation via Sockets (das werden wir anschließend in der Praxis noch sehen) oder für die Umsetzung des Remote Method Invocation-Konzepts (RMI) verwendet. Die Defaultverschlüsselung von Objekten schützt dabei private und transiente Daten und unterstützt die Evolution von Klassen. Eine Klasse kann ihre eigene externe Verschlüsselung implementieren und ist dann aber selbst verantwortlich für das externe Format. Serialization basiert im Wesentlichen auf dem Package java.io mit seinen Ein- und Ausgabeströmen.

Das Schreiben von Objekten und primitiven Daten in einen Object Stream funktioniert meist so, wie in dem nachfolgend skizzierten Beispiel:

 // Serializiere das Tagesdatum in eine Datei
 FileOutputStream f = new FileOutputStream("tmp");
 ObjectOutput s = new ObjectOutputStream(f);
 s.writeObject("Heute: ");
 s.writeObject(new Date());
 s.flush();

Zuerst wird ein OutputStream (hier ein FileOutputStream) benötigt, der die Bytes empfängt. Dann wird ein ObjectOutputStream kreiert, der in den FileOutputStream schreibt. Nächster Schritt ist, den Textstring und das Date-Objekt in den Stream zu schreiben. Allgemein gilt, dass Objekte mit der writeObject()-Methode und primitive Daten mit den Methoden eines DataOutput-Stroms (etwa writeInt(), writeFloat() oder writeUTF()) in den Stream geschrieben werden.

Der umgekehrte Vorgang - das Lesen aus einem Object Stream - funktioniert ähnlich. Wir schauen uns das skizzierte Beispiel an, wie die oben serialisierten Daten wieder deserialisiert werden können.

 // Deserialisiert einen String und ein Datum aus 
// einer Datei
 FileInputStream in = new FileInputStream("tmp");
 ObjectInputStream s = new ObjectInputStream(in);
 String today = (String)s.readObject();
 Date date = (Date)s.readObject();

Zuerst wird ein InputStream (hier ein FileInputStream) benötigt, der als Quelle dient. Dann wird ein ObjectInputStream kreiert, der aus dem InputStream liest. Nächster Schritt ist, den Textstring und das Date-Objekt aus dem Stream zu lesen. Allgemein gilt, dass Objekte mit der readObject()-Methode und primitive Daten mit den Methoden eines DataInput-Stroms (etwa readInt(), readFloat(), oder readUTF()) in den Stream gelesen werden.

Bei dem nachfolgenden Beispiel der Socket-Kommunikation werden wir eine vollständige Anwendung von Serialization sehen.

16.2 JavaBeans

Wir kommen zu einem äußerst spannendem Thema - den Java Beans, welches massiv auf der Technik der Reflection aufsetzen. JavaBeans ist der Name für die Java-Komponenten innerhalb eines Objektmodells. Oder genauer: JavaBeans ist ein portables, plattformunabhängiges, in Java geschriebenes Komponentenmodell, das Sun in Kooperation mit führenden Industrieunternehmen der Computerbranche entwickelt hat. Motiviert wurden Beans höchst wahrscheinlich durch die grafischen Komponenten, die Visual Basic mit den Werkzeugen seiner IDE bereitgestellt hat. Visual Basic beinhaltete das erste Komponentenmodell, das sich auf dem Massenmarkt durchsetzte. JavaBeans können wie Visual-Basic-Komponenten visuell manipuliert werden, was gerade für viele Programmiereinsteiger eine erhebliche Erleichterung darstellt. In Visual Basic war der Erfolg einer solchen einfachen Erstellung von Programmen zu sehen. Das schnelle Erfolgserlebnis brachte viele Einsteiger dazu, sich mit dieser Sprache näher zu befassen. Aber auch für professionelle Programmierer erleichtert visuelles Erstellen von Oberflächen und ähnlichen Strukturen ein Projekt, indem es relativ einfache, aber stupide und zeitaufwändige Arbeit extrem beschleunigt.

Ein JavaBean besteht im Allgemeinen aus einer Sammlung einer oder mehrerer Java-Klassen, die oft in einer einzelnen JAR-Datei zusammengefasst sind. Ein JavaBean kann zum einen eine Komponente sein, die zur Erstellung einer Benutzerschnittstelle verwendet wird. Die visuellen Entwicklungsumgebungen für Java stellen solche Werkzeug-Tools bereit, etwa den frei verfügbaren JBuilder Foundation von Inprise (mehr Informationen zu diesem Tool und anderen Java-Entwicklungsprogrammen gibt es im Anhang).

Abbildung 16.1:  Die visuellen Komponenten  des JBuilders sind JavaBeans

Es gibt aber nicht nur Beans, die zur Laufzeit einer Applikation sichtbar sind. Es gibt auch solche, die im Rahmen einer Java-Applikation im Hintergrund verwendet werden sollen. Dies ist etwa eine Komponente, die Datenbank-Zugriffsfunktionalitäten bereitstellt. Auch diese wird im Rahmen einer visuellen IDE über eine Komponente in einer Toolbox bereitgestellt und kann visuell zur Designzeit bearbeitet werden.

In der einfachsten Ausführung handelt es sich bei einem JavaBean um eine Java-Klasse vom Typ public, die über einen parameterlosen Konstruktor verfügt. JavaBeans verfügen normalerweise über Eigenschaften, Methoden und Ereignisse, die bestimmten Namenskonventionen (auch als Design-Muster bezeichnet) unterliegen. Obwohl JavaBeans schon seit Anfang 1997 als Schlagwort durch die Presse geistern, ist die Technologie selbst noch nicht so alt. Zudem gab es bei den Beans in der Anfangsphase einige Irrungen und Wirrungen. Aber seit dem JDK 1.2 sind Beans schlüssig in Java integriert. Zu den Neuerungen gegenüber der ersten Generation zählen die bessere Unterstützung in Applikationen und Browsern, das Zusammenspiel mit Drag&Drop sowie das Activation-Framework.

16.2.1 Wie passen JavaBeans in Java?

JavaBeans bestehen im Grunde aus normalem Java-Code, sind also reine Java-Klassen. Nur der Grund, warum sie erstellt werden, wie sie verwendet werden und welche Konventionen sie einhalten, hebt sie gegenüber »normalen« Java-Klassen hervor. Sie werden sehr oft nicht auf Grund von Tipparbeit erstellt oder modifiziert, sondern mittels eines eigenen Werkzeugs - meist einer modernen visuellen IDE. Wenn man die volle Funktionalität des Beans-Konzepts nutzen will, kommt man um den Einsatz einer solchen professionellen IDE kaum herum. Aber auch die Verwendung von Beans unterscheidet sich von dem Einsatz »normaler« Java-Klassen. Statt auf Quelltextebene aus einer Java-Klasse eine Instanz zu erzeugen, wird per Drag&Drop aus einer Toolliste ein Symbol des Beans in einen Container gezogen. Damit das funktioniert, müssen Beans einige strenge Konventionen einhalten und bestimmte Eigenschaften bereitstellen, damit IDEs das leisten können. Dennoch - genau wie andere Komponententypen auch, sind JavaBeans einfach wiederverwendbare Codeteile, die aber mit minimaler Auswirkung auf den Test des Programms, dem sie hinzugefügt werden, aktualisiert werden können. Sie sind plattformübergreifende, reine Java-Komponenten, die in viele visuelle IDEs installiert und von da aus bei der Konstruktion eines Programms per Drag&Drop verwenden können.

Das Kernmodell von JavaBeans besteht im Wesentlichen aus Properties (Eigenschaften bzw. mit Namen versehene Attribute, die einer Komponente zugeordnet sind), Events (Ereignissen) und Methoden.

Von besonderem Interesse ist, dass die Eigenschaften einer so in einer Applikation integriertem Bean-Komponente im Rahmen einer geeigneten visuellen Entwicklungsumgebung mit der Maus bzw. einem Property-Sheet-Fenster verändert werden kann. In dem Property-Sheet-Fenster kann man gegebenenfalls Werte verändern, die sich dann unmittelbar auf die Eigenschaften einer Komponente auswirken.

Abbildung 16.2:  Der Inspektor - das Property-Sheet-Fenster des JBuilders

Dabei sollte beachtet werden, dass die Eigenschaften der Komponente durchaus als private deklariert sein können (und meist auch sind) und das Property-Sheet-Fenster eventuell nur öffentliche Zugriffsmethoden bereitstellt. Damit kann gezielt ein intelligenter Filtermechanismus für erlaubte Eigenschaften einer Bean-Komponente realisiert werden (Stichwort Datenkapselung).

Mithilfe der visuell abgebildeten Eigenschaften kann also das Erscheinungsbild und das Verhalten einer Komponente beeinflusst werden. Die JavaBeans-Architektur unterscheidet zwischen so genannten Indexed, Bound und Constraint Properties:

Abbildung 16.3:  Nicht alles kann für eine Eigenschaft gesetzt werden

Zu den einem Bean zugeordneten Faktoren zählen wie angedeutet auch die Ereignisse, auf die das Objekt (natürlich kann man ein Bean auch als reines Objekt betrachten) reagieren soll. Die Besonderheit von Beans ist es nun, dass das Property-Sheet-Fenster in der Regel in einem Pull-Down-Menü oder einem Registerblatt die Events anzeigt, die für die aktuelle Komponente verwendet werden können. Von besonderem Vorteil ist die Tatsache, dass die Quelltextstruktur für diese Events mit einem einfachen Klick mit der Maus generiert werden kann und »nur noch« mit Leben gefüllt werden muss.

Abbildung 16.4:  Der Inspektor des JBuilders zeigt in einem Registerblatt die erlaubten Ereignisse einer Komponente an.

Um aus einer IDE heraus diese Zusammenhänge von Quelltext-Details analysieren zu können, muss diese Eigenschaften und zur Verfügung stehende Methoden einer Bean-Komponente über einen Mechanismus herausfinden, der Introspektion genannt wird und massiv auf der Reflection-Technik aufbaut. Dabei wird der Bytecode einer Komponente - sie ist ja im Prinzip reiner Java-Source - durchleuchtet. Auf Grund dessen können die Eigenschaften, Methoden und Ereignisse der Komponente bestimmt und der IDE bereitgestellt werden. Umgekehrt wird jede in dem Property-Sheet-Fenster vorgenommene Änderung an der richtigen Stelle im Quelltext aktualisiert.

Sind JavaBeans Applets oder eigenständige Applikationen?

Weder noch. Beans sind keine Applets. Sun gibt als wesentlichen Unterschied die Möglichkeit der visuellen Erstellung und eine relativ eng ausgelegte Zielfunktionalität von Beans an. Allerdings stellt Sun ebenso fest, dass Applets so entwickelt werden können, dass sie wie Beans aussehen und arbeiten.

Auf der anderen Seite sind Beans aber auch in der Regel keine eigenständigen Anwendungen (obwohl es explizit nicht verboten ist, sie mit einer main()-Methode auszustatten). Applets und Anwendungen können jedoch aus JavaBeans modular zusammengesetzt werden. Es ist bereits seit langer Zeit Ziel der Computerwelt, mittels Komponenten eine Wiederverwendbarkeit von vorgefertigten Teilen zu gewährleisten, um komplexere Anwendungen aus leicht zu kombinierenden Einzelteilen zu erstellen. Die objektorientierte Theorie verfolgt auf abstrakterer Ebene das gleiche Ziel. Damit sind Komponenten bei konsequenter Umsetzung hervorragend in die objektorientierte Theorie integriert und eine logische Folge der objektorientierten Programmierung.

Sie bieten gleichwohl gegenüber den normalen objektorientierten Techniken einige Vorteile. Die Wiederverwendung von vielen Objekten scheitert oft daran, dass das Design nicht zu der speziell benötigten Aufgabe passt. Das Objekt ist zu speziell oder passt in einem entscheidenden Detail nicht genau. Also wird neu programmiert. Bei gutem Design bieten Komponenten als kleine Programme, die nur ganz beschränkte Aufgaben erfüllen, eine bessere Alternative. Sie sind wie kleine Bausteine, aus denen die eigentlichen Anwendungen zusammengesetzt werden.

Bei den JavaBeans steht für die Zusammenarbeit der Komponenten über so genannte Bridges eine Schnittstelle - auch zu anderen Komponenten wie ActiveX-Controls oder OLE-Komponenten - zur Verfügung.

JavaBeans contra ActiveX-Controls

Wenn man an Komponenten denkt, ist die Verbindung zu ActiveX-Controls naheliegend. Vergleichbar sind JavaBeans recht gut mit ActiveX-Controls oder OCX-Controls. Sie können wie diese visuell manipuliert werden und bieten über so genannte Bridges eine Schnittstelle zu anderen Komponenten vom gleichen Typ, jedoch auch andersartigen Typen - wie bei Beans zu ActiveX-Controls (die so genannte ActiveX-Brücke oder ActiveX-Bridge) oder OLE-Komponenten - an. Im Gegensatz zu diesen Komponenten sind JavaBeans jedoch explizit plattformunabhängig und im Sicherheitskonzept von Java integriert. Überdies gibt es grundlegende Unterschiede in der dahinter liegenden Philosophie. Die ActiveX-Technologie ist erheblich aufwändiger, weil sie ein größeres Spektrum an potenziellen Anwendungen abdeckt. JavaBeans müssen durch ihre Integration in das Java-Sicherheitskonzept auch nicht registriert werden und sind damit viel leichter und flexibler einsetzbar.

Ein weiterer Unterschied ist, dass JavaBeans-Komponenten zur Laufzeit einer Anwendung dynamisch geladen werden können und sich in ein bestehendes Modell noch zur Laufzeit einbinden. Bei ActiveX-Controls und Windows (in den meisten Versionen) muss der Rechner neu gestartet werden.

16.2.2 Beans erstellen

In der Regel erstellt man JavaBeans nicht mehr »von Hand«, sondern mit einer professionelle Java-IDE. Das heißt aber nicht, dass man nicht ohne ein solches Tool Beans erstellen könnte. Wenn man Beans erstellen möchte, basiert die Realisierung der meisten erweiterten Funktionalitäten auf den Paketen java.beans und java.beans.beancontext. Aber ein einfaches JavaBean kann sogar schon so unkompliziert aussehen:

import java.awt.*;
import javax.swing.JPanel;
public class MeinBean extends JPanel {
  BorderLayout borderLayout1 = new BorderLayout();
  // Eigenschaft des Beans - private
  private String sample = "Beispiel";
  public MeinBean() {
          initial();
      }
  private void initial()  {
    this.setLayout(borderLayout1);
  }
  // Wert der Eigenschaft abfragen
  public String getSample() {
    return sample;
  }
  // Wert der Eigenschaft setzen  
  public void setSample(String newSample) {
    sample = newSample;
  }  }

Das Bean wurde mit dem angegebenen Namen und der angegebenen Superklasse erstellt. Die Klasse ist als public deklariert und verfügt über einen parameterlosen Konstruktor. Selbst in diesem rudimentären Status handelt es sich aber um ein gültiges JavaBean.

Um das Bean nun aber laufen zu lassen, kommt man nicht um eine geeignete Testumgebung herum. Das Bean besitzt ja keine main()-Methode. Jede Java-IDE, mit der man Beans erstellen kann, bietet eine solche Laufzeit-Testumgebung für Beans. Aber Sie können auch von Sun eine kostenlose Bean-Testumgebung laden - das BDK (JavaBeans Development Kit oder kurz Beans Development Kit). Sie finden es unter http://java.sun.com/products/javabeans/software/bdk_download.html.

Abbildung 16.5:  Hier gibt es das BDK

Das BDK stellt einen Bean-Container, die »BeanBox« und einige Beispielcodes bereit. Das Anfang 2001 aktuelle BDK 1.1 basiert mindestens auf der Java-2-Standard-Edition SDK 1.2 oder höher.

Wenn man das Bean um eine sinnvolle Funktionalität erweitern will, muss man es einfach wie eine normale Java-Klasse um die Dinge ergänzen, die es tun soll. Das klingt zwar ziemlich nichtssagend, soll aber deutlich machen, dass die Unterschiede zu der Erstellung einer GUI-Applikation in der Tat nicht riesig sind. Wir könnten das Bean-Beispiel, das bisher nur ein Swing-Panel mit einem Borderlayout und einer Beispieleigenschaft beinhaltet, einfach um zwei Buttons und ein Label erweitern, das über die Buttons gefüllt und gelöscht werden kann. Was wir also tun würden, ist, wie bei GUI-Java-Applikationen gewohnt die Benutzerschnittstelle des Beans zu erweitern.

Interessanter und speziell auf Beans angepasst ist der Vorgang, wie Eigenschaften zu Beans hinzugefügt und dann einer IDE bereitgestellt werden. Diese Eigenschaften legen die Attribute fest, über die ein Bean verfügt. Diese werden wie bereits gesagt meist nicht direkt offen gelegt. JavaBeans-Eigenschaften verfügen statt dessen üblicherweise über eine Lese- und eine Schreibzugriffsmethode, auch entsprechend als Getter- und Setter-Methode bezeichnet. Eine Getter-Methode gibt den aktuellen Wert der Eigenschaft zurück. Mit einer Setter-Methode wird die Eigenschaft auf einen neuen Wert gesetzt. Wenn die Eigenschaft schreibgeschützt sein soll, gibt es keine Setter-Methode. Eine Eigenschaft eines Beans kann von jedem beliebigen Java-Datentyp sein.

Wenn Sie ein Bean in einer IDE bereitstellen wollen, kann man optional BeanInfo-Klassen zur Dokumentation im Rahmen der IDE erstellen. Sie können einige Eigenschaften verbergen, sodass sie im Property-Sheet-Fenster der IDE nicht angezeigt werden. Auf solche Eigenschaften kann weiterhin mit Programmcode zugegriffen werden, Benutzer können jedoch ihre Werte in der Entwurfsphase nicht ändern. Eine BeanInfo kann Informationen über das Icon in der Werkzeugleiste oder eine in der IDE anzuzeigene Hilfe beinhalten. Die BeanInfo basiert im Wesentlichen auf dem Paket java.beans.*.

Noch wichtiger ist es, Ereignisse zu Beans hinzufügen, damit ein JavaBean das Generieren (oder Auslösen) von Ereignissen und/oder das Senden eines Ereignisobjekts an ein Listener-Objekt auslösen kann (eine oder beide dieser Funktionalitäten sind möglich). Dazu müssen natürlich die Überwachung von Ereignissen und Reagieren auf die Ereignisse implementiert werden. Sowohl die Reaktion auf Standardereignisse als auch selbst definierte Ereignisse lassen sich implementieren. Allgemein gilt, dass eine Komponente bei einem aufgetretenen Ereignis benachrichtigt wird, wenn die passende Listener()-Methode aufgerufen wird. Eine Bean-Klasse beinhaltet darüber hinaus oft fire<event>-Methoden, die ein Ereignis an alle registrierten Listener senden. Für jede Methode in der Listener-Schnittstelle wird ein derartiges Ereignis generiert. Wenn ein Bean zum Überwachen von Ereignissen als Listener definiert werden soll, muss der zu überwachenden Ereignistyp mit der passenden Listener-Schnittstelle implementiert werden.

Schauen wir uns eine entsprechend erweiterte Bean-Klasse (zwei Eigenschaften) an, die auf die Standard-actionPerformed()-Methode (hier mit Beispiel noch leer) reagieren kann.

import javax.swing.JPanel;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class MeinBean2 extends JPanel implements ActionListener {
  BorderLayout borderLayout1 = new BorderLayout();
  // Eigenschaft des Beans - private
  private String sample = "Beispiel";
  private String farbe;
  private transient Vector actionListeners;
  public MeinBean2() {
          initial();
      }
  private void initial()  {
    this.setLayout(borderLayout1);
  }
  // Wert der Eigenschaft abfragen
  public String getSample() {
    return sample;
  }
  // Wert der Eigenschaft setzen  
  public void setSample(String newSample) {
    sample = newSample;
  }
  public void setFarbe(String newFarbe) {
    farbe = newFarbe;
  }
  public String getFarbe() {
    return farbe;
  }
  public synchronized void removeActionListener(ActionListener l) {
    if (actionListeners != null && actionListeners.contains(l)) {
      Vector v = (Vector) actionListeners.clone();
      v.removeElement(l);
      actionListeners = v;
    }
  }
  public synchronized void addActionListener(ActionListener l) {
    Vector v = actionListeners == null ? new Vector(2) : (Vector) actionListeners.clone();
    if (!v.contains(l)) {
      v.addElement(l);
      actionListeners = v;
    }
  }
  protected void fireActionPerformed(ActionEvent e) {
    if (actionListeners != null) {
      Vector listeners = actionListeners;
      int count = listeners.size();
      for (int i = 0; i < count; i++) {
        ((ActionListener) listeners.elementAt(i)).actionPerformed(e);
      }
    }
  }
  public void actionPerformed(ActionEvent e) {
  }  }

16.2.3 Spezielle Anwendungen von JavaBeans

JavaBeans werden im Allgemeinen dafür erstellt, besondere Anwendungen zusammenzufassen, damit diese dann per Mausklick bereitstehen. Dazu zählt beispielsweise die Bündelung von Datenbankfunktionalitäten in einem Bean. Dazu zählt der Zugriff auf relationale Datenbanken über das JDBC-API (Java Database Connectivity), worauf wir nachfolgend eingehen werden.

Neben der Wiederverwendung von Bausteinen und der visuellen Erstellung von Programmteilen ist ein weiterer wichtiger Aspekt bei JavaBeans die mögliche Verteilung auf verschiedene Server. Einmal entwickelte Beans werden üblicherweise in JAR-Dateien verpackt und können ggf. erst beim End-User zusammengesetzt werden.

Dies setzt natürlich ein erheblich komplexeres Architekturmodell voraus, als es bei rein lokalen Anwendungen der Fall ist. Die Object Management Group (OMG) beschreibt in ihrem Modell Object Management Architecture (OMA) eine solche Architektur verteilter Komponenten. Das CORBA-Konzept spezifiziert die dafür benötigten Dienste. Auch dieses Thema konkretisieren wir in den nächsten Abschnitten mit Beispielen.

Verteilung auf verschiedene Server heißt bei JavaBeans übrigens nicht, dass diese sich auch noch zur Laufzeit dort befinden, sondern JavaBeans verfolgen als lokales Komponentenmodell zur Laufzeit die Kommunikation der Komponenten auf der lokalen Maschine. Sie müssen also zwingend dort zusammengesetzt sein oder werden zumindest dynamisch nachgeladen und binden sich in das laufende Modell ein. Diese Einschränkungen haben den Grund, dass die Entwicklung von verteilten Komponenten zur Laufzeit ein noch komplizierteres Modell erfordert.

16.2.4 Beans einer IDE bereitstellen

Wie ein fertiges Bean einer IDE zur Verfügung gestellt wird (was eigentlich immer für einen Nutzen des Beans notwendig ist, wenn man keine main()-Methode integriert hat), kann nicht allgemeingültig gesagt werden. Jede geeignete IDE stellt dafür ein eigenes Verfahren bereit, das in der dortigen Hilfe beschrieben sein sollte. Allgemein muss aber meist die kkompilierte Datei bzw. das ganze Paket mit dem Bean über die Konfiguration der IDE hinzugefügt werden. Die meisten IDEs stellen dazu einen Assistenten bereit, der Beans aus Paketen importieren lässt. Diese tauchen dann in der Werkzeugleiste der IDE auf und lassen sich wie die Standard-Beans per Drag&Drop einsetzen.

16.3 Verteilte Systeme - RMI, IDL und CORBA

Mit den Schlagworten RMI, IDL und CORBA bewegen wir uns zu dem Bereich der verteilten Systeme. Um diese einleitend etwas zu erläutern, beginnen wir mit dem Ursprung - den monolithischen Systemen und Großrechnern. In solchen Systemen waren (und sind immer noch) Geschäftslogik, Benutzeroberfläche und Funktionalität in einer einzigen großen Anwendung enthalten. Etwa ein Datenbanksystem, auf das nur per dummer Terminals zugegriffen wird. Die erste Weiterentwicklung dieser Technologie war die (später zweischichtig genannte) Client-Server-Architektur, mit der ein Teil der Aufgaben einer Anwendung zu einem intelligenten Client (etwa einem PC) verlagert werden konnte. Client-Server-Anwendungen sind in der Regel so konzipiert, dass der Client auf jeden Fall die Benutzeroberfläche beinhaltet. Die eigentliche Funktionalität bleibt beim Server. Die Geschäftslogik wird mal hier, mal da geführt und bildet mit der jeweiligen Schicht eine Einheit. Ein weiterer Fortschritt war die mehrschichtige Client-Server-Architektur, die in der Regel drei (im Prinzip aber beliebig viele) Schichten aufweist. Die drei beschriebenen logischen Bestandteile einer Applikation (Geschäftlogik, Benutzeroberfläche und Funktionalität) werden in logische Schichten aufgeteilt. Der nächste logische Schritt war die Auflösung der strengen Client-Server-Abhängigkeiten. Dabei wird die gesamte Funktionalität einer Anwendung in Form von Objekten dargestellt. Von der Anzahl her sind das meist noch mehr logische Einheiten als bei mehrschichtigen Client-Server-Applikationen. Diese Objekte sind weder vom Ort her gebunden (im Rahmen der technischen Möglichkeiten natürlich doch), noch liegt eindeutig fest, dass ein Objekt nur Server und eines nur Client ist. Die Unterschiede verschwimmen. Ein solches System nennt man dann ein verteiltes System.

Ein ganz großer Fortschritt im Bereich der Realisierung von verteilten Programmierung ist das RMI-Modell, auf das unter anderem auch Java-Applikationen (sowohl eigenständige Applikationen, aber auch Applets und Beans) zugreifen können. Das Remote Method Invocation Interface (RMI) bietet die Möglichkeit, Java-Klassen, die auf einer anderen virtuellen Maschine laufen, anzusprechen. Dabei ist es egal, ob die virtuelle Maschine lokal vorhanden oder irgendwo im Internet ausgeführt wird. RMI stellt ein API zur Verfügung, mit dessen Hilfe die besagte Kommunikation zweier Komponenten über Adress- oder Maschinenräume hinweg möglich ist. RMI ist eine Art objektorientierter RPC-Mechanismus (Remote Procedure Call), der speziell für Java entwickelt wurde. Das heißt aber auch, dass RMI nur genutzt werden kann, wenn sowohl das Server- als auch das Client-Objekt in Java implementiert sind. Deswegen wird auch keine spezielle Beschreibungssprache benutzt, um das entfernte Interface zu beschreiben. In Java werden im SDK 2 zur Umsetzung des RMI-Konzeptes im Wesentlichen fünf Packages verwendet:

java.rmi 
java.rmi.dgc 
java.rmi.registry 
java.rmi.server 
java.rmi.activation

Zwei in der JDK-Version 1.1.x eingeführte Programme, rmic - der Java RMI Stub Compiler - und rmiregistry (Java Remote Object Registry) dienen zur programmiertechnischen Umsetzung des RMI-Konzeptes. Neu im JDK 1.2 wurde das Tool rmid - Java RMI Activation System Daemon - hinzugenommen.

In diesem Zusammenhang wurde auch das Konzept der oben bereits beschriebenen Object Serialization aufgenommen, um darüber Inhalte eines Objekts in einen Stream zu speichern und wieder zu reproduzieren. Dies ist bei der Versendung von Objekt-Inhalten zwischen verteilten Client-Server-Beziehung in der Regel notwendig. Das Java-Package java.io beinhaltet die entsprechenden Erweiterungen. serialver heißt das Object Serialization Tool für Java.

Nun ist aber RMI nicht die einzige Möglichkeit, verteilte Systeme aufzubauen, auch nicht unter Java. Da gibt es einmal die Möglichkeit der so genannten Socket-Programmierung. Dies ist ein Kanal, über den Anwendungen oder Teile davon (Threads) miteinander verbunden sind und kommunzieren können. Eine Abart der Socketkommunikation ist der so genannte Remote-Prozedurenaufru> (RPC - Remote Procedure Call), wo die Socket-Kommunikation nicht direkt, sondern über eine darüber liegende Schnittstelle realisiert wird. Socket-Programmierung ist im Allgemeinen recht tief an der Hardware angesiedelt und aufwändig zu realisieren. Wir demonstrieren sie dennoch mit Beispielen etwas weiter unten im Rahmen des Netzwerkabschnitts. Weitere Standards für die verteilte Programmierung sind der DCE-Standard (Distributed Computing Environmen>) und das - weitgehend auf die Windows-Plattform beschränkte2 - DCOM (Distributed Component Object Model) von Microsoft. Und last but not least CORBA, worauf wir neben der Socket-Kommunikation als einzige Technik genauer eingehen. Der Grund ist, dass CORBA wohl die universellste Technik ist. Dazu relativ leicht einzusetzen, flexibel und stabil. Das macht CORBA und seine Basis-Beschreibungssprache IDL - trotz RMI - auch für verteilte Java-Applikationen zu einem idealen Partner. Zumal man dann auch verteilte Systeme erstellen kann, die nicht nur Java einsetzen.

16.3.1 Java IDL oder wie ist RMI in Bezug auf CORBA zu sehen?

RMI und CORBA werden über IIOP (Internet InterORB Protocol) zusammengebracht. Eine logische Fortsetzung der RMI-Entwicklung ist die Java IDL (Interfaces Definition Language), die es Java ermöglicht, eine Verbindung zu anderen Verteilungsplattformen, wie zum Beispiel CORBA (Common Object Request Broker Architecture), aufzubauen. Dies bedeutet, entfernte Schnittstellen über IDL zu definieren. IDL ist eine Definitionssprache, die die Kommunikation zwischen verschiedenen Programmiersprachen über Schnittstellen ermöglicht und in das CORBA-Konzept integriert. Sie ist eine objektorientierte Sprache, die aber nur der Definition von Schnittstellen dient und nicht irgendwelchen konkreten Implementierungen. Dazu gilt, dass IDL sprachenunabhängig ist und zur Sprachabbildung (Language Mapping) für die unterschiedlichsten Sprachen (etwa Java, C/C++, Smalltalk oder Cobol) verwendet werden kann. Die Syntax von IDL ähnelt stark der von Java und auch die anderen Sprachmerkmale sind verwandt und für Java-Programmierer leicht zu lesen. IDL

  • unterscheidet Groß- und Kleinschreibung,
  • bildet Blöcke mittels geschweifter Klammen,
  • beendet Anweisungen mit einem Semikolon,
  • ist polymorph,
  • kennt Exceptions,
  • benötigt keine Speicherverwaltungsbefehle,
  • kann Schnittstellen erweitern (über Doppelpunkt und Nachstellen der Schnittstelle),
  • deklariert Methoden ähnlich wie Java in Schnittstellen,
  • hat ähnliche Datentypen wie Java (aber alle klein geschrieben, auch string)
  • und kennt die traditionellen Java-Kommentare.

Viele Schlüsselwörter von IDL kommen auch in Java vor. Mittels IDL werden Module erstellt (eingeleitet mit dem Schlüsselwort module), in denen Schnittstellendeklarationen (Schlüsselwort interface) festgelegt werden. Module entsprechen den Paketen unter Java, und insbesondere die Schnittstellenphilosophie von Java und IDL respektive CORBA ist nahezu deckungsgleich.

Der neben IDL zweite wichtige Eckpfeiler von CORBA ist der so genannte ORB (Object Request Broker). Das SDK 2 enthält einen ORB, der es erlaubt, verteilte Anwendungen auf der Basis von Java und CORBA zu schreiben. Kern der Realisierung davon sind alle Elemente, die in den mit org.omg.CORBA beginnenden Paketen enthalten sind. Ein solcher ORB basiert auf folgendem Konzept:

Wenn eine Anwendungskomponente einen Remote-Dienst verwenden will, muss sie darauf eine Objektreferenz erlangen. Besonders, wenn sich das Remote-Objekt auf einem entfernten Rechner befindet, aber auch über Namensräume auf dem gleichen Rechner hinweg. Solche Objektreferenzen werden als IOR (Interoperable Object References) bezeichnet, wenn sie das IIOP verwenden. In CORBA erfolgt die gesamte Kommunikation zwischen den Objekten über diese IOR. Das hat zur Folge, dass zwischen Objekten nur Referenzen weitergegeben werden (Pass by Reference). Über diese kann die Anwendungskomponente (der CORBA-Client) dann die Methoden und Eigenschaften des entfernten Objekts (des CORBA-Servers) nutzen. Dabei sollte beachtet werden, dass unter CORBA jede Komponente als Server betrachtet wird, wenn sie CORBA-Objekte enthält, die anderen Objekten Dienste bereitstellen. Analog ist ein CORBA-Client eine Komponente, die auf einen Dienst eines anderen CORBA-Objekts zugreift. Eine CORBA kann also zur gleichen Zeit sowohl als Server als auch als Client agieren, je nach Sichtweise (ein einleitend schon beschriebenes Charakteristikum von verteilten Systemen).

Die Hauptaufgabe des ORB ist es nun, die Auflösung der Objektreferenzen über beliebige (!) Verbindungen hinweg zu gewährleisten. Dabei wird es unter Umständen notwendig sein, Parameter eines entfernten Methodenaufrufs in ein Format zu wandeln, das über ein Netzwerk zum Remote-Objekt übertragen werden kann (so genanntes Marshalling) und natürlich auch dessen Rückwandlung (Unmarshalling). Ein Client ruft einfach die gewünschte Remote-Methode auf. Für den Client erscheint sie als lokale Methode. Dies gewährleistet, dass die gesamte Formatübertragung und Kommunikation plattformunabhängig stattfindet. Auch dies gehört zu den Leistungen, die ein ORB bietet. Verschiedene ORBs auf verteilten Plattformen (das können physikalisch getrennte Rechner oder auch nur verschiedene Shells auf einem Rechner sein) kommunizieren nun auf hoher Ebene (etwa auf TCP/IP aufsetzend) von Netzwerkprotokollen über das GIOP (General Inter-ORB Protocol). Es handelt sich aber wie gesagt um ein sehr allgemeines Protokoll, das durch andere Protokolle ergänzt wird.

Wo ist nun der Unterschied zwischen RMI und IDL bzw. CORBA genau zu sehen? Während RMI eine reine Java-Lösung darstellt, ist CORBA eine von der Java-Sprache vollkommen losgelöste Lösung für verteilte Strukturen. IDL ist dazu eine neutrale Beschreibungssprache. CORBA ist insbesondere sehr robust und kann zusammen mit Java eingesetzt werden, muss es aber nicht. Die OMG verabschiedete vor einiger Zeit ein IDL/Java-Mapping, das die Abbildung der Interface Definition Language (IDL) auf die Java-Sprache definiert.

Die sprachunabhängige Definition von Komponenten und die plattformübergreifende Kommunikation ist in einer unternehmensweiten Architektur ein unbedingtes Muss. CORBA ist zurzeit die einzige Architektur, die beide Anforderungen erfüllt.

16.3.2 Entwicklung einer CORBA-Anwendung in Java

Die Entwicklung einer CORBA-Applikation, die auf Java basiert, kann in einige immer wiederkehrende Schritte gegliedert werden.

Generierung der IDL-Schnittstelle

Der erste Schritt besteht darin, eine oder mehrere IDL-Schnittstelle(n) zu generieren, etwa so wie in dem nachstehenden einfachen Beispiel (Dateiname Hallo.idl) mit der Definition einer Schnittstelle. Diese stellt eine Methode bereit, die einen String zurückgibt:

module HalloApp 
{
    interface Hallo
    {
        string sagHallo();
    };
};

Anwendung eines IDL-Compilers

Im zweiten Schritt wird der IDL-Quelltext mit einem IDL-Compiler übersetzt. Das SDK 2 stellt im Rahmen des JDK 1.3 einen solchen IDL-Compiler bereit. Alternativ können Sie IDL-Compiler wie VisiBroker oder jeden anderen geeigneten verwenden (auch von älteren JDK-Versionen, sofern es nicht zu Inkompatibilitäten in den resultierenden Dateien kommt). Wenn Sie den IDL-Compiler des SDK 2 idlj wie folgt verwenden, werden Ihnen nach der Anwendung unter anderem dann vorgefertigte Client-Stubs und Server-Skeletons als Schablonen bereitstehen:

idlj -fclient -fserver Hallo.idl

Die Angabe von Optionen ist in dem IDL-Compiler des JDK 1.3 nicht unbedingt notwendig. Ohne diese Optionen werden keine Skeletons erstellt. Beachten Sie aber, dass sowohl die Online-Dokumentation als auch die direkte Hilfe zu dem IDL-Compiler des SDK Inkonsistenzen aufweisen und diese Optionen nicht unbedingt angeben. Wenn der Aufruf auch ohne die Optionen funktioniert und alle nachstehenden Dateien erzeugt werden - umso besser.

Beachten Sie, dass das IDL-Konvertierungtool im SDK lange Zeit idltojava hieß und in diversen Quellen immer noch so geführt wird. Auch Namen wie idl2java oder ähnliche Namen, die mit idl beginnen, sind gebräuchlich, wenn es IDL-Compiler sind, die nicht zum JDK gehören. Die konkrete Anwendung ist natürlich nicht zwingend identisch mit idlj und muss der jeweiligen Dokumentation entnommen werden. Allgemein gilt aber, dass die Arbeit der IDL-Compiler nicht immer einwandfrei ist, die Dokumentationen nicht ganz fehlerfrei sind und es zu Komplikationen kommen kann.

Ein Client-Stub ist ein Java-Quelltextteil, der einem Client erlaubt, auf eine Server-Komponente zuzugreifen. Dieser Quelltextteil wird zusammen mit dem eigentlichen Client-Anteil einer Anwendung kompiliert. Stubs dienen dazu, den ORB des Clients anzuweisen, die Formatübertragung für abgehende und ankommende Parameter durchzuführen. Analog handelt es sich beim Server-Skeleton um einen Quelltextteil um das Gegenstück auf Serverseite, das einem Client Funktionalität bereitstellt. Sie bilden das Gerüst eines Servers und übergeben ankommende Parameter an den von dem Entwickler geschriebenen Implementierungsquelltext. Zusätzlich werden abgeleitete Parameter wieder an den Client zurückgeleitet.

Aus der oben aufgeführten IDL-Quelltextdatei wird ein Unterverzeichnis mit dem Namen des IDL-Moduls (nicht dem Namen der IDL-Datei - eine weitere Verwandtschaft zu Java) generiert (also HalloApp). Dieses enthält nach der Kompilierung die folgenden Dateien:

Hallo.java
HalloHelper.java
HalloHolder.java
HalloOperations.java
_HalloImplBase.java
_HalloStub.java

Bei diesen Java-Dateien handelt es sich um besagte Stubs und Skeletons sowie einige Hilfsklassen:

  • Die Datei _HalloImplBase.java beinhaltet eine von der Klasse org.omg.CORBA.portable.ObjectImpl abgeleitete abstrakte Klasse, die das Server-Skeleton darstellt. Die Klasse implementiert das Hallo.java-Interface und die Standard-CORBA-Schnittstelle org.omg.CORBA. portable.InvokeHandler. Die nachfolgend noch zu erstellende Serverklasse HalloServant erweitert dann _HalloImplBase.
  • Die Datei _HalloStub.java beinhaltet eine von der Klasse org.omg.CORBA.portable.ObjectImpl abgeleitete Klasse, die CORBA-Funktionalität für den Client beinhaltet. Sie implementiert das Hallo.java-Interface.
  • Die Datei Hallo.java ist eine Schnittstelle, die die Java-Version von dem IDL-Interface beinhaltet. Sie ist von HalloOperations, org.omg.CORBA.Object und org.omg.CORBA.portable.IDLEntity abgeleitet. In älteren Versionen des JDK beinhaltete diese Schnittstelle direkt die Methode sagHallo(). Diese Deklaration ist im aktuellen SDK 2 in die Datei HalloOperations.java verlagert, die - wie oben angegeben - im-plementiert wird.
  • Die Datei HalloHelper.java beinhaltet eine finale Klasse mit Hilfsfunktionalitäten - etwa zum Casten von CORBA-Objekten.
  • Die Datei HalloHolder.java beinhaltet eine weitere finale Klasse mit einem öffentlichen Instanzmember vom Typ Hallo. Sie dient der Unterstützung von Ein- und Ausgabeargumenten, die nicht exakt zwischen CORBA und Java übereinstimmen. Die Klasse implementiert org.omg.CORBA.portable.Streamable.
  • Die Datei HalloOperations.java beinhaltet das Interface HalloOperations, das nur die Methode String sagHallo() bereitstellt.

Nun ist alles vorbereitet, um konkret die Implementierung des Servers und des Clients vorzunehmen. Wir wollen die Dateien HalloServer.java und HalloClient.java nennen und halten uns an die Beispiele, wie sie Sun auch ähnlich in der Online-Dokumentation bzw. als Demo bereitstellt. Die Java-Dateien werden wie die automatisch generierten Dateien in das Verzeichnis HalloApp gestellt.

Implementierung des Servers

Der Server soll zwei Klassen besitzen, zum einen HalloServant, die die abstrakte Klasse _HalloImplBase vervollständigt. Die eigentliche Server-Klasse HalloServer beinhaltet die main()-Methode, die Folgendes leisten soll:

  • Eine ORB-Instanz kreieren.
  • Eine Instanz von HalloServant kreieren und beim ORB anmelden.
  • Eine CORBA-Objektreferenz entgegennehmen für einen benannten Kontext, in welchem das neue CORBA-Objekt registriert wird.
  • Das neue Objekt registieren in dem benannten Kontext unter dem Namen »Hallo«.
  • Auf Aufrufe des neuen Objekts warten.
package HalloApp;
import HalloApp.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;
class HalloServant extends _HalloImplBase 
{
 public String sagHallo()
 {
 return "\nIch tue nie, was mir befohlen wird!\n";
 }  }
public class HalloServer { 
 public static void main(String args[]) {
 try{
 // Kreieren und Initialisieren des ORB
 ORB orb = ORB.init(args, null);
 // Kreieren des Servants und Registrieren beim 
 // ORB
 HalloServant halloRef = new HalloServant();
 orb.connect(halloRef);
 // Erhalten des Rootnamingcontext
 org.omg.CORBA.Object objRef = 
 orb.resolve_initial_references("NameService");
 NamingContext ncRef = NamingContextHelper.narrow(objRef);
  // Binden der Objekt-Referenz
 NameComponent nc = new NameComponent("Hallo", "");
 NameComponent path[] = {nc};
 ncRef.rebind(path, halloRef);
 // Auf Aufrufe von Clients warten
 java.lang.Object sync = new java.lang.Object();
 synchronized (sync) {
 sync.wait();
 }
 } catch (Exception e) {
 System.err.println("ERROR: " + e);
 e.printStackTrace(System.out);
 }
 }  }

Implementierung des Clients

Die Implementing des Clients soll Folgendes leisten:

  • Kreieren eines ORB
  • Erhalten einer Referenz auf den benannten Kontext
  • Suchen nach »Hallo« in den benannten Kontext und entgegennehmen einer Referenz auf das CORBA-Objekt
  • Aufrufen der von dem Objekt bereitgestellten Methode sagHallo() und Ausgabe des Resultats
package HalloApp;
import HalloApp.*;
import org.omg.CosNaming.*;
import org.omg.CORBA.*;
public class HalloClient {
 public static void main(String args[]) {
 try{
 // Kreieren und Initialisieren des ORB
 ORB orb = ORB.init(args, null);
  // Erhalten des Rootnamingcontext
 org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");
 NamingContext ncRef = NamingContextHelper.narrow(objRef);
 // Erhalten einer Referenz auf den benannten 
 // Kontext
 NameComponent nc = new NameComponent("Hallo", "");
 NameComponent path[] = {nc};
 Hallo halloRef = HalloHelper.narrow(ncRef.resolve(path));
  // Aufruf der Serverobjekte und Ausgabe des 
  // Resultats
 String ups = halloRef.sagHallo();
 System.out.println(ups);
 } catch (Exception e) {
 System.out.println("ERROR : " + e) ;
 e.printStackTrace(System.out);
 }
 }  }

Erstellen und Laufenlassen

Der abschließende Schritt besteht nun darin, alle Java-Dateien - sowohl die vom IDL-Compiler generierten als auch die selbst erstellten - zu kompilieren. Dies erfolgt von außerhalb des HalloApp-Verzeichnisses (wo sich alle Java-Dateien befinden sollten) mit der folgenden Syntax:

javac *.java HalloApp/*.java

Im nächsten Schritt brauchen Sie ein weiteres Tool des JDK: tnameserv. Dieses stellt einen Nameserver bereit, der gestartet werden muss. Dazu müssen Sie einen weitgehend beliebigen Port angegeben (möglichst jenseits von 1024). Um etwa den Nameserver mit Port 2000 zu starten, genügt folgender Aufruf:

tnameserv -ORBInitialPort 2000

Abbildung 16.6:  Der Nameserver ist gestartet.

Im nächsten Schritt wird der CORBA-Server gestartet. Dies erfolgt beispielsweise in einer zweiten DOS-Box bzw. Shell-Umgebung mit Angabe der Server-Applikation und des Ports. Etwa so, wenn Sie direkt außerhalb des Unterverzeichnisses HalloApp stehen:

java HalloApp/HalloServer -ORBInitialPort 2000

Abbildung 16.7:  Der CORBA-Server ist gestartet, obwohl man nicht viel sieht.

Als letzten Schritt lassen wir analog dem CORBA-Server den CORBA-Client laufen, ebenfalls wieder aus einer anderen Shell-Umgebung:

java HalloApp/HalloClient -ORBInitialPort 2000

Abbildung 16.8:  Manchmal tut der CORBA-Client  doch, was ihm gesagt wird.

Das Beispiel ist - wenn es auf einem Rechner, nur durch verschiedene Shells getrennt, gestartet wird - nicht besonders eindrucksvoll. Sie sollten aber bedenken, dass Client und Server auch beliebig in einem Netzwerk verteilt sein können. Diese Netzwerkfähigkeit von Java wollen wir nun näher betrachten.

16.4 Netzwerkzugriffe, Sockets und Servlets

Durch seine Internet-Orientierung ist es für Java zwingend, Netzwerkfähigkeiten mitzubringen. Damit ist erst einmal nur gemeint, dass es möglich sein muss, mit einem Applet oder einer Java-Applikation über ein Computernetzwerk eine Verbindung zu einem anderen System aufzubauen. Den Fall von beliebig in einem Netzwerk verteilten Objekten, die mittels CORBA oder anderer Standards kommunizieren, haben wir als Spezialfall ja gerade behandelt.

Das Thema Netzwerke gehört auf jeden Fall zur hohen Schule der EDV und die Programmierung von Netzwerkprogrammen setzt dem noch eins drauf. Wir werden uns dennoch an das Thema wagen und versuchen, Licht in die Netzwerkwelt zu bringen. Schauen wir uns zunächst Servlets an.

16.4.1 Java-Servlets, JSP und SSI

Java-Servlets gehören zu den wichtigsten Entwicklungen von Java. Grob gesagt geht es darum, in einer Client-Server-Beziehung (z.B. der Zusammenarbeit von Browser und Server im Internet) einige Arbeiten vom Client auf einen in Java geschriebenen Server bzw. ein dort laufendes Java-Modul zu verlagern. Dafür gibt es mehrere Gründe.

In traditionellen Client-Server-Beziehungen wird vorwiegend ein so genanntes 2-Tier-System verwendet (Tier = Schicht). Wir haben es bei der Einleitung zu dem Thema angesprochen. Ein Serverprogramm hat dabei hauptsächlich die Aufgabe, die Daten zu speichern und zu verwalten und bei Bedarf dem Clientprogramm zur Verfügung zu stellen. Die eigentliche Anwendung oder zumindest die Benutzeroberfläche ist vom Serverprogramm getrennt und läuft vollständig auf dem Client. Das ist auch sinnvoll so. Individuelle Logik befindet sich so beim Client, allgemeine Funktionalität befindet sich auf dem Server. Allerdings enthalten normale Anwendungen mehr Funktionalität und Logik, als für jeden Client individuell notwendig ist. Viele Bereiche einer Anwendung können zu der allgemeinen Funktionalität gezählt werden. Ebenso gibt es Vorgänge, die nur zentral auf einem Server erfasst werden können (beispielsweise ein Gästebuch für Webseiten). Es erscheint sinnvoll, diese allgemeine Funktionalität ebenfalls auf den Server zu verlagern.

Dieses Verfahren wird 3-Tier-Architektur genannt bzw. allgemein Multi-Tier-Architektur (besonders, wenn es sich um noch mehr Schichten handelt). Schicht 1 ist wie gehabt die Datenschicht. Die neue zweite Schicht ist ein so genannter Applikations-Server und realisiert die allgemeine Funktionalität in dem System. Schicht 3 ist wie bisher die individuelle Client-Funktionalität, allerdings um die allgemeine Funktionalität abgespeckt. Meist reduziert sich die individuelle Funktionalität auf eine reine Präsentationsschicht.

Ein solcher in Schicht 2 realisierter Applikations-Server kann als eine Art Verbindungsoffizier zwischen Schicht 1 und 3 interpretiert werden. Er vermittelt zwischen dem Datenbestand und der Präsentation und kann den Datenverkehr in einem Netzwerk erheblich reduzieren.

Servlets bieten Web-Entwicklern einen einfachen, einheitlichen Mechanismus, mit dem die Funktionalität eines Webservers erweitert und auf vorhandene Systeme zugegriffen werden kann. Durch Servlets und deren dynamische Möglichkeiten sind viele Web-Anwendungen erst möglich geworden. Servlets und darauf aufbauende Techniken bieten eine komponentenbasierte, plattformunabhängige Technik für die Erstellung von WWW-Anwendungen. Damit stehen sie in Konkurrenz zu den vor Java meist verwendeten (und auch heute noch zu findenden) CGI-Anwendungen (Common Gateway Interface - Allgemeine Zugangsschnittstelle) und auch proprietären Server-Erweiterungsmechanismen wie NSAPI (Netscape Server API) oder Apache-Modulen. Servlets sind aber leistungsfähiger als CG>-Anwendungen und dennoch explizit server- und plattformunabhängig. >

Kurzer CGI-Exkurs

Da Servlets im Wesentlichen geschaffen wurden, um Vorgänge von einem CGI-Prozess zu optimieren, soll CGI hier kurz erläutert werden. Dessen Grundvorgänge bilden die Basis für den Umgang mit Servlets.

CGI ist eine Schnittstelle für einen Client, über die die Übergabe von Parameterwerten an Programme oder Scripte auf einem Server erfolgen kann. Dort werden die übergebenen Werte verarbeitet und bei Bedarf eine Antwort (meist ein HTML-Dokument) an den Client generiert. Festhalten muss man bei CGI immer wieder, dass es sich nicht um eine Programmiersprache handelt, sondern nur um ein Protokoll, dessen wesentliches Charakteristikum die plattformübergreifende Standardisierung ist. CGI-Scripte können in jeder Sprache erstellt werden, die Scripte generieren kann und für die auf dem jeweiligen Server ein passender Interpreter vorhanden ist. Die meistverbreitete Sprache zur Erstellung von CGI-Scripten ist Perl (die Sprachspezifikation ist unter http://www.perl.com zu finden).

Die grundlegenden Schritte, die bei der Verwendung eines CGI-Sciptes ablaufen, sind folgende:

  • Aktionen, die auf dem Client ausgelöst werden, müssen ungeprüft zum Server zurückgeschickt werden.
  • Auf dem Server werden sie verarbeitet.
  • Anschließend wird das daraus ermittelte Ergebnis als Antwort zum Client zurückgeschickt.

Ein typischer CGI-Vorgang lässt sich in vier Stufen einteilen:

  • Übertragung einer Client-Anforderung an den Webserver
  • Start des CGI-Programms auf dem Server
  • Lauf des CGI-Programms auf dem Server
  • Senden der Antwort an den Client

Wenn beispielsweise ein HTML-Formular durch den Client abgeschickt wird, findet die Übertragung an den Webserver statt (der so genannte Request). Die meisten CGI-Scripte verwenden für den CGI-Requests das HTTP-Protokoll. Diese HTTP-Request-Header werden i.d.R. als Umgebungsvariablen übertragen. Im Formular kann man über zwei Angaben - der Angabe action für den URL des Webservers samt vollständiger Pfad- und CGI-Programmangabe und method für die Art und Weise der Datenübertragung - den genauen Ablauf des Vorgangs spezifizieren.

Bevor die Daten an den Server gesendet werden, werden sie zunächst vom Browser zu einer einzigen Zeichenkette verpackt. Die Art der Datenübertragung wird durch den Parameter method gesteuert. Dabei wird grundsätzlich zwischen GET und POST unterschieden. Der Unterschied zwischen den beiden Methoden besteht hauptsächlich darin, wie der Server das zurückgeschickte Formular speichert und verarbeitet. Bei GET wird das zurückgeschickte Formular in der Standard-Umgebungsvariablen QUERY_STRING gespeichert. Das CGI-Programm/Script wertet den Inhalt dieser Umgebungsvariablen aus. Danach wird der restliche Inhalt weiterverarbeitet. POST veranlasst das CGI-Programm, das Formular wie eine auf der Kommandozeilenebene erfolgte Benutzereingabe zu behandeln. Es gibt daher kein EndOfFile-Signal (EOF) und die Länge der übermittelten Daten muss vom CGI-Programm aus einer weiteren Standard-Umgebungsvariable (CONTENT_LENGTH) entnommen werden.

Wenn bei einem Formular die Eingaben per GET zum Server geschickt werden sollen, wird aus der im <FORM>-Tag angegebenen URL des CGI-Programms/Scripts und der aus den Eingabedaten erzeugten Zeichenkette, getrennt durch ein »?«, eine Pseudo-URL erzeugt. Die Pseudo-URL beinhaltet die Zeichen der Benutzereingabe nicht unverändert, sondern bei der Generierung wird der Prozess der URL-Kodierung durchlaufen. Diese URL-Kodierung ersetzt alle Leerzeichen in dem String, der aus den Benutzereingaben zusammengesetzt wird, durch Pluszeichen. Zusätzlich werden sämtliche reservierte Zeichen, die der Benutzer im Formular eingegeben hat (etwa das Gleichheitszeichen oder das kaufmännische Und), in hexadezimale Äquivalente konvertiert. Ein so konvertiertes Zeichen wird jeweils mit dem Prozentzeichen eingeleitet. Danach folgt der Hexadezimalcode.

Wenn die Daten aus einem Formular an ein CGI-Script übermittelt werden, kommen alle Daten als ein Satz von Name-Wert-Paaren an. Der Name ist jeweils der, der in dem entsprechenden Tag auf der HTML-Seite festgelegt wurde. Die Werte sind das, was der User eingetragen oder ausgewählt hat. Dieser Satz von Name-Wert-Paaren wird in einem langen String übermittelt, den das CGI-Script wieder auflösen muss. Ein solcher String ist ungefähr so aufgebaut:

name1=wert1&name2;=wert2&name3;=wert3

Der String muss beim kaufmännischen UND (&) und dem Gleichheitszeichen (=) in einzelne Stücke geteilt werden. Die entstandenen Teilstücke müssen dann noch weiter verarbeitet werden.

Der Start des CGI-Programms/Scripts auf dem Server erfolgt automatisch, da der Server auf Grund der URL erkennt, dass nicht einfach eine Webseite an den Client zurückgesendet, sondern ein CGI-Programm/Script gestartet werden soll. Die dazu notwendigen Daten (im Fall eines Formulars die Benutzereingaben) werden aus den oben spezifizierten Umgebungsvariablen der Pseudo-URL ausgelesen.

Der Lauf des CGI-Programms/Scripts sieht in der Regel wie folgt aus:

  • Zuerst muss das CGI-Programm/Script die empfangene Zeichenkette entschlüsseln. Dies bedeutet, die Umgebungsvariablen werden ausgelesen. Der weitere Programmablauf wird durch die so erhaltenen Parameter gesteuert.
  • Die aus dem eigentlichen Inhaltsstring erhalte Zeichenkette wird an den definierten Trennzeichen in die einzelnen Werte zerlegt.
  • Der eigentliche Programmablauf wird gestartet.

Das Senden der Rückantwort (Response) an den Client hat nichts mehr mit CGI speziell zu tun und unterscheidet sich nicht sonderlich von dem Senden einer normalen Webseite an den Client. Gewöhnlich wird das Ergebnis über den Webserver an den Client gesendet. Das muss aber nicht sein. Es gibt sowohl so genannte geparste und nicht-geparste Antwort-Header.

Ein geparster Header ist noch nicht vollständig und muss vom Webserver vervollständigt werden. Dazu muss die Antwort von der CGI-Anwendung ausgewertet und der Header ergänzt werden, bevor der Header an den Client zurück geht. Insbesondere muss einem geparsten Header (ebenso einem nicht-geparsten Header) immer mindestens eine Leerzeile folgen und ein Teil des geparsten Headers muss eine so genannte Server-Direktive sein. Dies sind spezielle Befehle für den Webserver, die er bei der Auswertung des Antwort-Headers berücksichtigt. Besonders wichtig sind die Server-Direktiven Content-type (meist Content-type: text/html), die den MIME-Typ der zurückgegebenen Daten angibt, Location, die den URL angibt, wo der Browser das Ergebnis anzeigen soll und Status mit dem HTTP-Statuscode. Aus den restlichen HTTP-Antwort-Header-Feldern, die eine CGI-Anwendung als Teil des geparsten Headers zurückgeben, wird der endgültige HTTP-Antwort-Header an den Client zusammengesetzt.

Ein nicht-geparster Header ist bereits vollständig aus der CGI-Anwendung erzeugt worden und wird vom Webserver nicht nochmal angepackt.

Um CGI-Scripte oder allgemein HTTP-Prozesse richtig zu verstehen, ist eine Kenntnis der Umgebungsvariablen, die der Webserver bereitstellt, nahezu unumgänglich. Zahlreiche Informationen über sich selbst und über den vom Client empfangenen Request stellt der Server in Form dieser Environment-Variablen bereit. Die nachfolgenden Umgebungsvariablen stehen unter CGI unabhängig vom jeweiligen Server stets zur Verfügung:

Umgebungsvariable Beschreibung
AUTH_TYPE Falls das Script geschützt ist, kann hier die verwendende Authentisierungsmethode angegeben werden.
CONTENT_LENGTH Angabe der Länge der auf der Standardeingabe verfügbaren Daten in Bytes bei POST. Bei GET ist die Variable leer.
CONTENT_TYPE Typ einer Requestanfrage. Format ist ein MIME-Typ. Beispiel: application/x-www-form-urlencoded
GATEWAY_INTERFACE Version der CGI-Spezifikation, die dieser Server unterstützt. Format: CGI/Version, Beispiel: CGI/1.1
HTTP_ACCEPT Liste der Dateitypen des gesendeten Objekts, die der das CGI-Script aufrufende Browser verarbeiten kann. Die Liste ist ein Sammlung von MIME-Typen nach dem Schema Kategorie/Unterkategorie. Die Kategorie legt den Typ der Datei fest, die Unterkategorie die Datei-erweiterung. Beispiel: image/gif, image/jpeg
HTTP_REFERER Der URL, woher das Script aufgerufen wurde.
HTTP_USER_AGENT Allgemeine Informationen über das Clientprogramm.
PATH_INFO Zusatzinformationen zum Pfad des CGI-Verzeichnisses, relativ zum Rootverzeichnis des Webservers. Die Umgebungsvariable enthält jedoch keine Information darüber, an welcher Stelle im Filesystem diese Relativangabe zu finden ist. Beispiel: /CGI/CGI/
PATH_TRANSLATED Absolute Pfadangabe des CGI-Verzeichnisses. Der Server leitet den Wert von PATH_INFO durch sein Mapping-System ab und bestimmt damit den absoluten Pfad.
QUERY_STRING Zeichenkette aus einem Web-Formular bei Verwendung von GET. Format ist der MIME-Typ application/x-www-form-urlencoded.
REMOTE_ADDR IP-Adresse des Webservers, wenn dieser ein CGI-Script von einem anderen Webserver laden muss. Der Wert wird nicht von allen Clients gesetzt.
REMOTE_HOST DNS-Name des Webservers, von dem ein Request stammt, wenn dieser ein CGI-Script von einem anderen Webserver laden muss. Der Wert wird nicht von allen Clients gesetzt. Zudem kann es vorkommen, dass der Server diese Information nicht besitzt. Für diesen Fall gibt es die Variable REMOTE_ADDR.
REMOTE_IDENT Benutzeridentifikation nach dem RFC 931 Authentisierungs-System. Diese Protokollinformationen können genutzt werden, wenn auf dem Webserver das entsprechende Protokoll (ident) für einen geschützten Zugriff installiert ist.
REMOTE_USER Benutzername des Aufrufers vom CGI-Script bei geschützten Dokumenten, wenn die Server-Authentisierung aktiviert ist.
REQUEST_METHOD Zugriffsmethode des Requests (GET oder POST).
SCRIPT_NAME Angabe des CGI-Scripts als relative URL (von dem DNS-Namen des Webservers aus gesehen).
SERVER_NAME Name des Rechners, auf welchem das Server-Programm läuft. Die Angabe kann als Host-Name, DNS-Alias oder IP-Adresse erfolgen.
SERVER_PORT Port, auf dem der Request erfolgte.
SERVER_PROTOCOL Name und Version des Protokolls, über das der Zugriff erfolgte. Das Format ist HTTP/Version. Beispiel: HTTP/1.0
SERVER_SOFTWARE Bezeichnung der Serversoftware, die die Ausführung des CGI-Scripts veranlasst. Das Format ist name/version. Beispiel: CERN/3.0

Tabelle 16.1:   Die CGI-Umgebungsvariablen

Servlet-Hintergründe

Kommen wir zu Servlets zurück. CGI-Prozesse sind zwar erprobt und haben geraume Zeit den Anforderungen einer Client-Server-Beziehung im Internet genügt. Dennoch haben die immer weiter wachsenden Anforderungen der letzten Jahre moderne, standardisierte, in ein übergeordnetes System integrierte und leistungsfähigere Konzepte erzwungen. Servlets sind in Java geschrieben und können direkt auf die gesamte Java-API-Familie zugreifen, einschließlich der JDBC-API, die den Zugriff auf Datenbanken (siehe Seite 840) ermöglicht. Zudem können Servlets auf Bibliotheken mit HTTP-spezifischen Aufrufen zugreifen. Servlets gibt es mit verschiedenen Inhaltstypen. Von HTML über XHTML3 und WML4 bis hin zu XML (eXtensible Markup Language - eine Auszeichnugssprache, mit der die Tags definiert werden, die zum Identifizieren von Daten und Text in XML-Dokumenten nötig sind). Eine sehr interessante Variante von Servlets sind JSP (Java Server Pages). Die JSP-Technologie stellt eine Erweiterung der Servlet-Technologie dar und wurde entwickelt, um das Erstellen von HTML- und XML-Seiten mit Servlets zu vereinfachen. Mit JSP lassen sich festgelegte Inhalte auf einfache Weise mit dynamischen Informationen kombinieren (wir werden dies an einem Beispiel sehen).

Wie schon angedeutet, benötigen Sie für die Erstellung von Servlets nicht nur das JDK, sondern ergänzende APIs. Sun stellt unter http://java.sun.com/products/servlet/index.html Informationen und Downloads für die Arbeit mit Servlets zur Verfügung (siehe Abbildung 16.9).

Abbildung 16.9:  Hier gibt es alles  zu Servlets

Sie finden dort insbesondere auch die Dokumentation zur Servlet-API (siehe Abbildung 16.10).

Abbildung 16.10:  Die Dokumentation der Servlet-API

Wenn Sie Servlets laufen lassen wollen, benötigen Sie auch einen passenden Java-Server, wo sich die Servlets wie nachfolgend beschrieben integrieren können. Einen solchen Applikations-Server (Tomcat) finden Sie unter http://jakarta.apache.org/builds/jakarta-tomcat/release/v3.2.1/bin/, wohin Sie von der Sun-Seite aus verzweigen können (siehe Abbildung 16.11).

Abbildung 16.11:  Downloads zu Servlets - mit Hyperlinks zu Apache und Tomcat

Tomcat ist eine offene Referenzimplementierung für JSP 1.1/Servlets 2.2, die frei verfügbar ist und in diversen Webservern und Entwicklungs-Tools eingesetzt wird (siehe Abbildung 16.12).

Abbildung 16.12:  Unter jakarta.apache.org gibt es den Tomcat-Server.

Abbildung 16.13:  Eine andere Version von Tomcat

Das Konzept eines Java-Servers

Wenn Sie einen Java-Server benötigen, um Servlets laufen zu lassen - um was handelt es sich dabei und warum braucht man ihn? Das Konzept eines Java-Servers ist folgendes: Ein allgemeines Serverprogramm wird auf einem Host installiert und bei Bedarf durch kleine Module erweitert, die noch nicht vorhandene Funktionalitäten bei Erfordernis realisieren.

Diese Module werden Servlets genannt und sind eigentlich nichts anderes als Applets ohne eine Oberfläche, die - statt in einer Webseite und einem Browser - in ein solches allgemeines Serverprogramm integriert werden. Sie werden deshalb auch als serverseitige Applets bezeichnet. Servlets kommunizieren über eine definierte Schnittstelle (dem ServletContext) mit der Laufzeitumgebung auf dem Server. Servlets haben eine ganze Reihe von netten Eigenschaften.

  • Es handelt sich zum einen um normale Java-Objekte. Damit reicht im Prinzip gewöhnliches Java zur Erstellung. Für ein Servlet importieren Sie einfach (neben java.io.* und java.util.*) die ergänzenden Servlet-Pakete in Ihren Source und erstellen Ihre Servlet-Klasse als Subklasse von HttpServlet (Beispiel: public class MeinServlet extends HttpServlet).
  • Sämtliche Sicherheitsmechanismen von Java greifen ebenfalls bei Servlets.
  • Datenbankzugriffe und andere Operationen auf dem Server sind mit Servlets trotz des Java-Sicherheitskonzepts (dem Sandkasten der Applets) möglich. Dies ist kein Widerspruch zu den Sicherheitsmechanismen! Es liegt einfach daran, dass Servlets bereits auf dem Server laufen und dort - falls vertrauenswürdig - lesen und schreiben können. Sie wildern also nicht auf fremden Rechnern.
  • Java-Servlets können von Clients angesprochen werden, die nicht in Java geschrieben sind.

Der Aufbau eines Java-Servers

Ein Java-Serverprogramm besteht aus einem Kern und aus einer Reihe von bereits vorinstallierten, ergänzenden Standard-Servlets, die für eine gewisse Grundfunktionalität sorgen.

Für weitergehende Funktionalität wird der Java-Server nach und nach durch neue Servlets erweitert. Wenn eine spezielle Funktionalität benötigt wird, wird sie einfach als Servlet erstellt, lokal kompiliert und dann mit FTP in das dafür vorgesehene Verzeichnis (z.B. WEB-INF/classes bei Tomcat) auf den Server geladen. Dazu müssen Sie selbstredend über ein entsprechendes Programm und vor allem über ausreichende Rechte verfügen. Hier ist auch das Hauptproblem im Umgang mit Servlets zu sehen. Wenn Sie keinen geeigneten Server haben und/oder nicht genügend Rechte, um Servlets dem Java-Serverprogramm hinzuzufügen, können Sie Servlets zwar leicht programmieren (das sehen wir gleich), aber nicht zum Laufen bringen. Im Prinzip ist aber dieses Hinzufügen zu einem Java-Serverprogramm unkompliziert. Im Servlet-Verzeichnis des Serverprogramms wird die neue Klasse, die das Servlet repräsentiert, abgelegt und mit einem Alias-Namen über die Konfigurationsdialoge des Serverprogramms angemeldet.

Servlets werden dynamisch in den Server geladen, wenn eine Anfrage vom Client kommt. Der Server muss also nicht neu gestartet werden, um das Servlet nutzen zu können. Genauso ist es möglich, ein Servlet beim Start des Servers zu laden.

Ein Servlet wird dann zerstört, wenn seine destroy()-Methode aufgerufen wird (Sie sehen die Verwandtschaft zu den Applets). Danach beseitigt der Garbage Collector in der Laufzeitumgebung das zerstörte Servlet.

Wenn das Servlet Ausgaben erzeugt, schreibt es diese in einen Stream (den so genannten ResponseStream).

Die Java-2-Plattform hat ein neues Servlet-Konzept eingeführt. Die vollständige Servlet-API (javax.servlet.*) zählt in dieser Plattform nicht mehr zum JDK (wie bis noch zur JDK 1.2 Beta 3-Version), sondern gilt als eigenständiges Produkt - das Java Servlet Development Kit. Es ist wie das JDK frei von Sun zur Verfügung gestellt und kann von der Servlet Product Page auf den Java-Softwarewebseiten geladen werden. Diese API benötigen Sie zusätzlich, wenn Sie Servlets erstellen wollen.

Grundlegende Servlet-Funktionalitäten

Ein Servlet verfügt wie ein normales Applet über eine init()-Methode, die beim Start ausgeführt wird. Diese wirft die Ausnahme ServletException aus. Ganz wichtig (und ein elementarer Unterschied zu Applets) ist die Methode protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException. Mit dieser werden die Anfragen eines Clients verarbeitet. Die Methode hat zwei Übergabeparameter: HttpServletRequest und HttpServletResponse. Der erste Übergabeparameter enthält die Anfrage des Clients, der zweite Übergabeparameter die Ausgabe des Servlets. Die Methode service() wirft zwei Ausnahmen aus - ServletException und IOException.

Mit HttpServlet steht eine abstrakte Klasse zur Verfügung, von der eine Unterklasse abgeleitet werden kann, um ein HTTP-Servlet zu erstellen, das Anforderungen von einer Website empfängt und an diese sendet (also den Inhaltstyp HTML verwendet). Wenn Sie von HttpServlet eine Unterklasse ableiten, müssen Sie mindestens eine der nachfolgenden Methoden überschreiben.

Auf die Zugriffsart GET abgestimmt ist die Methode public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException. Die GET-Methode ermöglicht dem Client, Informationen von dem Webserver zu lesen, indem sie einen an eine URL angehängten Abfrage-String übergibt, um dem Server mitzuteilen, welche Informationen er zurückgeben muss. Das kann wie in dem nachfolgende Muster aussehen:

/** Eine HTTP-Anforderung Get bearbeiten*/
public void doGet(HttpServletRequest request, HttpServletResponse  response) throws 
ServletException, IOException {
 response.setContentType(CONTENT_TYPE);
 PrintWriter out = response.getWriter();
 out.println("<font color=\"red\">");
 out.println("<p>Das Servlet hat ein GET empfangen und sieht rot.</p>");
 out.println("</font>");
}

Eine Alternative ist die Methode public void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, wenn das Servlet PUT-Anforderungen in HTTP unterstützt. Mithilfe der PUT-Operation kann ein Client eine Datei auf dem Server platzieren, was etwa dem Senden einer Datei über FTP entspricht.

Wenn das Servlet POST-Anforderungen in HTTP unterstützt, kann der Client mithilfe der HTTP-POST-Methode public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException Daten von unbegrenzter Länge auf einmal an den Webserver versenden. Das kann wie im folgenden Muster erfolgen:

/** Die HTTP-Anforderung Post bearbeiten*/
public void doPost(HttpServletRequest request, HttpServletResponse response) throws 
ServletException, IOException {
response.setContentType(CONTENT_TYPE);
PrintWriter out = response.getWriter();
out.println(
<html><head><title>Mein Servlet</title> </head>");
out.println("<body><p>In der Kuerze liegt die Wuerze</p></body></html>");
}

Eine weitere Methode, die Sie überschreiben können, ist public void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, sofern das Servlet DELETE-Anforderungen in HTTP unterstützt. Mithilfe der DELETE-Operation kann ein Client ein Dokument oder eine Webseite von einem Server entfernen.

Für Anfragen stehen dem Request-Objekt u.a. folgende Methoden zur Verfügung (beachten Sie die Analogien zu den CGI-Umgebungvariablen):

Methode Beschreibung
getAuthType() Wenn der Authorisierungsstatus eines anfragenden -Clients bekannt ist, wird er hiermit zurückgegeben.
getMethode() Rückgabe der Methode, mit der eine Anfrage erfolgt ist. Das können GET, HEAD und POST sein.
getRequestURL() Rückgabe der URL als URL-Objekt.
getProtocol() Rückgabe des bei der Anfrage verwendeten Protokolls.
getServletPath() Rückgabe des Pfads von dem angeforderten Servlet als Zeichenkette.
getPathInfo() Weitere Informationen über den Pfad.
getQueryString() Rückgabe des Abfragestrings.
getContentLength() Rückgabe der Länge des Inhalts.
getContentType() Der Typ des Inhalts.
getServerName() Rückgabe des Servernamens.
getServerPort() Rückgabe des zum Host gehörenden Ports.
getRemoteAddr() Rückgabe der IP-Nummer des anfragenden Clients.
getRemoteHost() Rückgabe des Namens des anfragenden Hosts.

Tabelle 16.2:   Methoden für das Anfrage-Objekt

Dem Antwortobjekt stehen natürlich ein paar Methoden zur Verfügung. Wenn sie Sinn machen (was nicht immer gegeben ist), muss oft nur get mit set in dem Methodennamen vertauscht werden.

Beispiel: Aus getContentLength() wird setContentLength(), um die Länge eines Inhalts festzulegen.

Servlets beherrschen eine eigene Variante der Stromtechnik. Es stehen ServletInputStream und ServletOutputStream für die Ein- und Ausgabe zur Verfügung. Darüber können beliebige Objekte (HTML-Seiten, Bilder usw.) zwischen Client und Server hin- und hergeschickt werden.

Beispiel:

ServletInputStream in = anfrage.getInputStream();
ServletOutputStream out = anfrage.getOutputStream();

Für die Ausgabe der Antwort eines Servlets in dem Ausgabebereich des Clients ist out von besonderem Interesse (wie bei clientseitigen Java-Applikationen, wo nur die Klasse System vorangestellt wird). Hier gibt es die bekannten println()-Methoden.

Servlets aufrufen

Um ein Servlet von einem Client aus aufzurufen, müssen Sie im Grunde einfach dessen URL verwenden. Der URL muss neben dem Protokoll den Namen des Hosts mit dem Servlet und den Namen des Servlets selbst enthalten. Optional sind Argumente, wenn das Servlet welche benötigt.

Servlets lassen sich sowohl von einer HTML-Seite als auch aus einem anderen Servlet heraus aufrufen. Um ein Servlet von einer HTML-Seite aus aufzurufen, muss nur der URL des Servlets in einem geeigneten HTML-Tag verwendet werden. Dies kann selbstverständlich ein Hyperlink-Tag (eine Verankerung), aber auch das <FORM>-Tag von einem Formular (etwa <FORM action="http://localhost:8080/servlet/MeinServlet method="post">), ein Submit-Tag in einem Formular, ein allgemeiner HTML-Button oder ein Meta-Tag (beispielsweise über das Attribut http-equiv in dieser Form: <META http-equiv="refresh" content="5; url=http://localhost:8080/servlet/MeinServlet;">) sein. Servlet-URLs können in HTML-Tags überall dort verwendet werden, wo auch die Verwendung einer normalen URL möglich ist.

Das Aufrufen von Servlets von anderen Servlets aus kann darüber erfolgen, dass ein Servlet eine HTTP-Anforderung für ein anderes Servlet ausgibt. Ein Servlet kann aber auch die public-Methoden eines anderen Servlets direkt aufrufen, wenn die beiden Servlets auf demselben Server laufen. Zuerst muss der Name des aufzurufenden Servlets ermittelt werden. Danach wird auf dessen Servlet-Objekt zugegriffen und eine der public--Methoden des Servlets aufgerfuen. Zugriff auf das Servlet-Objekt erhalten Sie über die getServlet()-Methode des ServletContext-Objekts. Das ServletContext-Objekt kann mit dem ServletConfig-Objekt ermittelt werden, das im Servlet-Objekt gespeichert ist.

Praktische Servlet-Beispiele

Das erste Servlet-Beispiel soll beim Aufruf SSI (Server Side Includes) verwenden. Das bedeutet, der Aufruf des Servlets erfolgt direkt beim Start einer Webseite über einen HTML-Kommentar-Tag, das vom Server als Befehl interpretiert wird. Die allgemeine Syntax eines SSI-Kommandos sieht so aus:

<!--#[Kommando] [Tag1]=[Wert1] [Tag2]=[Wert2] ... -->

SSI basiert auf der Grundlage des Server-Parsing. Bei gewöhnlichen Webseiten erhält der Webserver eine Anforderung für eine Webseite und sendet diese ohne jegliche Überprüfung an den Client zurück. Wenn eine Seite jedoch Server-Parsing fordert, analysiert der Webserver die HTML-Seite vor der Rücksendung und sucht dabei nach serverbasierenden Befehlen. Erkennt der Webserver einen solchen Befehl, führt er die entsprechende Aktion aus.

Grundsätzlich muss bei der Arbeit mit SSI beachtet werden, dass nicht alle Webserver diese Technik unterstützen oder erlauben. Damit die Verwendung von SSI-Anweisungen in einer Webseite für den Server sofort offensichtlich wird, werden solche Webseiten meist die Dateierweiterungen .SHTM oder .SHTML bekommen (das ist aber in der Serverkonfiguration anzupassen). Webseiten mit den normalen Endungen .HTM oder .HTML werden von den meisten Servern nicht geparst und damit werden die SSI-Anweisungen nicht beachtet.

Schauen wir uns nun das Beispiel für ein vollständiges Servlet an. Zuerst die Java-Datei, die ein Servlet darstellt:

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class MeinServlet extends HttpServlet {
  private static final String CONTENT_TYPE = "text/html";
  /**Globale Variablen initialisieren*/
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
  }
  /**Die HTTP-Anforderung GET bearbeiten*/
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws 
ServletException, IOException {
  response.setContentType(CONTENT_TYPE);
  PrintWriter out = response.getWriter();
  out.println(
  "<center><font color=\"red\" size=\"7\">");
  out.println("Das Servlet hat ein GET empfangen und keine Lust zu einer sinnvolleren Antwort 
;-).");
  out.println("</font></center>");
  }
  /**Ressourcen bereinigen*/
  public void destroy() {
  }  }

Wir benötigen für den SSI-Fall noch eine Webseite (in der Regel mit der Dateierweiterung .shtml versehen, damit der Server sie nach Anweisungen parst), über die ein Client eine Anfrage an das Servlet stellt. Das Servlet wird in diesem Beispiel - eng verwandt mit einem Applet (allerdings fehlen logischerweise Angaben wir Höhe und Breite) - mit dem Tag <SERVLET> in die Webseite integriert. Wenn das Servlet beim Laden aufgerufen werden soll und noch eine zusätzliche Möglichkeit in der Webseite für einen Start per Hyperlink existieren soll, sieht die Webseite folgendermaßen aus:

<html><body>
<p>Ausgabe von Servlet <code>MeinServlet</code> GET:</p>
<hr><servlet codebase="" code="MeinServlet.class">
</servlet><hr>
<p><a href="/servlet/MeinServlet">Klicken Sie hier, um Servlet MeinServlet erneut 
aufzurufen</a></p>
</body></html>

Zum Start der gesamten Geschichte muss das Servlet wie gesagt in ein passendes Serverprogramm implementiert werden und von einem Client aus die Webseite geladen werden. Der oben erwähnte Tomcat-Server von Apache ist beispielsweise so ein Serverprogramm. Im Client sehen Sie nach dem Laden die teilweise vom Servlet generierte, zum Teil auch aus purem HTML aufgebaute Seite.

Abbildung 16.14:  Die Antwort des Servlets

Wenn Sie den Hyperlink anklicken, wird das Servlet erneut kontaktiert und nur die Rückgabe des Servlets im Client angezeigt.

Ein zweites Beispiel zeigt die Anwendung von JSP. JSP-Dateien (im Prinzip reine HTML-Dateien) werden meist mit der Erweiterung .jsp gespeichert und rufen über die Anweisung der Form <jsp:useBean id="ServletID" scope="session" class="Servlet" /> das Servlet auf. Erstellen Sie zunächst die Datei jsp1.jsp:

<HTML><HEAD>
<jsp:useBean id="JspBId" scope="session" class="meinservlet.JspB" />
<jsp:setProperty name="JspBId" property="*" />
</HEAD><BODY>
<FORM method="post">
<BR>Bitte einen Wert eingeben :  <INPUT NAME="sample"><P>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Und weg damit">
<INPUT TYPE="RESET" VALUE="Nix is">
<P>Wert der Eigenschaft Bean ist:<jsp:getProperty name="JspBId" property="sample" />
</FORM>
</BODY></HTML>

Die Java-Datei selbst ist ziemlich einfach:

public class JspB {
  private String sample = "Vorbelegung";
  // Beispieleigenschaft abfragen
  public String getSample() {
    return sample;
  }
  // Beispieleigenschaft setzen
  public void setSample(String newValue) {
    if (newValue!=null) {
      sample = newValue;
}  }  }

Wenn Sie die JSP-Datei aufrufen (das Servlet sollte natürlich im Server lauffähig bereit stehen), erhalten Sie ein Eingabeformular.

Abbildung 16.15:  Die Webseite zum Entgegennehmen der Benutzereingaben und dem Aufruf des Servlets

Im Eingabeformular können Sie etwas eintragen, dann mit dem Button das Servlet aufrufen und ihm den Wert übermitteln.

Abbildung 16.16:  Das geht an den Server.

Das Servlet generiert eine neue Seite, wo der eingebene Wert als dynamische Information enthalten ist.

Abbildung 16.17:  Die Antwort des Servlets

16.4.2 Allgemeine Netzwerkzugriffe

Für allgemeine Netzwerkzugriffe steht Java das Paket java.net zur Verfügung. Die darin enthaltenen Klassen erlauben unter Java relativ einfache, plattformübergreifende Vernetzungsoperationen. Darunter fallen das Lesen und Schreiben von Dateien über das Netzwerk oder das Verbinden mit den üblichen Internet-Protokollen.

Um die Einschränkungen von Lese- und Schreiboperationen bei Applets wissen Sie bereits (Stichwort Sicherheit), aber ansonsten sind Netzwerkzugriffe mit Java leichter zu realisieren als mit den meisten anderen Sprachen.

Man unterscheidet in Java (wie auch in allgemeiner Netzwerktechnik) im Wesentlichen zwischen paketorientierter Kommunikation über das Netzwerk und der verbindungsorientierten Kommunikation.

  • Die erste Variante versendet einfach ein Datenpaket mit der Adresse des Zielrechners (in den meisten Fällen eine IP-Adresse und Port) und kümmert sich nicht weiter darum (weder um den genauen Weg, noch darum, ob es überhaupt ankommt). So etwas findet man meistens bei WAN-Netzwerken (Wide Area Network) wie dem Internet.
  • Die zweite Variante baut eine permanente Verbindung zu einem Zielrechner auf, die im Laufe der Kommunikation immer wieder verwendet wird. Sie wird einmal aufgebaut und dann gehalten, bis die Arbeit abgeschlossen ist. Dies nennt man eine semi-permanente Verbindung. Sie finden diese hauptsächlich bei LAN-Netzwerken (Local Area Network), wo sich Betriebsmittel und Leitungen in einer Hand befinden, aber auch bei einer direkten Wahlverbindung zwischen zwei Rechnern.

Wenden wir uns zuerst der verbindungsorientierten Kommunikation zu.

Die URL-Klassen von Java

Internet und Java gehören zusammen. Damit aber auch Java und Netzwerke jeder Art. Java ist von Anfang an als eine Netzwerksprache entworfen worden. Die URL-Klasse bietet einige interessante Möglichkeiten, um eine Internet-Ressource unter Java zu spezifizieren.

Die URL-Klasse (URL steht für Uniform Resource Locator) besitzt Konstruktoren und Methoden für die Verwaltung einer URL. Dies können beliebige Objekte oder Dienste im Internet sein. Das TCP-Protokoll, über das Sie normalerweise einen Netzwerkzugriff auf einen Server realisieren werden, benötigt zwei Teile zur Information: die IP-Adresse des Rechners und die Port-Nummer.

Holen eines URL-Objektes

Die IP-Adresse des Rechners werden Sie meist nicht direkt angeben (und größtenteils auch nicht wissen), sondern statt dessen verwenden Sie den DNS-Namen des Rechners. Dieser Name wird mittels spezieller Namensserver dann automatisch in die IP-Nummer übersetzt, wenn die Information auf der Reise zum Zielserver benötigt wird. Wir sind darauf bei der beschreibung der Funktionsweise des Internets eingegangen.

Den Port benötigen Sie nicht immer, denn normalerweise ist Port 80 für den Web-Dienstport reserviert. Wenn Sie also keinen Port angegeben, wird der adressierte Server normalerweise diesen Port defaultmäßig verwenden.

Es gibt jedoch keine feste Regel, die besagt, dass Port 80 der Web-Port sein muss. Es ist einfach Usus, aber nicht zwingend. Die URL-Klasse berücksichtigt diese möglichen Spezifikations-Variationen. Um eine neue URL zu erstellen, sind diverse Konstruktoren möglich. Nachfolgend drei Beispiele:

Konstruktor Beschreibung
public URL(String url_zeichenkette) throws MalformedURLException; Dieser Konstruktor erzeugt ein URL-Objekt, das in der url_zeichenkette sämtliche notwendigen Informationen für den Zugriff (Protokoll, Host, evtl. Port, Pfad und/oder Dateiname) enthalten sollte.
public URL(String protokoll, String host, int port, String Datei) throws MalformedURLException; Dieser Konstruktor erzeugt ein URL-Objekt, das sämtliche notwendigen Informationen für den Zugriff (Protokoll, Host, Port, Pfad und/oder Dateiname) als einzelne Parameter benötigt.
public URL(String protokoll, String host, String Datei) throws MalformedURLException; Dieser Konstruktor erzeugt ein URL-Objekt, das für den Zugriff außer dem Port sämtliche notwendigen Informationen (Protokoll, Host, Pfad und/oder Dateiname) als einzelne Parameter benötigt.

Tabelle 16.3:   URL-Konstruktoren

Die ausgeworfene Ausnahme sollten Sie jedes Mal gezielt abfangen (try-catch-Block), denn das Holen eines URL-Objekts ist ein sehr instabiles Unterfangen. Dazu können Sie folgenden Code verwenden (Syntax mit dem ersten Konstruktor):

String url = ... // irgendeine Url
try {
meinUrl = new URL (url);
}
catch(MalformedURLException e) {
  // tue etwas sinnvolles
}

Anzeigen eines URL-Objekts in einem Browser

Um ein URL-Objekt aus einem Applet an einen Browser weiterzugeben, brauchen Sie nur eine einzige Zeile Code:

getAppletContext.showDocument(meineURL);

Die Methoden showDocument(meineURL) bzw. showDocument(meinURL, String) gehören zum Interface java.applet.AppletContext und zeigen in einem Browser den URL an. Testen wir das in einem kleinen Beispiel:

import java.net.URL;
import java.net.MalformedURLException;
public class Netz1 extends java.applet.Applet {
  URL homepage;
  public void init()  {
   try {
    homepage=new URL("http://localhost/");
    }
    catch (MalformedURLException e)
    {
    System.out.println(
  "Nix gibt's:" + e.toString());
    }
    getAppletContext().showDocument(homepage);
   }  }

Das Applet lädt bei der Initialisierung die Startpage des lokalen Hosts (in unserem Fall) und zeigt sie direkt statt des Applets an. Eingebunden wird das Applet wie üblich:

<html><body>
<APPLET code="Netz1.class" height=200 width=200>
</APPLET>
</body></html>

Öffnen von Webverbindungen

Statt ein URL-Objekt nur anzuzeigen, kann man es auch direkt in einem Applet verwenden. Sicherheitsgründe begrenzen jedoch diese Form von Netzwerkverbindungen. Ein Applet kann normalerweise nur eine Verbindung zu dem Host aufbauen, von dem es ursprünglich geladen wurde (falls der Browser es überhaupt kommunizieren lässt). Sicherheitslücken und zu freizügige Einstellungen des Browsers seien bei dieser Aussage außer Acht gelassen.

Die Klasse URL definiert eine Methode public final InputStream openStream() throws IOException, die eine Netzwerkverbindung mit einer bestimmten URL öffnet und eine Instanz der Klasse InputStream ausgibt. Wir haben in dem Kapitel über die Ein- und Ausgabe in Java gesehen, dass man einen solchen Strom in einen DataInputStream konvertieren kann. Damit können Sie die Zeichen aus dem Strom auf vielfältige Weise lesen und anschließend verarbeiten. Wenn Sie beispielsweise wie oben eine URL in meineURL gespeichert haben, können Sie folgenden Code verwenden, um zeilenweise die Zeichen zu verarbeiten:

try {
 InputStream eingabe = meineURL.openStream();
 DataInputStream data_wert = 
  new DataInputStream(new BufferedInputStream(eingabe));
 String zeile;
 while ((zeile = data_wert.readLine()) != null) {  
// tue etwas mit den Zeilen
 }  }
catch  (IOException e);{
  // tue etwas sinnvolles
}

Die URLconnection-Klasse

Bei der openStream()-Methode handelt es sich um eine vereinfachte Variante einer Methode aus der allgemeineren URLconnection-Klasse. Diese abstrakte Klasse enthält diverse Möglichkeiten, Dateien anhand beliebiger URLs aus dem Netz zu laden und bietet eine weitaus größere Flexibilität zur Handhabung von URL-Verbindungen. Sie ist die Superklasse für alle Klassen, die eine Verbindung zwischen einer Anwendung und einer URL repräsentieren. Über diese Klasse können Verbindungen sowohl zum Lesen als auch zum Schreiben über diesen Kanal aufgebaut werden.

16.4.3 Sockets

Die Klassen URL und URLconnection bieten nur relativ eingeschränkte Möglichkeiten für Netzwerkverbindungen. Für die meisten Fälle werden sie zwar ausreichen, aber es gibt genügend Anwendungen für weitergehende Funktionalitäten, etwa andere Protokolle. Dafür stellt Java neben den schon angesprochenen Techniken für verteilte Anwendungen und Servlet-Kontakte die Klassen Socket und ServerSocket zur Arbeit mit Sockets zur Verfügung.

Die Klasse Socket ist für die Erstellung und Verwaltung von Sockets auf der Clientseite und die Klasse ServerSocket für die Erstellung und Verwaltung von Sockets auf der Serverseite zuständig.

Java besitzt die gleichen UNIX-Wurzeln wie das Internet. Die von UNIX-Sockets abgeleitete Klasse Socket stellt ein Grundobjekt in der Internet-Kommunikation dar, das das TCP-Protokoll unterstützt. Die Socket-Klasse verfügt über diverse Datenstrommethoden zur Ein- und Ausgabe, was das Auslesen und Schreiben in die Socket-Klasse sehr vereinfacht.

Um eine Verbindung über die Socket-Klasse aufzubauen, erstellen Sie eine Instanz von der Klasse. Dies geschieht mit der folgenden Syntax:

Socket verbindung = new Socket(hostname, portnummer);

Dabei ist hostname durch den tatsächlichen Namen des Hosts und portnummer durch die Nummer des Ports zu ersetzen. Dabei ist die Klasse InetAddress aus dem Paket java.net sehr nützlich. Ein aus dieser Klasse erstelltes Objekt enthält sowohl den symbolischen DNS-Namen als auch die IP-Adresse des jeweiligen Rechners. Und einige sehr nützliche Methoden:

  • Die Methode public String getHostName() liefert den Namen des Hosts für diese IP-Addresse. Wenn ein Securitymanager aktiv ist, wird eine SecurityException ausgeworfen, wenn die Abfrage nicht erlaubt ist. Dazu wird die Methode SecurityManager.checkConnect(java.lang.String, int) genutzt.
  • Die Methode public byte[] getAddress() liefert die IP-Addresse des InetAddress-Objekts als byte-Array.
  • Die Methode public String getHostAddress() liefert die IP-Adresse als String in der Form »%d.%d.%d.%d«.
  • Die Methode public static InetAddress getByName(String host) throws UnknownHostException bestimmt die IP-Addresse eines Hosts, wenn der Name des Hosts gegegeben ist. Dieser Hostname kann in der From www.rjs.de oder als eine String-Repräsentation der IP-Addresse angegeben werden. UnknownHostException wird ausgeworfen, wenn der Host nicht gefunden werden kann.
  • Die Methode public static InetAddress[] getAllByName(String host) throws UnknownHostException liefert alle IP-Addressen von einem Host als Array, wenn der Name des Hosts gegeben ist. Dieser Hostname kann in der From www.webscripting.de oder als eine String-Repräsentation der IP-Addresse angegeben werden. UnknownHostException wird ausgeworfen, wenn der Host nicht gefunden werden kann. Wenn ein Securitymanager aktiv ist, wird eine SecurityException ausgeworfen, wenn die Abfrage nicht erlaubt ist. Dazu wird die Methode SecurityManager.checkConnect(java.lang.String, int) genutzt. UnknownHostException wird ausgeworfen, wenn für den Host keine IP-Adresse gefunden werden kann.
  • Besonders interessant ist die Methode public static InetAddress getLocalHost() throws UnknownHostException, die den lokalen Host in Form seiner IP-Adresse zurückliefert. Wenn ein Securitymanager aktiv ist, wird eine SecurityException ausgeworfen, wenn die Abfrage nicht erlaubt ist. UnknownHostException wird ausgeworfen, wenn der Host nicht gefunden werden kann.

Sobald die Verbindung geöffnet ist, können Sie wie üblich mit Ein- und Ausgabeströmen arbeiten. Die Eingabe könnte so realisiert werden:

DataInputStream eingabe = new DataInputStream(new 
BufferedInputStream(verbindung.getInputStream()));

Die Ausgabe könnte analog laufen:

DataOutputStream ausgabe = new DataOutputStream(new 
BufferedOutputStream(verbindung.getOutputStream()));

Sehr sinnvoll sind in diesem Zusammenhang serialisierte Objekte, die zwischen Socket und ServerSocket hin- und hergesendet werden. Wir werden das im nachfolgenden Beispiel tun.

Ein Socket sollte geschlossen werden, wenn alle Arbeiten erledigt sind. Dazu dient die close()-Methode.

Beispiel: verbindung.close();

Die Klasse ServerSocket kümmert sich um Client-Anforderungen, wobei ServerSocket diesen Service nicht selbst ausführt, sondern ein Socket-Objekt im Auftrag des Clients erstellt. Die Kommunikation wird dann über dieses Objekt ausgeführt. Man nennt diese Objekte serverseitige Sockets.

Ein ServerSocket richtet sich nach einem TCP-Port, um eine Client-Verbindung aufzubauen. Wenn sich nun ein Client an diesen Port anschließt, muss diese Verbindung mittels einer eigenen Methode - der accept()-Methode - akzeptiert werden. Wenn Sie diese Methode im Server notiert haben, wird der Server dort auf eine Verbindung zu einem Client warten.

Um ein ServerSocket zu erstellen, gehen Sie ähnlich vor wie bei den clientseitigen Sockets. Als Erstes erstellen Sie eine Instanz von der Klasse. Dies geschieht mit der folgenden Syntax:

ServerSocket verbindung = new ServerSocket(portnummer);

Dabei ist portnummer durch die Nummer des Ports zu ersetzen. Sie haben damit einen Port festgelegt, den Sie dann für Client-Anfragen mit der accept()-Methode freigeben können.

Beispiel: verbindung.accept();

Danach können Sie wieder die Ein- und Ausgabeströme verwenden, um mit dem Client über Lese- und Schreibvorgänge zu kommunizieren. Spielen wir den Vorgang in einem Beispiel durch, wobei wir hier mit serialisierten Daten arbeiten. Zuerst erstellen wir den Quelltext für den Server.

import java.io.*;
import java.net.*;
import java.util.*;
public class MeinServer {
 // Serversocket erstellen und den Strom zum 
 // Empfang von serialisierten Objekten verwenden
   public static void main(String args[]) {
      ServerSocket meinServerSocket = null;
      Socket meinSocket = null;
      String str1 = null;
      String str2 = null;
      try {
  meinServerSocket = new ServerSocket(1234);
  System.out.println("Server ist gestartet und wartet auf einen Client.");
  // Warten auf eine Verbindung zu diesem 
  // Socket
    meinSocket = meinServerSocket.accept();
  // Eingabestrom von serialisierten Objekten
  // entgegennehmen
  InputStream o = meinSocket.getInputStream();
  ObjectInput s = new ObjectInputStream(o);
  str1 = (String) s.readObject();
  str2 = (String) s.readObject();
  s.close();
  // Ausgabe der vom Client übergebenen Werte
  System.out.println(str1);
  System.out.println(str2);
      } 
      catch (Exception e) {
  System.out.println("Nix is: " 
  + e.getMessage());
  System.exit(1);      
  }
    }  }

Der Client sieht so aus:

import java.io.*;
import java.net.*;
import java.util.*;
public class MeinClient {
    public static void main(String args[]) {
      try {
    // Socket erstellen
  Socket meinSocket = new 
    Socket(InetAddress.getLocalHost(), 1234);
  // Outputstream erstellen, mit Socket 
  // verbinden und serialisieren
  OutputStream o = 
    meinSocket.getOutputStream();
  ObjectOutput s = new ObjectOutputStream(o);
    System.out.println(
    "Ab geht es an den Server.");
  s.writeObject(
    "Server, das sagt dir dein Client:");
  s.writeObject(args[0]);
  s.flush(); 
  s.close();
    System.out.println("Das war's. Und ciao.");
      } 
      catch (Exception e) {
  System.out.println("Nix is: " 
    + e.getMessage());
  System.exit(1);
      }
  }  }

Starten Sie zuerst den Server und dann in einer eigenen DOS-Box bzw. einer zusätzlichen Shell das Clientprogramm. Der Server meldet sich mit einer kurzen Ausgabe und wartet dann auf eine Verbindung zum Client. Der Client übergibt die serialisierten Strings an den Server, der diese dann in seiner Shell ausgibt.

Sollte das Beispiel nicht funktionieren, wird es unter Umständen am Port liegen. Probieren Sie einen anderen Port aus.

Abbildung 16.18:  Socket-Kommunikation über zwei Shells hinweg

Wenn ein Server mehrere Clients bedienen soll, muss er sinnvollerweise Multithreading-fähig sein. Ein Thread wartet dabei permanent auf Verbindungen.

Datagram-Sockets

Kommen wir nun zu einer paketorientierten Verbindungsart. Über Datagram-Sockets zu kommunizieren ist einfacher als über die Klassen Socket und ServerSocket, die auf TCP basierende Sockets sind. Die Kommunikation ist sogar schneller, da der Verbindungsaufwand bedeutend geringer ist. Das TCP-Protokoll ist ein sehr sicheres Protokoll. So versucht es beispielsweise, Pakete nochmals zu versenden, wenn ein Fehler auftritt. Die darauf aufsetzenden Sockets können und müssen diese Fähigkeiten auch nutzen, was zwar sicher ist, jedoch zu Lasten der Geschwindigkeit geht.

Ein Datagram-Paket dagegen wird einfach als eine Ansammlung von Bytes an ein empfangendes Programm gesendet. Das empfangende Programm wartet in der Regel an einer bestimmten IP-Adresse und einem zugehörigen Port. Man nennt diese Kommunikation über Datagramme UDP (Unreliable Datagram Protocol):

Das Empfängerprogramm wird zum Sender, wenn es dem sendenden Programm eine Antwort schicken möchte. Dazu wird einfach an die aus dem empfangenen Sendevorgang bekannte IP- und Portadresse des ursprünglichen Senders zurückgeschickt.

Das Verfahren ist aus der Funktechnik bekannt. Eine Station wird abwechselnd zum Sender und Empfänger.

Datagram-Sockets sind dann zur Kommunikation sinnvoll, wenn es auf Geschwindigkeit und/oder geringe Übertragungsmengen ankommt und die Übertragungssicherheit nicht so wichtig oder anderweitig gewährleistet ist. Letzteres ist z.B. dann gegeben, wenn die Kommunikation nur lokal stattfindet oder die Leitungen störungsfrei und sicher sind.

Versenden eines Datagram-Pakets

Um ein Datagram-Paket zu versenden, erstellen Sie zuerst eine Instanz von der Klasse. Dies geschieht beispielsweise mit der folgenden Syntax:

DatagramPacket meinPacket = new DatagramPacket( byte_array, nachrichten_laenge, 
internet_addresse, port);

  • Die Angabe byte_array ist ein byte-Array, das die versendete Nachricht beinhaltet.
  • Die Angabe nachrichten_laenge ist ein Integerwert, der die Länge der Nachricht angibt.
  • Die Angabe internet_addresse beinhaltet die IP-Adresse, wo die Nachricht hin gesandt werden soll.
  • Die Angabe port ist wieder ein Integerwert, der die Port-Nummer spezifiziert.

Anschließend erstellen Sie eine Instanz von DatagramSocket:

DatagramSocket meinSocket = new DatagramSocket();

Mit der send()-Methode können Sie nun das gerade erstellte Paket versenden:

meinSocket.send(meinPacket);

Das Socket sollte wieder geschlossen werden, wenn alle Arbeiten erledigt sind. Dazu dient wieder die close()-Methode: meinSocket.close();

Empfangen eines Datagram-Pakets

Das Datagram-Paket ist nun unterwegs zum Ziel und soll dort empfangen werden. Um ein Datagram-Paket zu empfangen, gehen Sie ziemlich ähnlich zum Senden vor. Sie erstellen zuerst eine Instanz von der Klasse DatagramPacket, allerdings mit einem etwas veränderten Konstruktor. Dies geschieht nun beispielsweise mit der folgenden Syntax:

DatagramPacket empfangPacket = new DatagramPacket(buffer, buffer.length );

Die Angabe buffer ist wieder ein byte-Array, das die Nachricht nach dem Empfang beinhalten soll, danach folgt die Länge des Arrays.

Anschließend erstellen Sie eine Instanz von DatagramSocket:

DatagramSocket empfangSocket = new DatagramSocket(port);

Die Angabe port ist ein Integerwert, der durch den Port zu ersetzen ist, den der Sender verwendet.

Mit der receive()-Methode können Sie nun das spezifizierte Paket empfangen: empfangSocket.receive(empfangPacket);

Das Socket sollte über die close()-Methode wieder geschlossen werden, wenn alle Arbeiten erledigt sind.

16.5 Datenbanken und JDBC

Die netzwerkorientierte Struktur von Java macht die Sprache zu einem idealen Kandidaten für Client-Server-Einsätze. Das 3-Tier-Schichtmodell und Servlets haben dies ja schon angedeutet. Client-Server-Datenbanken sind ein sehr wichtiges Beispiel in der Praxis. Java wird als plattformunabhängiger Client für Datenbanken eingesetzt, auf die über das Netzwerk zugegriffen werden kann. Wegen der Plattformunabhängigkeit muss man sich keine großen Gedanken über plattformspezifische Fragestellungen machen.

Für den Zugang zu Datenbanken von außen bietet Java eine einfache Möglichkeit - die Java DataBase Connectivity (JDBC). Diese Schnittstelle erlaubt es Entwicklern von Datenbankanwendungen, datenbankunabhängige Java-Clients zu schreiben, die auf zahlreiche verbreitete relationale Datenbanken zugreifen können.

Auf Grund des modularen Aufbaus der JDBC-Spezifikation können aufgabenbezogene Erweiterungen hinzugefügt und für die Datenverarbeitung notwendige Tools jederzeit integriert werden. Diese können über den JDBC-Layer mit den erforderlichen Datenbanken kommunizieren. Die JDBC-Treiber übernehmen dabei die gesamte Datenbankanbindung. Sofern sich die Datenbankentwickler an die normale Standard SQL-Syntax (SQL = Structured Query Language) halten, sollte jedes Datenbankprodukt mit einem JDBC-kompatiblen Treiber verwendet werden können.

JDBC ist nicht als Produkt zu verstehen, sondern es handelt sich um die abstrakte Spezifikation einer Schnittstelle zwischen einer Client-Anwendung und einer SQL-Schnittstelle. Das Interface ist als Low-Level-API zum grundlegenden SQL-Zugriff entworfen worden. Es liegt an den verschiedenen Herstellern von Datenbanken und Software, ob JDBC-kompatible Treiber eingebaut werden, damit Java-Anwendungen mit den Datenbanksystemen verbunden werden können.

16.5.1 Was sind relationale Datenbanken?

Die Grundlagen von Datenbanken dürften nicht jedem Leser ein Begriff sein. Aber auch einige Leser mit Vorkenntnissen in Programmierung werden bisher noch keine Erfahrung mit der Erstellung und dem Zugriff auf Datenbanken aus einer eigenen Applikation heraus gemacht haben. Das Thema Datenbankprogrammierung ist eher im Bereich der professionellen Software-Entwicklung angesiedelt. JDBC gibt Ihnen die Möglichkeit, aus Java relativ einfach auf relationale Datenbanken zuzugreifen. Wir wollen deshalb in einem kleinen Exkurs erst einmal klären, was sich hinter dem Konzept einer relationalen Datenbank verbirgt.

Im Allgemeinen enthalten Datenbanken Informationen, die in einer bestimmten Weise angeordnet sind und die auf Grund von bestimmten Abfragen aufbereitet und ausgegeben werden können. Eine Datenbank kann in tabellarischer Form vorliegen, aber ebenso komplexer geordnet sein. Man unterscheidet vom Konzept her drei Haupttypen von Datenbanken:

  • Hierarchisch strukturierte Datenbanken
  • Relational aufgebaute Datenbanken
  • Netzwerkdatenbanken

Historisch das älteste System ist die hierarchisch strukturierte Datenbank. In der Computersteinzeit (70er- bis 80er-Jahre) war das System sehr populär. Dabei werden Daten als ein Baumsystem mit Datensammlungen behandelt, die als Äste dargestellt werden. In einem hierarchischen Schema müssen Zugriffe auf die Daten entlang der Baumstruktur verlaufen. Die üblichste Beziehung in einer hierarchischen Struktur ist eine »Eins zu Vielen-Beziehung« zwischen den Datensätzen. Eine »Viele-zu-Viele-Beziehung« ist ohne erhebliche Redundanz (Überschneidungen von Daten ohne zusätzlichen Informationsgehalt) nicht zu bewerkstelligen.

Aber klären wir erst einmal, was das mit »Eins zu Vielen-Beziehung« bzw. »Viele-zu-Viele-Beziehung« auf sich hat.

Es gibt drei Arten von Beziehungen zwischen Datensätzen:

  • Eins zu Eins: Ein Datensatz in einer Tabelle ist mit einem Datensatz in einer anderen Tabelle verbunden.
  • Eins zu Vielen: Ein Datensatz in einer Tabelle kann mit vielen Datensätzen in einer anderen Tabelle verknüpft sein.
  • Viele zu Vielen: Ein Datensatz in einer Tabelle kann genau wie bei einer »Eins-zu-Vielen-Beziehung« mit vielen Datensätzen in einer anderen Tabelle verknüpft sein. Daneben können aber auch weitere Datensätze in dem Beziehungsgeflecht solche »Eins-zu-Vielen-Beziehung« mit vielen Datensätzen in einer anderen Tabelle haben.

Auf das hierarchische Schema folgten irgendwann die Netzwerk-Datenbanken. Das Netzwerk-Datenmodell entspricht einer »Viele-zu-Vielen-Beziehung« zwischen den Datenelementen. Man versucht sich den Unterschied zu dem hierarchischen Schema so zu verdeutlichen, indem man das hierarchische Schema mit einer Eltern-Kind-Beziehung vergleicht, während das Netzwerkschema eine Gleichgestellten-Beziehung repräsentiert.

In den 90ern entstand das relationale Datenzugriffskonzept. Das relationale Schema betrachtet die Daten als eine Tabelle mit Zeilen und Spalten. Die Zeilen (auch Records genannt) stellen die Datensätze der Tabelle dar. Jede Zeile unterteilt sich entsprechend der Spalten in der Tabelle in Felder. Die Felder enthalten die eigentlichen Daten. Das Hauptkonzept beim relationalen Schema ist, dass die Daten einheitlich sind. Jede Zeile einer Tabelle enthält die gleiche Anzahl an Spalten. Viele solcher Tabellen (die sich in ihrer Struktur unterscheiden können) bilden eine Datenbank.

Die Relationen zwischen verschiedenen Tabellen einer Datenbank sind der Namensgeber für das Konzept. Um zwei Tabellen miteinander zu verbinden, müssen die zwei Tabellen mindestens eine gemeinsame Spalte haben. Diese gemeinsame Spalte enthält die Information, die für einen Datensatz der einen Tabelle einen Zugriff auf die zugehörigen Informationen aus der anderen Tabelle erlaubt. Auf diesem Weg entsteht eine Eins zu Vielen-Beziehung. Die gemeinsamen Felder nennt man Schlüssel. Wenn man diesen Schlüssel mit Zuordnungsinformationen in einer separaten Tabelle verwaltet, spricht man von einer Index-sequenziellen Datenbank.

Ein anderer Weg führt über eine dritte Tabelle mit zwei Spalten, eine für ein Schlüsselfeld der ersten Tabelle und die zweite entsprechend für die zweite Tabelle. Über diese beiden Spalten werden die Datensätze der beiden Tabelle einander zugeordnet, wodurch eine Relation entsteht. Dies ist dann eine Viele zu Vielen-Beziehung.

16.5.2 Was ist SQL?

SQL (Structured Query Language) ist eine universelle Datenbanksprache, die Aktionen auf relationalen Datenbanken ermöglicht. Unter solche Aktionen fallen das Erzeugen (create), Aktualisieren (update), Einfügen (insert) und Löschen (delete) von Daten oder Datendefinitionen für die Erzeugung von Tabellen und Spalten. Des Weiteren gibt es Möglichkeiten, den Zugriff auf Datenelemente zu beschränken und Anwender und Gruppen zu erzeugen.

Weitere Bestandteile der Sprache betreffen das allgemeine Datenmanagement, Backup-Verfahren, das Kopieren und Aktualisieren von umfangreichen Datensätzen und Transaktionsverarbeitung (SQL-Statements, die Datenreihen und -felder in eine Datenbank löschen, aktualisieren oder hinzufügen).

Nahezu jeder Datenbankhersteller stellt über eine eigene Implementation von SQL sicher, dass man mit SQL auf seine Datenbank zugreifen kann.

16.5.3 JDBC versus ODBC

Einer der ersten JDBC-kompatiblen Treiber ist der JDBC-Treiber für ODBC-kompatible Datenbanken, mit dem Java-Programmierer leicht auf eine beliebige ODBC-kompatible Datenbank zugreifen können. Microsofts ODBC (Open Database Connectivit>) basiert auf dem gleichen Konzept wie JDBC - dem X/Open SQL CLI (Call Level Interface). Die JDBC-Spezifikation ist der ODBC-Spezifikation von Microsoft zwar sehr ähnlich, ist aber - als von Sun entwickelte Schnittstelle - natürlich besser auf die Zusammenarbeit mit Java ausgelegt. Zusätzlich können viele der Mängel von ODBC - wie zu viele Zeichenketten, Verarbeitung von sehr großen binary-Objekten und falsch verweisende Pointer - mit Java leichter gehandhabt werden oder treten auf Grund der Art, wie Java unbekannte Datentypen behandelt, gar nicht erst auf. Die Hauptunterschiede zwischen JDBC und ODBC liegen darin, wie Daten zwischen dem Aufrufer und dem Treiber hin- und hergeschickt werden. ODBC ist ein C-basiertes API. Daher verwendet es das »(void*)«-Casting, um Spaltenergebnisse an die Aufrufprozedur zurückzugeben. Java verwendet dagegen Methoden, die die erwarteten Typen direkt an den Aufrufer zurückgeben. Da Java ebenso nicht auf eine statische Größe von Arrays beschränkt ist, können Probleme auf Grund zu großer Zeichenketten und verschiedener Zeichengrößen leicht innerhalb von Java gehandhabt werden. Darüber hinaus besitzt JDBC Sicherheitsstufen, die es bei ODBC nicht gibt.

16.5.4 Grundaufbau von JDBC und des JDBC-Managers

JDBC arbeitet auf zwei Stufen. Die erste Stufe ist der Verbindungsaufbau zwischen der Java-Anwendung und dem JDBC-Treibermanager mittels der JDBC-API. Über diesen JDBC-Treibermanager kann ein Java-Programm dann mehrere JDBC-Treiber verwalten und mit ihnen Informationen und Daten austauschen. Jeder Treiber wiederum kann aus Java direkt auf lokale Daten zugreifen, ODBC als Zwischenebene dazwischen schalten oder einen Netzwerkzugriff auf eine Datenbank auslösen. Die Treiber registrieren sich bei dem JDBC-Manager während der Initialisierung, sodass der Manager einen Überblick über alle verfügbaren Treiber hat. Der JDBC-Manager hat noch weitergehende Aufgaben. So untersucht er beispielsweise den Zustand eines Treibers, damit ggf. ein Applet oder eine Applikation einen geeigneten Treiber herunterladen kann, wenn noch keiner auf dem System vorhanden ist.

Während des Versuchs, sich mit einer Datenbank zu verbinden, gibt das Java-Programm eine Datenbank-URL an den JDBC-Manager weiter. Der JDBC-Manager ruft dann jeden geladenen JDBC-Treiber, bis man die angefragte URL öffnen kann. Jeder Treiber ignoriert solche URLs, die Datenbanken erfordern, zu denen er sich nicht verbinden kann. Java Client-Programme können diesen Vorgang des Treibersuchens übergehen und explizit angeben, welcher Treiber verwendet werden soll, wenn der Java-Client bereits vorher weiß, welcher Treiber geeignet ist. Die URLs von JDBC haben immer die folgende Form: jdbc:subprotocol:subname

Das Subprotokoll ist der Name des Verbindungsprotokolls, und der Subname ist der Name der jeweiligen Datenbank innerhalb der Domäne des Protokolls. Sofern über den Subnamen Informationen über Host und Port verschlüsselt sind, sollte dieser den Host und den Port in der URL-Standard-Notation angeben: //hostname:port/subname

Beispielsweise kann eine ODBC-Datenbank mit Namen Bankkonten über den URL jdbc:odbc:Bankkonten spezifiziert werden. Die gleiche Datenbank auf dem Rechner bundesbank zusammen mit dem Verbindungsprotokoll ixnet würde folgende URL haben:

jdbc:ixnet://bundesbank/Bankkonten

Jeder Client kann mehrere Datenbankverbindungen geöffnet haben, wobei jede von diesen Verbindungen die Instanz einer Klasse ist, die aus java.sql.connection abgeleitet ist. Die Mehrfachverbindung kann über den gleichen JDBC-Treiber oder über mehrere unterschiedliche Treiber laufen.

16.5.5 Woraus besteht das JDBC-API?

JDBC besteht aus mehreren portablen Java-Klassen/-Schnittstellen und liegt derzeit als JDBC 2 vor, das im SDK 2 von Java integriert ist. JDBC wurde vor dem SDK 2 so gut wie ausschließlich über Klassen realisiert, die allesamt zum Paket java.sql gehören. Dieses Paket beinhaltet u.a. die nachfolgend beschriebenen Schnittstellen, Klassen und Ausnahmen.

Wichtigste Schnittstellen:

Interface Beschreibung
CallableStatement Ein Interface für die Ausführung von SQL-Stored-Proce-dures.
Connection Eine Connection (Session) mit einer angegebenen Datenbank. Die Schnittstelle enthält eine Menge Funktionalität, von der Transaktionsverarbeitung bis zum Erzeugen von Statements bildet sie die Grundlage.
DatabaseMetaData Allgemeine Metainformationen über die Datenbank. Um den JDBC-Einsatz möglichst einfach zu halten, unterstützen JDBC-konforme Programme die JDBC-Schnittstelle mit zusätzlichen Informationen (so genannte Metadaten) über die Datenbank und die Ergebnisse. Dies erfolgt mit Methoden der Schnittstellen java.sql.DatabaseMetaData und java.sql.ResultSetMetaData. Das bedeutet, es gibt über die eigentlichen Datenbankinhalte hinausgehende Informationen. Dies können beispielsweise der Tabellenname, die Breite einer Spalte, die Typen in einer Spalte usw. sein. Die Schnittstelle DatabaseMetaData stellt Katalogfunktionen bereit, die denen in ODBC ähneln. Eine Anwendung kann die zugrunde liegenden DBMS-Systemtabellen abfragen. ODBC gibt die Informationen als ResultSet zurück. JDBC gibt die Ergebnisse als ein ResultSet-Objekt mit wohldefinierten Spalten zurück. Ein DatabaseMetaData-Objekt stellt über 100 Methoden zur Verfügung. Die meisten davon werden jedoch wahrscheinlich nur selten Verwendung -finden.
Driver Eine ganz wichtige Schnittstelle. Jede Treiberklasse muss sie implementieren. Die Schnittstelle stellt gewöhnlicherweise Informationen wie PropertyInfo, Versionsnummer usw. bereit.
ResultSet Eine Tabelle mit Daten, die ein Datenbank-ResultSet repräsentieren, das gerade durch die Ausführung von einen Abfragestatement der Datenbank generiert wurde.
ResultSetMetaData Metainformationen über die Typen und Eigenschaften der Spalten in einem ResultSet-Objekt. Die Schnittstelle dient also zum Auswerten von Ergebnissen einer Abfrage. Verglichen mit dem DatabaseMetaData-Objekt ist das -ResultSetMetaData-Objekt leichter und hat weniger -Methoden. Über das ResultSetMetaData-Objekt kann man mehr über die Typen und Eigenschaften von Spalten in einem ResultSet herauszufinden. Die Methoden des ResultSetMetaData-Objekts wie getColumnLabel() und getColumnDisplaySize() kann man deshalb in normalen Anwendungsprogrammen verwenden.
SQLData Mapping von anwenderdefinierten SQL-Typen.
SQLInput Ein Eingabestrom, der einen Strom von Daten beinhaltet, die als Instanz von einem SQL-struktierten oder -getrennten Typ zu verstehen sind.
SQLOutput Ein Ausgabestrom zum Schreiben der Attribute von anwenderdefinierten Typen in die Datenbank.
Statement Ausführung eines statischen SQL-Statements und wieder Entgegennehmen der produzierten Resultate.
Struct Standard-Mapping zwischen der Java-Programmiersprache für einen SQL-strukturierten Typ.

Tabelle 16.4:   Schnittstellen für JDBC

Klassen:

Klasse Beschreibung
Date Wrapper für JDBC um java.util.Date zur Identifikation als ein SQL-Datum.
DriverManager Die Basisklasse zum Managen eines Satzes von JDBC-Treibern unter JDBC vor dem JDBC 2.0 API (dort gibt es alternative Möglichkeiten). Die Klasse enthält die Treiber-informationen, Statusinformationen und mehr. Wenn ein Treiber geladen ist, registriert er sich beim DriverManager. Wenn eine Verbindung geöffnet wird, wählt der DriverManager den Treiber in Abhängigkeit vom JDBC URL aus. Der DriverManager gibt ein Connection-Objekt zurück, wenn Sie die getConnection()-Methode verwenden.
DriverPropertyInfo Treibereigenschaften für den Aufbau einer Connection.
SQLPermission Die Erlaubnis, die der Securitymanager checken wird, wenn Appletcode eine der setLogWriter-Methoden aufruft. Die Klasse ist neu im Java 2 SDK hinzugefügt worden.
Time Wrapper für JDBC um java.util.Date zur Identifikation als ein SQL-Zeitwert.
Timestamp Wrapper für JDBC um java.util.Date zur Identifikation als ein SQL-TIMESTAMP.
Types Definition von Konstanten, die zur Identifikation von generierten SQL-Typen verwendet werden (JDBC-Typen).

Tabelle 16.5:   Klassen für JDBC

Wichtigste Exceptions:

Exception Beschreibung
SQLException Fehler beim Zugriff auf eine Datenbank.
SQLWarning Warnungen beim Zugriff auf eine Datenbank.

Tabelle 16.6:   Exceptions   für JDBC

Wenn irgend möglich verwendet JDBC statische Typenfestlegungen zur Kompilierungszeit. Laufzeitfehler beim Datenbankzugriff erzeugen eine Ausnahme vom Typ java.sql.SQLException, die - wie alle Ausnahmen - eine Ableitung von java.lang.Exception ist. Aus diesem Grund sollten sämtliche Datenzugriffsmethoden throws SQLException zum Abfangen bereitstellen und diese in einem try-catch-Block bearbeiten. So etwas könnte folgendermaßen aussehen (eine einfache Ausgabe von Ausnahmen auf das Standardausgabegerät):

try {
// JDBC-Aufruf
}
catch (SQLException e) {
System.out.println(
"In folgendem SQL-Aufruf ist eine Ausnahme aufgetreten: " + 
e.getSQLState());
System.out.println("Die Meldung der Ausnahme lautet: " + e.getMessage());
System.out.println("Der zugehoerige  Errorcode: " + e.getErrorCode());
}

Da ein JDBC-Aufruf zu Folgefehlern durch verkette Ausnahmen führen kann, wird oft die Ausnahmebehandlung in eine Schleifenstruktur verpackt:

try {
// JDBC-Aufruf
}
catch (SQLException e) {
while (e != null) {
// ... Behandlung einer Ausnahme
e.getNextException();
}  }

Anders als eine SQLException, die das Programm wegen der ausgeworfenen Ausnahme bemerkt, verursacht eine SQLWarning in einem Java-Programm erst einmal keine Aufmerksamkeit oder gar Probleme. Eine SQLWarning wird an das Objekt, dessen Methode die Warnung verursacht hat, angehängt. Sie sollten deshalb mit der getWarning()-Methode, die für alle Objekte verfügbar ist, auf Warnungen hin überprüfen.

16.5.6 Die JDBC-Treiber

Die JDBC-Treiber existieren in verschiedenen internen Ausprägungen:

  • Der Typ JDBC-ODBC ist für den Zugriff auf alle vorhandenen ODBC-Treiber und ihre Datenquellen zuständig. Dieser Treiber wurde gemeinsam von Sun und Intersolv entwickelt und nennt sich JDBC-ODBC-Bridge. Die JDBC-ODBC-Bridge ist als jdbcOdbc.class in Java implementiert und eine native Bibliothek, um auf den ODBC-Treiber zuzugreifen (keine Standard-Klasse). Unter Windows ist die native Bibliothek eine DLL (JDBCODBC.DLL). Da JDBC sehr am Design von ODBC orientiert ist, ist die ODBC-Bridge eine dünne Schicht über JDBC. Intern mappt dieser Treiber JDBC-Methoden auf ODBC-Aufrufe und tritt so mit jedem verfügbaren ODBC-Treiber in Interaktion. Der Vorteil dieser Bridge ist, dass über JDBC fast alle Datenbanken angesprochen werden können, da es für die meisten Datenbanken ODBC-Treiber gibt. Wenn die ausführenden Binärdateien auf dem Clientrechner installiert sind, kann darüber dann der Zugriff auf Datenbestände erfolgen. Der wohl größte Nachteil der JDBC-ODBC-Bridge ist die bei großen Datenbanken zu geringe Performance.
  • Der zweite Typ von Treiber konvertiert JDBC-Aufrufe in Textkommandos einer darunter liegenden speziellen Datenbank-API, beispielsweise DB2, Informix, Oracle oder andere DBMS. Damit müssen auf dem Client wieder plattformabhängige Binärdateien vorhanden sein, die diese Kommandos sinnvoll auswerten und die eigentlichen Datenbankzugriffe erledigen. Vorteil dieser Technik gegenüber der JDBC-ODBC-Bridge ist bessere Performance.
  • Die dritte Form von JDBC-Treibern spricht aus Java direkt Datenbanken an. Zumeist sind dies lokale Datenbanken. Dazu werden alle Zugriffs-, Lese-, Schreib- und Auswertungsaufgaben unmittelbar implementiert.
  • Der vierte Typ von JDBC-Treibern ist für datenbankspezifische Netzwerkbefehle zuständig. Mittels dieses Treibers werden JDBC-Anweisungen in datenbankspezifische Netzwerkbefehle übersetzt, die die Datenbank-API auf dem Server dann ausführt. Diese Treiberform ist explizit nicht auf zusätzliche Binärdateien auf dem Client (witzigerweise ist hier mit Client der Server gemeint - ist halt eine andere Beziehung) angewiesen. Statt dessen wird direkt mittels den Java-Netzwerk-Klassen mit der Datenbank kommuniziert. Diese verhalten sich dort wie jede andere Zugriffssoftware. Nahezu alle namhaften Hersteller von Datenbanken (und diverser Tools) bieten mittlerweile diese Form von Treibern an, weil sie einfach die effektivste, schnellste und sicherste Variante ist.

16.5.7 Schematischer Aufbau einer Datenbank-Applikation

Allgemein geht man bei Datenbankzugriffen so vor, dass man zuerst die notwendigen Packages einbindet. Das sind so gut wie immer java.net.URL bzw. java.net.* und java.sql.*. Oft brauchen Sie noch java.util.* und java.math.* (Letzteres, wenn Sie mit der Klasse BigDecimal arbeiten wollen).

Im nächsten Schritt laden Sie die gewünschten JDBC-Treiber unter ihrem Java-Namen. Dies kann mit folgender Syntax geschehen:

try {
// Laden der gewünschten Treiber
Class.forName(drivername);
// ... weitere sinnvolle Aktionen
}
catch (ClassNotFoundException e) {
// etwas Sinnvolles tun
}

Die Methode forName() der Klasse Class erledigt das Laden der Treiberklasse (ein Vorgang, der auf der Fähigkeit der Reflection basiert). Dazu muss man allerdings den vollständigen Klassennamen kennen. Für die JDBC-ODBC-Bridge sieht das so aus: Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

Die Namen von anderen Treibern sind in der Dokumentation des jeweiligen Herstellers angegeben.

Im nächsten Schritt können wir bereits eine Datenbankverbindung aufbauen. Als fiktives Beispiel soll unsere ODBC Datenbank mit Namen »dbtest« dienen. Über den URL jdbc:odbc:dbtest sprechen wir sie an:

String url="jdbc:odbc:dbtest";
Connection con = DriverManager.getConnection(url, "userid", "passwd");

Die Methode getConnection() liefert eine Connection zur Datenbank. Sie gibt es in verschiedenen Varianten. Insbesondere, wenn keine User-ID und kein Passwort notwendig sind, kann man auch einfach Folgendes notieren:

Connection con = DriverManager.getConnection(url);

Der nächste Schritt dient dazu, ein Abfrageobjekt zu erstellen und darauf ein (im Grunde beliebiges) SQL-Kommando auszuführen. In unserem Beispiel wollen wir sämtliche Datensätze aus einer Tabelle namens Tabelle1 ausgeben.

Statement abfrageobj = con.createStatement();
ResultSet ergebnis = abfrageobj.executeQuery("SELECT * from Tabelle1");

Wenn wir mittels einer SQL-Abfrage die Datensätze selektiert haben, sollten wir damit auch etwas Sinnvolles tun, z.B. auf dem Bildschirm nach gewissen Kriterien anzeigen, was folgende Methode tut:

private static void zeigeResultate (ResultSet res) throws SQLException {
// Zur Abfrage der Metadaten wird ein Metadatenobjekt 
// erstellt
ResultSetMetaData resmeta = res.getMetaData();
// Abfrage der Anzahl von Spalten in der Tabelle über 
// das Metadatenobjekt
int anzahlSpalten = resmeta.getColumnCount();
// Abfrage der Namen von den Spalten in der Tabelle 
// über das Metadatenobjekt und Ausgabe der vorhandenen 
// Spaltennamen (bis anzahlSpalten)
for (int i=1; i <= anzahlSpalten; i++) {
System.out.println(resmeta.getColumnName(i));
}
// Erzeuge einen Zeilenvorschub
System.out.println("");
// Anzeige der Daten über zwei ineinander 
// verschachtelte Schleifen
while(res.next()) {
for (int i=1; i <= anzahlSpalten; i++) {
// Trennzeichen für die Ausgabe
if (i > 1) System.out.print(";"); 
System.out.print (res.getString(i););
} // Ende for()-Schleife
// Erzeuge einen Zeilenvorschub
System.out.println("");
} // Ende while()-Schleife
System.out.println("Ende der Ausgabe!");
}

Zum Bewegen des Datensatzzeigers in dem Resultset sind insbesondere die folgenden Methoden von Interesse:

Methode Beschreibung
public boolean next() throws SQLException Nächsten Datensatz auswählen.
public boolean previous() throws SQLException Vorherigen Datensatz auswählen.
public void afterLast() throws SQLException Bewegt den Datensatzzeiger auf das Ende des ResultSet-Objekts (direkt hinter die letzte Zeile).
public void beforeFirst() throws SQLException Bewegt den Datensatzzeiger vor den Beginn des ResultSet-Objekts (direkt vor die erste Zeile).
public boolean first() throws SQLException Bewegt den Datensatzzeiger auf die erste Zeile des ResultSet-Objekts.
public boolean last() throws SQLException Bewegt den Datensatzzeiger auf die letzte Zeile des ResultSet-Objekts.
public void moveToCurrentRow() throws SQLException Bewegt den Datensatzzeiger auf die aktuelle Zeile.

Tabelle 16.7:   Methoden für die Navigation auf einem ResultSet-Objekt

Eine Methode wie zeigeResultate(ergebnis) sollte nun nach der Selektion der Abfrageergebnisse (am besten innerhalb eines try-catch-Blocks, um die SQLExceptions abzufangen) aufgerufen werden und dann im letzten Schritt die üblichen Aufräumarbeiten erfolgen:

abfrageobj.close();
con.close();

16.5.8 Praktischer Einsatz von JDBC

Lassen Sie uns einen Datenbankzugriff mit Java und der JDBC-ODBC-Bridge anhand eines kleinen, aber vollständigen Beispiels Schritt für Schritt durchspielen. Dabei greifen wir auf eine Access-Datenbank zu, die entsprechend mit einem ODBC-Treiber registriert ist. Sie können das Beispiel mit der auf der CD befindlichen Datenbank DBTest.mdb nachvollziehen oder auch eine eigene Datenbank verwenden. Das muss auch keine Access-Datenbank sein. Das ist ja gerade der Vorteil von JDBC bzw. auch ODBC. Sie müssen nur dafür sorgen, dass die Datenbank im Betriebssystem bereitsteht. Spielen wir zuerst die Anmeldung der Datenbank unter ODBC anhand von Windows NT durch.

Wenn Sie mit der Beispieldatenbank arbeiten wollen, sollten Sie diese in ein Verzeichnis auf der Festplatte kopieren. Das erleichert sowohl die Arbeit mit ODBC (was soll passieren, wenn die CD nicht eingelegt ist?) und macht natürlich erst Schreibvorgänge in der Datenbank möglich.

Um die Datenbank (oder jede andere) unter ODBC anzumelden, öffnen Sie die Systemsteuerung von Windows und wählen dort das ODBC-Management aus.

Abbildung 16.19:  Das ODBC-Management in der Systemsteuerung

Sie finden dort alle angemeldeten ODBC-Treiber und Schablonen für die zur Verfügung stehenden Datenbankformate. Wenn Sie eine neue Datenbank anmelden wollen, klicken Sie auf Hinzufügen....

Abbildung 16.20:  Hinzufügen einer neuen Datenbank

Da wir in unserem Beispiel mit einer Access-Datenbank arbeiten wollen, wählen wir dessen Treiber-Schablone aus. Sie müssen entsprechend die passende Treiber-Schablone auswählen.

Abbildung 16.21:  Hinzufügen einer neuen Datenbank auf Basis von Access

Im Folgedialog wählen Sie die physikalische Datenbank mit allen notwendigen Angaben und/oder - teils optionalen - Optionen (Pfad und Dateiname, Seitentimeout, Puffergröße, Exklusivzugriff, Schreibschutz, Beschreibung, Name). Von besonderem Interesse ist der Datenquellenname, denn dies ist die Angabe, über die Sie die Datenbank dann im Java-Quelltext ansprechen.

Abbildung 16.22:  Die genaue Angabe der Datenbank-Datei und ihrer Zugriffseigenschaften

Wenn die Datenbank dann so im Betriebssystem registriert ist, kann sie aus Java heraus, wie oben beschrieben, genutzt werden. Wir demonstrieren in den nachfolgenden Beispielen die wichtigsten Situationen.

Selects auf eine Datenbank

Zu einem einfachen Select auf die Datenbank und der Ausgabe der Ergebnisse genügt eine einfache Struktur wie die folgende:

import java.net.URL;
import java.sql.*;
class DBSelectTest {
 public static void main(String argv[]) {
 try {
 // Der JDBC-URL für den Zugriff auf ODBC
 String url = "jdbc:odbc:dbtest";
 // Verbindung zu der Datenbank
 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
 Connection con = DriverManager.getConnection(url);
 // Ausführung einer SQL-SELECT-Anweisung
 Statement stmt = con.createStatement();
 ResultSet rs = stmt.executeQuery(
  "SELECT * FROM Tabelle1");
 System.out.println("Das Ergebnis der Abfrage:");
 // Durch alle Datensätze des Ergebnisses
 // der Abfrage steppen und ausgeben
 while (rs.next()) {
 // Werte der Spalten der aktuellen Zeile
 String a = rs.getString(1);
 String b = rs.getString(2);
 String c = rs.getString(3);
 String e = rs.getString(5);
 String f = rs.getString(6);
 // Ausgabe des Resultats:
 System.out.print("Nr: " + a);
 System.out.print(", Vorn: " + b);
 System.out.print(", Nachn: " + c);
 System.out.print(", Str: " + e);
 System.out.print(", Ort: " + f);
 // Zeilenvorschub
  System.out.print("\n");
 }
 stmt.close();
 con.close();
 } 
 catch (java.lang.Exception ex) {
 ex.printStackTrace();
 }
 }  }

Das Beispiel schickt ein einfaches SQL-Statement an die Datenbank, in dem die Tabelle mit Namen Tabelle1 ausgewählt wird und dort alle Spalten - in der Abfrage - ausgewählt werden. Von dem zurückgegebenen Resultset werden zwar alle Datensätze (eine while-Schleife, in der mit der Methode next() alle Datensätze der Reihe nach ausgewählt werden) geliefert, es werden aber nur die Spalten 1, 2, 3, 5 und 6 ausgegeben. Die konkrete Abfrage wird mit der Methode public ResultSet executeQuery(String sql) throws SQLException ausgelöst.

Abbildung 16.23:  Gefiltertes Ergebnis der Abfrage

Wenn Sie die Datenbank gezielter abfragen wollen, können Sie im SQL-SELECT-Statement die Spaltennamen (die sie natürlich kennen müssen) direkt angeben. Etwa so:

ResultSet rs = stmt.executeQuery(
  "SELECT a, b, c, d, e FROM Tabelle1");

Allgemeine Syntax der SELECT-Anweisung ist folgende:

SELECT [ALL|DISTINCT] SpaltenListe FROM Tabname1 [, Tabname2 ...] [WHERE Suchbedingung] 
[GROUP BY SpaltenName1 [, SpaltenName2 ...]]
[HAVING Suchbedingung]
[UNION SubQuery]
[ORDER BY SpaltenName1 [ASC|DESC] [, SpaltenName2 [ASC|DESC]...]]

Wenn Sie aus dem Resultset nicht nur Strings herausholen wollen, können Sie diverse Methoden direkt auf dem ResultSet-Objekt anwenden. Etwa so:

 int a = rs.getInt(1);
 BigDecimal b = rs.getBigDecimal(2);
 char c[] = rs.getString(3).toCharArray();
 boolean d = rs.getBoolean(4);

Grundsätzlich gibt es zahlreiche dieser Abfrage-Methoden, die alle mit get beginnen und dann die Art der Extrahierung genauer spezifizieren. Die Namen der Methoden sind weitgehend selbsterklärend. Hier eine Auswahl der wichtigsten:

 Array getArray(int i)
 Array getArray(String colName)
 BigDecimal getBigDecimal(int columnIndex)
 BigDecimal getBigDecimal(String columnName)
 boolean getBoolean(int columnIndex)
 boolean getBoolean(String columnName)
 byte getByte(int columnIndex)
 byte getByte(String columnName)
 byte[] getBytes(int columnIndex)
 byte[] getBytes(String columnName)
 Date getDate(int columnIndex)
 Date getDate(int columnIndex, Calendar cal)
 Date getDate(String columnName)
 Date getDate(String columnName, Calendar cal)
 double getDouble(int columnIndex)
 double getDouble(String columnName)
 float getFloat(int columnIndex)
 float getFloat(String columnName)
 int getInt(int columnIndex)
 int getInt(String columnName)
 long getLong(int columnIndex)
 long getLong(String columnName)
 ResultSetMetaData getMetaData()
 Object getObject(int columnIndex)
 Object getObject(String columnName)
 int getRow()
 short getShort(int columnIndex)
 short getShort(String columnName)
 Statement getStatement()
 String getString(int columnIndex)
 String getString(String columnName)
 Time getTime(int columnIndex)
 Time getTime(int columnIndex, Calendar cal)
 Time getTime(String columnName)
 Time getTime(String columnName, Calendar cal)
 Timestamp getTimestamp(int columnIndex)
 Timestamp getTimestamp(int columnIndex, Calendar cal)
 Timestamp getTimestamp(String columnName)
 Timestamp getTimestamp(String columnName, Calendar cal)
 SQLWarning getWarnings()

Updates

Schauen wir uns nun an, wie aus Java heraus ein Update einer Datenbank realisiert werden kann. Wenn wir mit dem ResultSet arbeiten wollen, stehen zahlreiche Methoden für ein gezieltes Update von ganzen Datensätzen und/oder gezielt einzelnen Feldern der Datenbank zur Verfügung.

Die Ausführung eines Updates wird mit der Methode public int executeUpdate(String sql) throws SQLException angestoßen. Als SQL-Anweisung muss ein gültiges INSERT-, UPDATE- oder DELETE-Statement angegeben werden.

import java.net.URL;
import java.sql.*;
class DBTestUpdate {
 public static void main(String argv[]) {
// Die JDBC-URL für den Zugriff auf ODBC
 String url = "jdbc:odbc:dbtest";  
 Connection con;
 Statement stmt;
 try {
// Verbindung zum Treiber
 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  } 
 catch(java.lang.ClassNotFoundException e) {
  System.err.print("ClassNotFoundException: ");
  System.err.println(e.getMessage());
 }  
 try {
  con = DriverManager.getConnection(url);
  stmt = con.createStatement();              
  stmt.executeUpdate("insert into Tabelle2 " +
  "values('Eins', 'Zwei', 'Drei')");
  stmt.close();
  con.close();
  System.out.println("Fertisch");
 } 
 catch (java.lang.Exception ex) {
 ex.printStackTrace();
 }  }  }

Das Beispiel fügt einen neuen Datensatz in die Tabelle2 der Datenbank hinzu. Diese hat drei Spalten, die einfach ohne die Angabe eines Spaltennamens sequenziell gefüllt werden. Das Beispiel ist - um nur die entscheidenten Details zu zeigen - ganz einfach gehalten und zeigt nicht einmal an, was nach dem Update in der Datenbank steht (das machen wir aber gleich im nächsten Beispiel). Mit SQL kann man selbstverständlich auch gezielt einzelne Spalten ansprechen. Grundsätzlich verwendet man INSERT, um einen neuen Datensatz anzulegen. Allgemeine Syntax ist folgende:

INSERT INTO Tabname [(SpaltenName1 [,SpaltenName2 ... ])] VALUES (Wert1 [, Wert2 ...])

Bauen wir das Beispiel von eben ein wenig aus. Es sollen keine festen Werte hinzugefügt werden, sondern die Übergabewerte an das Programm5. Außerdem sollen alle nach dem Update in der Tabelle vorhandenen Werte unmittelbar nach der Aktion ausgegeben werden.

import java.net.URL;
import java.sql.*;
class DBTestUpdate2 {
 public static void main(String args[]) {
// Die JDBC-URL für den Zugriff auf ODBC
 String url = "jdbc:odbc:dbtest";  
 Connection con;
 Statement stmt;
 String sql = "";  
 // Auswerten der Übergabewerte und 
 // Zusammensetzen des Insert-Strings
 try{
  sql = "INSERT INTO Tabelle2" + " values('" + 
  args[0] + "', '" + args[1] + "', '"+ args[2]  
  + "')";
 }
 catch(Exception e) {
  System.err.print("Fehler: Das Programm benoetigt drei Uebergabewerte");
  System.err.println(e.getMessage());
 }  
 try {
// Verbindung zum Treiber
 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  } 
 catch(java.lang.ClassNotFoundException e) {
  System.err.print("ClassNotFoundException: ");
  System.err.println(e.getMessage());
 }  
   
 try {
  con = DriverManager.getConnection(url);
  stmt = con.createStatement();              
 // Datensatz updaten mit den
 // Übergabewerten beim Aufruf
  stmt.executeUpdate(sql);
 // Abfrage des Inhalts der Tabelle nach dem 
 // Update
  ResultSet rs = stmt.executeQuery(
  "SELECT * FROM Tabelle2");
  System.out.println("Das Ergebnis der Abfrage:");
 // Durch alle Datensätze des Ergebnisses
 // der Abfrage steppen und ausgeben
  while (rs.next()) {
 // Werte der Spalten der aktuellen Zeile
  String a = rs.getString(1);
  String b = rs.getString(2);
  String c = rs.getString(3);
 // Ausgabe des Resultats:
  System.out.print("Feld A: " + a);
  System.out.print(", Feld B: " + b);
  System.out.print(", Feld C: " + c);
  // Zeilenvorschub
  System.out.print("\n");
 }
  stmt.close();
  con.close();
  System.out.println("Fertisch");
 } 
 catch (java.lang.Exception ex) {
 ex.printStackTrace();
 }  }  }

Abbildung 16.24:  Zwei Inserts wurden durchgeführt.

Beachten Sie, dass das Programm unbedingt drei Übergabewerte benötigt. Das kann natürlich noch leicht perfektioniert werden, indem in diesem Fall Default-Leerwerte in die Datenbank geschrieben werden oder etwas Ähnliches.

Um einen bereits bestehenden Datensatz upzudaten, wird das SQL-Statement UPDATE verwendet. Allgemeine Syntax ist folgende:

UPDATE Tabname SET SpaltenName1 = Wert1| NULL [, SpaltenName2 = Wert2| NULL...] [WHERE 
Suchbedingung]

Grundsätzlich stellt die Klasse ResultSet auch für das Update einer Datenbank zahlreiche Methoden bereit, die alle mit update beginnen und dann die Art des Updates genauer spezifizieren. Die Namen der Methoden sind weitgehend selbsterklärend und zeigen die Verwandtschaft zu den Abfrage-Methoden. Hier eine Auswahl der wichtigsten:

 void updateBigDecimal(int columnIndex, BigDecimal x)
 void updateBigDecimal(String columnName, BigDecimal x)
 void updateBoolean(int columnIndex, boolean x)
 void updateBoolean(String columnName, boolean x)
 void updateByte(int columnIndex, byte x)
 void updateByte(String columnName, byte x)
 void updateBytes(int columnIndex, byte[] x)
 void updateBytes(String columnName, byte[] x)
 void updateDate(int columnIndex, Date x)
 void updateDate(String columnName, Date x)
 void updateDouble(int columnIndex, double x)
 void updateDouble(String columnName, double x)
 void updateFloat(int columnIndex, float x)
 void updateFloat(String columnName, float x)
 void updateInt(int columnIndex, int x)
 void updateInt(String columnName, int x)
 void updateLong(int columnIndex, long x)
 void updateLong(String columnName, long x)
 void updateNull(int columnIndex)
 void updateNull(String columnName)
 void updateObject(int columnIndex, Object x)
 void updateObject(int columnIndex, Object x, int scale)
 void updateObject(String columnName, Object x)
 void updateObject(String columnName, Object x, int scale)
 void updateRow()
 void updateShort(int columnIndex, short x)
 void updateShort(String columnName, short x)
 void updateString(int columnIndex, String x)
 void updateString(String columnName, String x)
 void updateTime(int columnIndex, Time x)
 void updateTime(String columnName, Time x)
 void updateTimestamp(int columnIndex, Timestamp x)
 void updateTimestamp(String columnName, Timestamp x)

Das Update einer Datenbank ist jedoch nicht ganz so trivial wie das einfache Auslesen. Es handelt sich immerhin um Schreiboperationen. Es kann zu Zugriffskonflikten kommen, unter Umständen müssen neue Datensätze, eventuell sogar neue Tabelle oder gar die ganze Datenbank erzeugt werden. Dazu gibt es die SQL-Anweisungen CREATE und DROP.

Updates von Datenbanken erledigt man oft auch nicht mit ResultSet, sondern PreparedStatement. Diese Schnittstelle stellt vorkkompilierte SQL-Statements bereit.

Löschvorgänge

Löschvorgänge in einer Datenbank sind eigentlich Spezialfälle einer Update-Aktion. Dementsprechend sieht die Beispieldatei fast gleich aus. Sie verwendet im Wesentlichen nur das SQL-DELETE-Statement. Dessen allgemeine Syntax sieht so aus:

DELETE FROM Tabname [WHERE Suchbedingung]

Wir wollen einfach in dem folgenden Beispiel die Tabelle leeren. Vorher wird der Inhalt der Tabelle angezeigt und nach dem Löschen zum Beweis nochmal eine Abfrage gestartet und ausgegeben (leer).

import java.net.URL;
import java.sql.*;
class DBTestDelete {
 public static void main(String argv[]) {
// Die JDBC-URL für den Zugriff auf ODBC
 String url = "jdbc:odbc:dbtest";  
 Connection con;
 Statement stmt;
 try {
// Verbindung zum Treiber
 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  } 
 catch(java.lang.ClassNotFoundException e) {
  System.err.print("ClassNotFoundException: ");
  System.err.println(e.getMessage());
 }  
  try {
  con = DriverManager.getConnection(url);
  stmt = con.createStatement();              
  ResultSet rs = stmt.executeQuery(
  "SELECT * FROM Tabelle2");
  System.out.println(
  "Tabelleninhalt vor dem Loeschen:");
 // Durch alle Datensätze des Ergebnisses
 // der Abfrage steppen und ausgeben
  while (rs.next()) {
 // Werte der Spalten der aktuellen Zeile
  String a = rs.getString(1);
  String b = rs.getString(2);
  String c = rs.getString(3);
 // Ausgabe des Resultats:
  System.out.print("Feld A: " + a);
  System.out.print(", Feld B: " + b);
  System.out.print(", Feld C: " + c);
  // Zeilenvorschub
  System.out.print("\n");
  }
  stmt.executeUpdate("DELETE FROM Tabelle2 ");
  System.out.println(
  "Tabelleninhalt nach dem Loeschen:");
 // Durch alle Datensätze des Ergebnisses
 // der Abfrage steppen und ausgeben
  rs = stmt.executeQuery(
  "SELECT * FROM Tabelle2");
  while (rs.next()) {
 // Werte der Spalten der aktuellen Zeile
  String a = rs.getString(1);
  String b = rs.getString(2);
  String c = rs.getString(3);
 // Ausgabe des Resultats:
  System.out.print("Feld A: " + a);
  System.out.print(", Feld B: " + b);
  System.out.print(", Feld C: " + c);
  // Zeilenvorschub
  System.out.print("\n");
 }
  stmt.close();
  con.close();
  System.out.println("Fertisch");
 } 
 catch (java.lang.Exception ex) {
 ex.printStackTrace();
 }  }  }

Abbildung 16.25:  Vorher ist was drin und dann isses weg.

Achten Sie vor dem Ausführen des Beispiels darauf, dass vorher das Update-Beispiel gestartet wurde oder die Tabelle anderweitig gefüllt wurde. Sie sehen sonst nicht viel.

Weiterführende Datenbank-Aktionen

Selbstverständlich stellen JDBC und Java Möglichkeiten für weiterführende Datenbank-Aktionen bereit, was aber hier den Rahmen sprengt. So gibt es die Methoden

void commit()
void rollback()
void setAutoCommit(boolean aC)

für Transaktionen und diverse ergänzende Dinge wie Transaktionslevels, Methoden zum Verwenden von Metadaten oder gecachte Connections und Techniken, um mit verknüpften Tabellen zu arbeiten. Für diese sehr umfangreiche Fragestellung sei explizit auf Spezialliteratur oder die Online-Dokumentation verwiesen.

JDBC jenseits von SQL

Vom seinem Funktionsprinzip her ist JDBC nicht auf SQL-Zugriffe beschränkt. Ein Treiber muss nur die Methode public boolean jdbcCompliant() enthalten. Damit wird mitgeteilt, ob der Treiber den ANSI-92 SQL-Standard unterstützt. Für eine SQL-Datenbank muss mindestens das so genannte ANSI SQL92 EntryLevel unterstützt werden. Aber auch wenn ein JDBC-Treiber diesem ANSI SQL92 EntryLevel nicht entspricht, kann er mit Textkommandos einen darunter liegenden DBMS-Treiber ansprechen. Dieser muss nur in der Lage sein, diese Kommandos sinnvoll auszuwerten. Damit lassen sich die spezifischen Funktionen des DBMS-Treibers mittels JDBC aus Java heraus nutzen, die Portabilität geht jedoch verloren, was dann bei anderen Datenbanktypen zu Fehlern führen kann. In diesem Fall wird eine Ausnahme erzeugt.

JDBC-Sicherheit

Java verfolgt den Vertrauensstatus (Trusted Status) eines jeden Datenbanktreibers. Dies hat die Folge, dass nicht vertrauenswürdige Treiber die gleichen Beschränkungen wie nicht vertrauenswürdige Applets haben. Damit können sie nicht auf das lokale Dateisystem, sondern nur auf die Datenbestände der Hosts zugreifen, von denen sie kommen.

Der JDBC-Manager geht sogar noch einen Schritt weiter - nicht vertrauenswürdige Treiber können nur zusammen mit den Applets verwendet werden, die vom gleichen Host kommen. Während des Verbindungsversuchs und der Suche nach dem geeigneten JDBC-Treiber greift der Manager nicht auf nicht-vertrauenswürdige Treiber der verschiedenen Hosts zurück.

16.6 Zusammenfassung

Ursprünglich war Java nur eine neue Programmiersprache, aber mittlerweile bietet Java auf vielfältigen Ebenen Erweiterungsmöglichkeiten. Auch rund um Java schreiten Entwicklungen voran, die sich mit Java kombinieren lassen oder auf dem Java-Konzept aufbauen. Wir haben uns in diesem Kapitel mit den Themen

  • Reflection
  • Serialization
  • JavaBeans
  • CORBA, IDL und RMI
  • Netzwerkzugriffe, Sockets und Java-Servlets
  • Datenbanken und JDBC

Schlaglichter herausgesucht, die einige dieser vielfältigen Einsatzmöglichkeiten darstellen. Vollständig beschrieben sind sie damit aber nicht. Dies macht ja gerade das ungeheuere Potenzial von Java aus. Ich hoffe, Ihnen hat der Einstieg in die Java-Welt Spaß gemacht und Sie können und werden Java als Grundlage Ihrer weiteren Programmiertätigkeit einsetzen.

1

Die Klasse stellt insbesondere die Methode getClass() bereit, mit der ein beliebiges Objekt zu jeder Klasse, die das Laufzeitsystem verwendet, während des Ladevorgangs ein Klassenobjekt vom Typ Class erzeugt werden kann sowie die Methode forName(), um ein Klassenobjekt zu einer Klasse eines Namens zu beschaffen. Wir wenden die Methode bei den Beispielen zu den Datenbankzugriffen an.

2

Es gibt auch Erweiterungen für andere Plattformen oder sie sind zumindest angekündigt (Solaris, Unix, MVS), aber Kern ist Windows.

3

Eine Umformulierung von HTML 4.0 in XML mit weitgehend identischen Tags und Attributen, aber strukturierteren und strengeren Codierungsregeln - beispielsweise werden alle XHTML-Tags klein geschrieben und zu jedem öffnenden Tag muss auch das schließende Tag gesetzt werden.

4

Wireless Markup Language - Textdateien, die XML-Inhalte enthalten und in ein kompaktes binäres Format für ein WAP-Gerät - Wireless Application Protoco> - kompiliert werden.

5

Wir erstellen also eine einfache Datenbank-Shell zum Update von Werten.


© Copyright Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH
Elektronische Fassung des Titels: Java 2 Kompendium, ISBN: 3-8272-6039-6 Kapitel: 16 Weiterführende Themen