benutzt wird. Diese Parameter werden beim Laden des Applets weitergeleitet. In der init()-Methode des Applets können Sie diese Parameter mit der getParameter()-Methode einlesen. getParameter() nimmt ein Argument an – eine Zeichenkette, die den Namen des Parameters bezeichnet, nach dem Sie suchen. Die Methode gibt einen String mit dem entsprechenden Wert zurück (wie bei Argumenten in Java-Applikationen werden alle Parameterwerte als Strings zurückgegeben). Um den Wert des font-Parameters aus der HTML-Datei zu erhalten, können Sie z. B. die folgende Zeile in die init()-Methode aufnehmen:
442
Erstellen von Applets
String theFontName = getParameter("font");
Die Namen der in angegebenen Parameter und die Namen der Parameter in getParameter() müssen absolut identisch sein, auch in Bezug auf Groß- und Kleinschreibung. Mit anderen Worten: unterscheidet sich von . Werden Ihre Parameter nicht richtig an das Applet weitergeleitet, überprüfen Sie die Parameternamen. Falls ein erwarteter Parameter nicht in der HTML-Datei angegeben wurde, gibt getParameter() null zurück. Sie sollten auf den null-Parameter testen und einen vernünftigen Standardwert setzen. if (theFontName == null) theFontName = "Courier";
Bedenken Sie außerdem, dass diese Methode Strings zurückgibt. Wenn der Parameter von einem anderen Objekt- oder Datentyp sein soll, müssen Sie den übergebenen Parameter selbst konvertieren. Nehmen Sie z. B. die HTML-Datei für das QueenMab-Applet. Um den size-Parameter zu parsen und ihn der Integer-Variablen theSize zuzuweisen, könnten Sie den folgenden Quellcode verwenden: int theSize; String s = getParameter("size"); if (s == null) theSize = 12; else theSize = Integer.parseInt(s);
Listing 14.4 zeigt Ihnen eine überarbeitete Fassung des Watch-Applets, in dem die Hintergrundfarbe als Parameter namens background angegeben werden kann: Listing 14.4: Der vollständige Quelltext von NewWatch.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
import java.awt.*; import java.util.*; public class NewWatch extends javax.swing.JApplet { private Color butterscotch = new Color(255, 204, 102); private String lastTime = ""; Color back; public void init() { String in = getParameter("background"); back = Color.black; if (in != null) { try { back = Color.decode(in);
443
Java-Applets erstellen
15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: }
}
catch (NumberFormatException e) { showStatus("Bad parameter " + in);
} } setBackground(back); } public void paint(Graphics screen) { Graphics2D screen2D = (Graphics2D)screen; Font type = new Font("Monospaced", Font.BOLD, 20); screen2D.setFont(type); GregorianCalendar day = new GregorianCalendar(); String time = day.getTime().toString(); screen2D.setColor(back); screen2D.drawString(lastTime, 5, 25); screen2D.setColor(butterscotch); screen2D.drawString(time, 5, 25); try { Thread.sleep(1000); } catch (InterruptedException e) { // nichts tun } lastTime = time; repaint(); }
Außerhalb der init()-Methode weist das Applet NewWatch nur wenige Neuerungen auf. In Zeile 7 wird ein Color-Objekt deklariert, und Zeile 28 ist so modifiziert, dass statt Color.black das neue Color-Objekt bei der Festlegung der aktuellen Farbe Verwendung findet. Die init()-Methode in den Zeilen 9–20 wurde neu geschrieben, sodass sie mit einem Parameter namens background arbeitet. Dieser Parameter sollte als hexadezimaler String angegeben werden – ein Doppelkreuz #, gefolgt von drei hexadezimalen Zahlen, die den Rot-, Grün- und Blauanteil der Farbe angeben. Schwarz ist #000000, Rot ist #FF0000, Grün ist #00FF00, Blau ist #0000FF, Weiß ist #FFFFFF usw. Wenn Sie bereits Erfahrungen mit HTML gemacht haben, sind Ihnen derartige hexadezimale Strings sicher vertraut. Die Klasse color hat eine Klassenmethode namens decode(String), die aus einem hexadezimalen String ein Color-Objekt erzeugt. Dies geschieht in Zeile 14 – der try-catch-Block soll einen möglichen NumberFormatException-Fehler auffangen, der aufträte, falls in keinen gültigen Hexadezimal-String beinhaltet.
444
Erstellen von Applets
Zeile 19 setzt das Applet-Fenster auf die Farbe, die durch das back-Objekt repräsentiert wird. Um dieses Programm ausprobieren zu können, benötigen Sie noch das HTMLDokument aus Listing 14.5. Listing 14.5: Der vollständige Quelltext von NewWatch.html 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
Watch Applet The current time: <param name="Code" value="NewWatch.class"> <param name="background" value="#996633"> This program requires a Java-enabled browser.
Das Tag auf dieser Seite hat dieselben classid- und codebase-Attribute, die Sie für alle Ihre Java-2-Applets benutzen sollten. Des Weiteren besitzt es HEIGHT- und WIDTHAttribute, um die Größe des Applet-Fensters anzugeben. In den Zeilen 9–10 werden zwei Parameter angegeben: Der erste gibt den Namen der Klassendatei des Applets an: NewWatch.class. Der zweite hat den Namen background und den Wert #996633, was einem Braunton entspricht. In Listing 14.4 legt Zeile 5 des Applets die Hintergrundfarbe der Seite mithilfe dreier Dezimalwerte fest. Wenn Sie diese HTML-Datei in den Browser laden, sieht das Ganze wie in Abbildung 14.5 aus.
Abbildung 14.5: Die Seite NewWatch.html im Browser
445
Java-Applets erstellen
Da das Applet-Fenster und die Webseite dieselbe Hintergrundfarbe haben, können Sie in Abbildung 14.5 den Umriss des Applets nicht sehen. Falls im HTML-Code, der das NewWatch-Applet lädt, kein Parameter background angegeben wird, ist die Standardvoreinstellung schwarz.
Der HTML-Konverter von Sun Bisher haben Sie zwei Tags kennen gelernt, um Applets präsentieren zu können: <APPLET> und . Es gibt auch noch ein drittes Tag, das in einigen Versionen des Netscape Navigators unterstützt wurde: <EMBED>. Selbst erfahrene Webentwickler haben Mühe, eine Webseite zu erstellen, die all diese Optionen unterstützt. Um dies zu vereinfachen, hat Sun eine Java-Applikation namens HTMLConverter erstellt, die existierende HTML-Seiten so bearbeitet, dass alle Applets über das Java-Plug-In ablaufen. Diese Applikation ist Teil des Java 2 SDK und kann über die Kommandozeile ausgeführt werden. Um den Konverter zu benutzen, erzeugen Sie zuerst eine Webseite, die ein Applet mithilfe des <APPLET>-Tags lädt. Die Applikation HTMLConverter lädt die Seite dann und konvertiert das HTML so, dass das Plug-In benutzt wird. Sobald die Seite erstellt ist, starten Sie HTMLConverter, wobei der Name des zu konvertierenden HTML-Dokuments das Argument ist, z. B.: HTMLConverter Watch3.html
Das vorausgehende Kommando sorgt dafür, dass alle Applets, die in Watch3.html enthalten sind, vom Java-Plug-In interpretiert werden. Der HTMLConverter überschreibt den angegebenen HTML-Code. Sollten Sie aus irgendeinem Grund die Nicht-Plug-In-Version der Seite behalten wollen, sollten Sie eine Kopie von ihr erstellen und den HTMLConverter die Kopie verändern lassen.
14.5 Zusammenfassung Auch wenn Applets nicht mehr das Zentrum der Java-Entwicklung sind, bleiben sie das JavaElement, mit dem am meisten Menschen in Berührung kommen, da auf Tausenden von Websites Applets verwendet werden. Schenkt man Altavista (http://www.altavista.com) Glauben, dann gibt es mehr als 4,6 Millionen Webseiten, die Applets beinhalten.
446
Workshop
Da sie in Webseiten ausgeführt und angezeigt werden, können Applets die Grafik, die Benutzerschnittstelle und die Ereignisstruktur des Webbrowsers verwenden. Diese Möglichkeiten bieten dem Applet-Programmierer eine große Menge an Funktionalität ohne große Plackerei. Heute haben Sie die Grundlagen der Applet-Erstellung erlernt:
Alle Applets, die Sie mit Java schreiben, sind Subklassen der Klasse javax.swing.JApplet, die die Verhaltensweisen dafür bietet, dass das Applet in einem Browser ausgeführt werden kann.
Applets haben fünf Hauptmethoden, die für grundlegende Aktivitäten eines Applets während seines Lebenszyklus verwendet werden: init(), start(), stop(), destroy() und paint(). Diese Methoden werden überschrieben, um bestimmte Funktionalitäten in einem Applet zu bieten.
Applets binden Sie über die Tags <APPLET> bzw. in eine HTML-Webseite ein, und das Tag kann benutzt werden, um mithilfe von Parametern festzulegen, wie das Applet funktioniert.
Um das Herunterladen von Applets auf eine Webseite zu beschleunigen, können Sie Java-Archivdateien benutzen.
Applets können von einer Webseite Informationen durch das -Tag erhalten. Im Körper des Applets können Sie auf diese Parameter mit der Methode getParameter() zugreifen.
Wenn Sie Features von Java 2 in Ihren Applets benutzen wollen, können Sie ein HTML-Dokument erstellen, das das Java-Plug-In anstelle des in den Browser eingebauten Interpreters benutzt.
14.6 Workshop Fragen und Antworten F
Ich habe ein Applet, das Parameter erwartet, und eine HTML-Datei, die diese Parameter übergibt. Beim Ausführen meines Applets erhalte ich aber nur null-Werte. Woran liegt das? A
Stimmen die Namen der Parameter (im NAME-Attribut) genau mit denen überein, die Sie mit getParameter() prüfen? Sie müssen völlig übereinstimmen, auch hinsichtlich der Groß- und Kleinschreibung. Achten Sie darauf, dass Ihre Tags innerhalb von öffnenden und schließenden <APPLET>-Tags stehen und dass kein Name falsch geschrieben wurde.
447
Java-Applets erstellen
F
Wie kann ich, obwohl Applets nicht über eine Kommandozeile oder einen Standard-Ausgabestream verfügen, eine einfache Debugging-Ausgabe wie System.out.println() in einem Applet vornehmen? A
Je nach Browser oder Java-fähiger Umgebung verfügen Sie über ein Konsolenfenster, in dem die Debugging-Ausgabe (die Ausgabe System.out.println()) erscheint, oder sie wird in einer Log-Datei gespeichert (Netscape verfügt im »Optionen«Menü über eine Java-Konsole, der Internet Explorer verwendet eine Java-Log-Datei, die Sie durch »Optionen/Erweitert« aktivieren können). Sie können in den Applets weiterhin Meldungen mit System.out.println() ausgeben, sollten diese aber anschließend entfernen, damit Sie den Anwender nicht verwirren!
F
Ich versuche, ein Applet auszuführen, aber ich sehe nur einen grauen Kasten. Kann ich die Fehlermeldungen, die dieses Applet erzeugt, irgendwo einsehen? A
Wenn Sie das Java-Plug-In zum Interpretieren des Applets benutzen, können Sie Fehlermeldungen und andere Informationen sehen, indem Sie die Java-Konsole öffnen. Unter Windows klicken Sie die Java-Tasse im Systemtray doppelt an. Einige Netscape-Versionen machen das Java-Ausgabefenster über ein PulldownMenü-Kommando einsehbar. Mozilla bietet eine Java-Konsole, wenn das JavaPlug-In installiert ist: Wählen Sie »Tools/Web Development/Java Console«.
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Welche Klasse sollte ein Applet erben, wenn Swing-Features im Programm benutzt werden? (a) java.applet.Applet (b) javax.applet.JApplet (c) beide 2. Welche Methode wird aufgerufen, wenn ein Applet-Fenster verschwindet und neugezeichnet werden muss? (a) start() (b) init() (c) paint()
448
Workshop
3. Was passiert, wenn Sie ein Java-2-Applet mit dem <APPLET>-Tag auf eine Webseite setzen und ein Internet Explorer ohne Java-Plug-In die Seite laden will? (a) Das Applet läuft problemlos. (b) Das Applet läuft nicht, stattdessen wird ein leerer grauer Kasten dargestellt. (c) Dem Besucher wird angeboten, das Java-Plug-In herunterzuladen und zu installieren.
Antworten 1. b. Wenn Sie die Verbesserungen von Swing in den Bereichen Benutzerschnittstelle und Event-Handling verwenden wollen, muss das Applet eine Subklasse von JApplet sein. 2. c. Sie können auch die Wiederanzeige des Applet-Fensters anfordern, indem Sie die repaint()-Methode des Applets aufrufen. 3. b. Das Applet funktioniert nicht, weil der Java-Interpreter des Internet Explorers keine Java-2-Applets unterstützt. Dem Besucher wird nur dann der Download und die Installation des Java-Plug-Ins vorgeschlagen, wenn Sie das -Tag benutzen.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Angenommen, Sie wollen ein Applet mit einer grafischen Benutzerschnittstelle erzeugen. Welche Methode sollten Sie überschreiben, um grafische Schnittstellenkomponenten zu erzeugen und dem Applet hinzuzufügen? a. paint(Graphics) b. start() c. stop() d. init() Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 14, und klicken Sie auf den Link »Certification Practice«.
449
Java-Applets erstellen
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Verbessern Sie das NewWatch-Applet so, dass auch die Textfarbe über einen Parameter gesetzt werden kann.
Erstellen Sie ein Applet, das mithilfe von Textfeldern zwei Zahlen als Eingabe nimmt, sie nach dem Klicken auf einen Button namens »Add« addiert und dann das Ergebnis ausgibt.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Webseite zum Buch: http://www.java21pro.com.
450
Tag 1
Einstieg in Java
33
Tag 2
Das Programmier-ABC
63
Tag 3
Arbeiten mit Objekten
93
Tag 4
Arrays, Bedingungen und Schleifen
117
Tag 5
Klassen und Methoden erstellen
145
Tag 6
Pakete, Schnittstellen und andere Klassen-Features
177
Tag 7
Threads und Ausnahmen
219
Tag 8
Datenstrukturen
253
Tag 9
Der Gebrauch von Swing
277
Tag 10
Die Erstellung einer Swing-Schnittstelle
307
Tag 11
Komponenten auf einer Benutzerschnittstelle anordnen
335
Tag 12
Auf Benutzereingaben reagieren
363
Tag 13
Farbe, Schriften und Grafiken
395
Tag 14
Java-Applets erstellen
421
Tag 15
Mit Eingaben und Ausgaben arbeiten
453
Tag 16
Objekt-Serialisation und -Inspektion
481
Tag 17
Kommunikation über das Internet
509
Tag 18
JavaSound
545
Tag 19
JavaBeans
565
Tag 20
Daten mit JDBC lesen und schreiben
587
Tag 21
XML-Daten lesen und schreiben
611
W O C H E
W O C H E
W O C H E
452
Mit Eingaben und Ausgaben arbeiten
5 1
Mit Eingaben und Ausgaben arbeiten
Viele Java-Programme müssen mit Datenquellen interagieren. Man kann auf vielerlei Art und Weise Informationen auf einem Computersystem speichern, z. B. als Dateien auf der Festplatte oder einer CD-ROM, als Seiten auf einer Website oder im RAM des Computers. Man könnte erwarten, dass diese verschiedenen Speichergeräte mit unterschiedlichen Techniken bedient werden. Glücklicherweise ist dies nicht so. In Java erfolgt das Speichern und Auslesen von Informationen über ein Kommunikationssystem namens Streams, das im java.io-Paket implementiert ist. Heute lernen Sie, wie man Eingabestreams zum Lesen und Ausgabestreams zum Speichern von Information erzeugt. Sie werden sich im Einzelnen beschäftigen mit:
Bytestreams, die man für Bytes, Integer und andere einfache Datentypen benutzt
Zeichenstreams, die man für Textdateien und andere Textquellen benutzt
Sie können mit allen Daten in der gleichen Weise umgehen, sobald Sie wissen, wie ein Eingabestream funktioniert, unabhängig davon, ob die Daten von einer Festplatte, aus dem Internet oder von einem anderen Programm kommen. Das Gleiche gilt mutatis mutandis für Ausgabestreams. Java 2 Version 1.4 enthält als Neuerung java.nio, ein Paket für die fortgeschrittene Programmierung von Input und Output. Da dieses Paket vor allem bei der Netzwerkprogrammierung nützlich ist, sehen wir es uns erst an Tag 17 näher an.
15.1 Einführung in Streams In Java werden alle Daten mit Streams geschrieben und gelesen. Streams tragen wie Wasserströmungen etwas von einem Ort zum anderen. Ein Stream ist ein Pfad, den Daten in einem Programm zurücklegen. Ein Eingabestream sendet Daten aus einer Quelle in ein Programm. Ein Ausgabestream sendet Daten aus einem Programm an ein Ziel. Heute haben Sie es mit zwei Arten von Streams zu tun: Bytestreams und Zeichenstreams. Bytestreams befördern Integer mit Werten zwischen 0 und 255. Es können vollkommen unterschiedliche Daten im Byteformat dargestellt werden, z. B. numerische Daten, ausführbare Programme, Internetverbindungen und Bytecode – die Klassendateien, die eine Java Virtual Machine ausführen kann. Man kann jede nur vorstellbare Art von Daten entweder mit individuellen Bytes oder einer Reihe von Bytes, die miteinander kombiniert sind, ausdrücken.
454
Einführung in Streams
Zeichenstreams sind ein spezieller Typ von Bytestream, der nur Textdaten verarbeitet. Sie unterscheiden sich von Bytestreams, weil Javas Zeichensatz Unicode unterstützt. Dieser Standard umfasst viel mehr Zeichen, als man mit Bytes ausdrücken könnte. Alle Arten von Daten, die mit Text zu tun haben, sind leichter als Zeichenstreams zu implementieren, so etwa Textdateien, Webseiten und andere gebräuchliche Textsorten.
Einen Stream verwenden Ob Sie einen Bytestream oder einen Zeichenstream verwenden: Die Prozedur, um sie zu verwenden, ist weitgehend identisch. Bevor wir uns die Einzelheiten der java.io-Klassen ansehen, gehen wir kurz den Prozess der Erstellung und der Verwendung von Streams durch. Bei einem Eingabestream erstellt man zuerst ein Objekt, das mit der Datenquelle assoziiert ist. Wenn die Quelle beispielsweise eine Datei auf Ihrer Festplatte ist, könnte ein FileInputStream-Objekt mit dieser Datei assoziiert werden. Wenn Sie dieses Streamobjekt haben, können Sie durch Verwendung einer der Methoden des Objekts Daten aus dem Stream lesen. FileInputStream hat eine read()-Methode, die ein aus der Datei gelesenes Byte zurückgibt. Wenn Sie das Lesen von Informationen aus diesem Stream beendet haben, rufen Sie die close()-Methode auf, um anzugeben, dass Sie fertig sind.
Bei einem Ausgabestream beginnen Sie mit der Erstellung eines Objekts, das mit dem Ziel der Daten assoziiert ist. Dieses Objekt kann aus der BufferedWriter-Klasse erstellt werden, die einen effizienten Weg zur Erstellung von Textdateien darstellt. Die write()-Methode ist der einfachste Weg, um Informationen an das Ziel des Ausgabestreams zu schicken. Beispielsweise kann eine write()-Methode eines BufferedWriter einzelne Zeichen an einen Ausgabestream schicken. Wie bei Eingabestreams ruft man die close()-Methode eines Ausgabestreams auf, wenn Sie keine weiteren Informationen senden wollen.
Einen Stream filtern Die einfachste Möglichkeit, einen Stream zu verwenden, besteht darin, ihn zu erzeugen und dann seine Methoden aufzurufen, um Daten zu senden oder zu empfangen, je nachdem, ob es sich um einen Ausgabe- oder Eingabestream handelt.
455
Mit Eingaben und Ausgaben arbeiten
Viele der Klassen, mit denen Sie es heute zu tun haben werden, erzielen bessere Ergebnisse, indem sie einen Filter mit einem Stream assoziieren, bevor sie Daten schreiben oder lesen. Ein Filter ist ein Streamtyp, der die Art verändert, wie mit einem existenten Stream umgegangen wird. Stellen Sie sich einen Damm über einem Bergfluss vor. Der Damm reguliert das Fließen des Wassers von oben nach unten. Der Damm ist eine Art Filter – wenn man ihn entfernt, fließt das Wasser weniger reguliert. Die Prozedur, um einen Filter auf einen Stream anzuwenden, sieht folgendermaßen aus:
Erzeugen Sie einen Stream, der mit einer Datenquelle oder einem Datenziel assoziiert ist.
Assoziieren Sie einen Filter mit diesem Stream.
Lesen oder schreiben Sie Daten von diesem Filter statt vom ursprünglichen Stream.
Die Methoden, die Sie bei einem Filter aufrufen, sind dieselben Methoden wie bei einem Stream: Es gibt wie bei einem ungefilterten Stream read()- und write()-Methoden. Man kann auch einen Filter mit einem anderen Filter assoziieren, sodass der folgende Pfad für eine Information möglich ist: Ein Eingabestream ist mit einer Textdatei assoziiert. Dieser Stream wird durch einen Spanisch-in-Englisch-Übersetzungsfilter gejagt, der dann durch einen Fluchfilter geschickt wird. Schließlich wird das Ganze an sein Ziel geschickt – einen Menschen, der den Text liest. Wenn Ihnen das so abstrakt immer noch verwirrend erscheint, werden Sie in den nächsten Abschnitten ausreichend Gelegenheit haben, dies in der Praxis zu erleben.
15.2 Ausnahmen Es gibt einige Ausnahmen im Paket java.io, die bei der Arbeit mit Dateien und Streams vorkommen können. Die Ausnahme FileNotFound tritt auf, wenn Sie versuchen, einen Stream oder ein DateiObjekt zu erzeugen und dabei eine Datei verwenden, die nicht gefunden werden konnte. EOFException bedeutet, dass das Ende einer Datei unverhofft angetroffen wurde, während Daten aus einer Datei mittels eines Eingabestreams gelesen wurden.
Diese Ausnahmen sind Unterklassen von IOException. Man kann mit diesen Ausnahmen umgehen, indem man alle Ein- und Ausgabeanweisungen mit einem try-catch-Block umschließt, der IOException-Objekte auffängt. Rufen Sie die toString()-Methode der Ausnahme im catch-Block auf, um mehr über das Problem herauszufinden.
456
Bytestreams
15.3 Bytestreams Alle Bytestreams sind entweder Unterklassen von InputStream oder von OutputStream. Diese Klassen sind abstrakt. Sie können also keine Streams erzeugen, indem Sie direkt Objekte dieser Klassen erzeugen. Stattdessen erzeugen Sie Streams durch eine ihrer Unterklassen, wie den folgenden:
FileInputStream und FileOutputStream – Bytestreams, die in Dateien auf Festplatte, CD-ROM und anderen Speichermedien gespeichert sind
DataInputStream und DataOutputStream – ein gefilterter Bytestream, aus dem Daten wie Integer oder Fließkommazahlen gelesen werden können
InputStream ist die Superklasse aller Eingabestreams.
Dateistreams Am häufigsten werden Sie mit Dateistreams arbeiten, mit denen man Daten mit Dateien auf Festplatten, CD-ROMs oder anderen Speichermedien austauscht, auf die man mit einem Ordnerpfad und einem Dateinamen zugreifen kann. An einen Datei-Ausgabestream sendet man Bytes, und von einem Datei-Eingabestream empfängt man Bytes.
Datei-Eingabestreams Ein Datei-Eingabestream wird mit dem Konstruktor FileInputStream(String) erzeugt. Das String-Argument ist der Name der Datei. Sie können den Dateinamen mit einer Pfadangabe versehen, sodass die Datei auch in einem anderen als dem Ordner liegen kann, in dem die ladende Klasse gespeichert ist. Die folgende Anweisung erzeugt einen Datei-Eingabestream von der Datei scores.dat: FileInputStream fis = new FileInputStream("scores.dat");
Nachdem Sie einen Datei-Eingabestream erzeugt haben, können Sie durch einen Aufruf seiner read()-Methode Bytes aus ihm lesen. Diese Methode gibt einen Integer zurück, der das nächste Byte im Stream enthält. Wenn die Methode -1 zurückgibt, was kein möglicher Byte-Wert ist, bedeutet das, dass das Ende des Dateistreams erreicht ist. Um mehr als ein Byte aus dem Stream zu lesen, können Sie seine read(byte[], int, int)Methode aufrufen. Die Argumente dieser Methode sind folgende: 1. ein Byte-Array, in dem die Daten gespeichert werden sollen
457
Mit Eingaben und Ausgaben arbeiten
2. das Element innerhalb des Arrays, in dem das erste Byte der Daten gespeichert werden soll 3. die Anzahl der Bytes, die gelesen werden sollen Im Gegensatz zu anderen read()-Methoden gibt diese keine gelesenen Daten zurück. Stattdessen gibt sie einen Integer zurück, der die Anzahl der gelesenen Bytes repräsentiert, oder -1, wenn keine Bytes gelesen wurden, bevor das Ende des Streams erreicht wurde. Die folgenden Anweisungen benutzen eine while-Schleife, um Daten in ein FileInputStream-Objekt namens diskfile zu lesen: int newByte = 0; while (newByte != -1) { newByte = diskfile.read(); System.out.print(newByte + " "); }
Diese Schleife liest die ganze durch diskfile referenzierte Datei byteweise und zeigt alle Bytes durch Leerzeichen getrennt an. Sie gibt -1 aus, wenn das Dateiende erreicht ist – dies können Sie problemlos mit einer if-Anweisung abfangen. Die Applikation ReadBytes in Listing 15.1 benutzt eine ähnliche Technik, um einen DateiEingabestream zu lesen. Die close()-Methode des Eingabestreams wird benutzt, um den Stream zu schließen, sobald das letzte Byte der Datei gelesen wurde. Das ist notwendig, um Systemressourcen freizugeben, die der offenen Datei zugeordnet sind. Listing 15.1: Der vollständige Quelltext von ReadByte.java 1: import java.io.*; 2: 3: public class ReadBytes { 4: public static void main(String[] arguments) { 5: try { 6: FileInputStream file = new 7: FileInputStream("class.dat"); 8: boolean eof = false; 9: int count = 0; 10: while (!eof) { 11: int input = file.read(); 12: System.out.print(input + " "); 13: if (input == -1) 14: eof = true; 15: else 16: count++; 17: } 18: file.close();
458
Bytestreams
19: 20: 21: 22: 23: 24: }
}
System.out.println("\nBytes read: " + count); catch (IOException e) { System.out.println("Error -- " + e.toString());
} }
Wenn Sie dieses Programm ausführen, erhalten Sie folgende Fehlermeldung: Error -- java.io.FileNotFoundException: class.dat (The system cannot find the file specified).
Diese Fehlermeldung sieht wie die Ausnahmen aus, die vom Compiler erzeugt werden, tatsächlich stammt sie jedoch aus dem catch-Block in den Zeilen 20–22 der ReadBytesApplikation. Die Ausnahme wird in den Zeilen 6–7 ausgeworfen, weil die class.dat-Datei nicht gefunden werden kann. Sie benötigen eine Datei mit Bytes, die gelesen werden soll. Das kann eine beliebige Datei sein – eine Möglichkeit ist die Klassendatei des Programms, die die Bytecode-Befehle enthält, die die Java Virtual Machine ausführt. Erzeugen Sie diese Datei, indem Sie eine Kopie der ReadBytes.class anfertigen und die Kopie in class.dat umbenennen. ReadBytes.class selbst dürfen Sie nicht umbenennen, da das Programm ansonsten nicht läuft. Benutzer von Windows können class.dat mit der MS-DOS-Eingabeaufforderung erzeugen. Rufen Sie den Ordner ReadBytes.class auf, und geben Sie folgendes DOS-Kommando ein: copy ReadBytes.class class.dat
UNIX-Benutzer können folgenden Befehl in die Kommandozeile eintippen: cp ReadBytes.class class.dat
Wenn Sie das Programm ausführen, wird jedes Byte von class.dat angezeigt. Am Ende wird die Gesamtzahl der Bytes ausgegeben. Wenn Sie class.dat mithilfe von ReadBytes.class erzeugt haben, sollten die letzten Zeilen der Ausgabe folgendermaßen aussehen: 177 0 1 0 0 0 96 0 99 0 17 0 1 0 25 0 0 0 62 0 15 0 0 0 6 0 10 0 8 0 12 0 9 0 14 0 10 0 17 0 11 0 23 0 12 0 49 0 13 0 55 0 14 0 60 0 16 0 63 0 10 0 67 0 18 0 71 0 19 0 96 0 20 0 99 0 21 0 128 0 23 0 1 0 28 0 0 0 2 0 29 -1 Bytes read: 953
Die Anzahl der Bytes, die pro Zeile angezeigt werden, hängt von der Spaltenbreite ab, die Text auf Ihrem System einnehmen kann. Welche Bytes angezeigt werden, hängt davon ab, welche Datei Sie zur Erstellung von class.dat verwendet haben.
459
Mit Eingaben und Ausgaben arbeiten
Datei-Ausgabestreams Ein Datei-Ausgabestream wird mit dem Konstruktor FileOutputStream(String) erzeugt. Die Verwendung entspricht der des Konstruktors FileInputStream(String). Sie können also den Dateinamen mit einer Pfadangabe versehen. Sie müssen bei der Angabe einer Datei, in die ein Ausgabestream geschrieben werden soll, Vorsicht walten lassen. Entspricht der Dateiname einem existenten File, dann wird das Original gelöscht, sobald Daten in den Stream geschrieben werden. Sie können einen Datei-Ausgabestream erzeugen, der Daten hinter das Ende eines existenten Files setzt, indem Sie den Konstruktor FileOutputStream(String, boolean) verwenden. Der String gibt die Datei an, und das boolesche Argument sollte true sein, damit Daten angehängt und keine existenten Daten überschrieben werden. Die write(int)-Methode des Datei-Ausgabestreams wird verwendet, um Bytes in den Stream zu schreiben. Nachdem das letzte Byte in die Datei geschrieben wurde, wird der Stream durch seine close()-Methode geschlossen. Um mehr als ein Byte zu schreiben, findet die write(byte[], int, int)-Methode Anwendung. Sie funktioniert analog zur bereits beschriebenen read(byte[], int, int)-Methode. Die Argumente dieser Methode sind das Byte-Array mit den auszugebenden Bytes, der Startpunkt im Array und die Zahl der zu schreibenden Bytes. Die Applikation WriteBytes in Listing 15.2 schreibt ein Integer-Array in einem Datei-Ausgabestream. Listing 15.2: Der volständige Quelltext von WriteBytes.java 1: import java.io.*; 2: 3: public class WriteBytes { 4: public static void main(String[] arguments) { 5: int[] data = { 71, 73, 70, 56, 57, 97, 15, 0, 15, 0, 6: 128, 0, 0, 255, 255, 255, 0, 0, 0, 44, 0, 0, 0, 7: 0, 15, 0, 15, 0, 0, 2, 33, 132, 127, 161, 200, 8: 185, 205, 84, 128, 241, 81, 35, 175, 155, 26, 9: 228, 254, 105, 33, 102, 121, 165, 201, 145, 169, 10: 154, 142, 172, 116, 162, 240, 90, 197, 5, 0, 59 } ; 11: try { 12: FileOutputStream file = new 13: FileOutputStream("pic.gif"); 14: for (int i = 0; i < data.length; i++) 15: file.write(data[i]); 16: file.close(); 17: } catch (IOException e) { 18: System.out.println("Error -- " + e.toString());
460
Einen Stream filtern
19: 20: 21: }
} }
Im Programm geschieht Folgendes:
Zeile 5–10: Ein Integer-Array namens data wird mit 66 Elementen erzeugt.
Zeile 12–13: Ein Datei-Ausgabestream wird mit dem Namen pic.gif im selben Ordner wie die WriteBytes.class-Datei erzeugt.
Zeile 14–15: Eine for-Schleife wird benutzt, um durch das data-Array zu laufen und die einzelnen Elemente in den Dateistream zu schreiben.
Zeile 16: Der Datei-Ausgabestream wird geschlossen.
Nachdem Sie das Programm ausgeführt haben, können Sie die Datei pic.gif mit einem Browser oder Grafikprogramm ansehen. Es ist ein kleines Bildchen im GIF-Format (Abbildung 15.1).
Abbildung 15.1: Die Datei pic.gif (vergrößert)
15.4 Einen Stream filtern Gefilterte Streams (Filtered Streams) sind Streams, die die Informationen modifizieren, die durch einen existenten Stream laufen. Man erzeugt sie mit einer der Unterklassen FilterInputStream oder FilterOutputStream. Diese Klassen nehmen selbst keine Filterungen vor. Dafür haben sie Unterklassen wie BufferInputStream und DataOutputStream, die spezielle Filterungen vornehmen.
Byte-Filter Information wird schneller übermittelt, wenn sie in großen Teilen verschickt werden kann, selbst dann, wenn diese großen Teile schneller empfangen als verarbeitet werden. Überlegen Sie sich, welche Art der Buchlektüre schneller ist:
Sie leihen sich ein Buch von einem Freund und lesen es durch.
Ihr Freund gibt Ihnen immer nur eine Seite des Buchs und rückt die nächste Seite nicht heraus, bevor Sie die vorherige durchgelesen haben.
461
Mit Eingaben und Ausgaben arbeiten
Offensichtlich ist der erste Weg schneller und effizienter. Das lässt sich auf die gepufferten Streams von Java übertragen. Ein Puffer (Buffer) ist ein Speicher, in dem Daten aufbewahrt werden können, bevor sie von einem Programm benötigt werden, das Daten liest oder schreibt. Durch die Verwendung eines Puffers können Sie an die Daten gelangen, ohne dass Sie ständig auf die ursprüngliche Datenquelle zurückgreifen müssen.
Gepufferte Streams Ein gepufferter Eingabestream füllt einen Puffer mit noch nicht verarbeiteten Daten. Wenn ein Programm diese Daten benötigt, sieht es zuerst in den Puffer, bevor es auf die ursprüngliche Streamquelle zurückgreift. Gepufferte Bytestreams verwenden die Klassen BufferedInputStream und BufferedOutputStream.
Ein gepufferter Eingabestream wird mit einem der beiden folgenden Konstruktoren erzeugt:
BufferedInputStream(InputStream) – erzeugt einen gepufferten Eingabestream für das angegebene InputStream-Objekt.
BufferedInputStream(InputStream, int) – erzeugt den angegebenen gepufferten InputStream mit einer Puffergröße von int.
Die einfachste Art, um Daten aus einem gepufferten Eingabestream zu lesen, besteht darin, seine read()-Methode argumentenlos aufzurufen, die normalerweise einen Integer zwischen 0 und 255 zurückgibt, der das nächste Byte im Stream repräsentiert. Wenn das Ende des Streams erreicht wurde und kein Byte mehr übergeben werden kann, wird -1 zurückgegeben. Sie können auch die read(byte[], int, int)-Methode verwenden, die Sie von anderen Eingabestreams kennen und die die Streamdaten in ein Byte-Array lädt. Ein gepufferter Ausgabestream wird mithilfe eines dieser beiden Konstruktoren erzeugt:
BufferedOutputStream(OutputStream) – erzeugt einen gepufferten Ausgabestream für
das angegebene OutputStream-Objekt.
BufferedOutputStream(OutputStream, int) – erzeugt den angegebenen gepufferten OutputStream mit einem Puffer der Größe int.
Die Methode write(int) des Ausgabestreams kann verwendet werden, um einzelne Bytes an den Stream zu schicken. Die Methode write(byte[], int, int) schickt eine Gruppe von Bytes aus dem angegebenen Byte-Array, wobei die Argumente das Byte-Array, der Startpunkt im Byte-Array und die Zahl der zu schreibenden Bytes sind.
462
Einen Stream filtern
Die Methode write() akzeptiert zwar einen Integer als Argument, der Wert sollte jedoch zwischen 0 und 255 liegen. Wenn Sie eine Zahl über 255 angeben, wird der Rest nach einer Teilung durch 256 gespeichert. Sie können dies ausprobieren, wenn Sie das Projekt ausführen, das wir später am heutigen Tag erstellen werden. Wenn die Daten an einen gepufferten Stream gerichtet sind, werden sie nicht an ihr Ziel geschickt, bevor der Stream voll ist oder die Methode flush() des gepufferten Streams aufgerufen wird. Das nächste Projekt, die Applikation BufferDemo, schreibt eine Serie von Bytes in einen gepufferten Ausgabestream, der mit einer Textdatei assoziiert ist. Der erste und der letzte Integer der Serie werden durch zwei Kommandozeilenargumente angegeben, wie im folgenden SDK-Kommando: java BufferDemo 7 64
Nachdem in die Textdatei geschrieben wurde, erzeugt BufferDemo einen gepufferten Eingabestream aus der Datei und liest die Bytes wieder ein. Listing 15.3 enthält den Quellcode. Listing 15.3: Der vollständige Quellcode von BufferDemo.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
import java.io.*; public class BufferDemo { public static void main(String[] arguments) { int start = 0; int finish = 255; if (arguments.length > 1) { start = Integer.parseInt(arguments[0]); finish = Integer.parseInt(arguments[1]); } else if (arguments.length > 0) start = Integer.parseInt(arguments[0]); ArgStream as = new ArgStream(start, finish); System.out.println("\nWriting: "); boolean success = as.writeStream(); System.out.println("\nReading: "); boolean readSuccess = as.readStream(); } } class ArgStream { int start = 0; int finish = 255; ArgStream(int st, int fin) { start = st; finish = fin; }
463
Mit Eingaben und Ausgaben arbeiten
28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: }
boolean writeStream() { try { FileOutputStream file = new FileOutputStream("numbers.dat"); BufferedOutputStream buff = new BufferedOutputStream(file); for (int out = start; out <= finish; out++) { buff.write(out); System.out.print(" " + out); } buff.close(); return true; } catch (IOException e) { System.out.println("Exception: " + e.getMessage()); return false; } } boolean readStream() { try { FileInputStream file = new FileInputStream("numbers.dat"); BufferedInputStream buff = new BufferedInputStream(file); int in = 0; do { in = buff.read(); if (in != -1) System.out.print(" " + in); } while (in != -1); buff.close(); return true; } catch (IOException e) { System.out.println("Exception: " + e.getMessage()); return false; } }
Die Ausgabe des Programms hängt von den beiden Argumenten ab, die in der Kommandozeile angegeben wurden. Wenn Sie 4 und 13 verwenden, erhalten Sie folgende Ausgabe: Writing: 4 5 6 7 8 9 10 11 12 13 Reading: 4 5 6 7 8 9 10 11 12 13
464
Einen Stream filtern
Diese Applikation besteht aus zwei Klassen: BufferDemo und einer Hilfsklasse namens ArgStream. BufferDemo liest die Werte der beiden Argumente aus, falls sie existieren, und benutzt sie im Konstruktor ArgStream(). Die Methode writeStream() von ArgStream wird in Zeile 14 aufgerufen, um eine Reihe von Bytes in einen gepufferten Ausgabestream zu schreiben. Die readStream()-Methode wird in Zeile 16 aufgerufen, um diese Bytes wieder einzulesen. Obwohl writeStream() und readStream() die Daten in zwei entgegengesetzte Richtungen verschieben, sind sie weitgehend identisch. Sie haben folgendes Format:
Der Dateiname numbers.dat wird verwendet, um einen Datei-Eingabe- oder Ausgabestream einzurichten.
Mit dem Dateistream wird ein gepufferter Eingabe- oder Ausgabestream erzeugt.
Die Methode write() des gepufferten Streams wird benutzt, um Daten zu senden, während mit der read()-Methode Daten empfangen werden.
Der gepufferte Stream wird geschlossen.
Da Dateistreams und gepufferte Streams IOExeption-Objekte im Falle eines Fehlers auswerfen, sind alle Operationen, die auf diese Streams zugreifen, in einen entsprechenden try-catch-Block eingeschlossen. Die booleschen Rückgabewerte in writeStream() und readStream() geben an, ob die Streamoperation erfolgreich verlief. Sie werden in diesem Programm nicht verwendet, aber es ist guter Stil, eine Meldung auszugeben, wenn Probleme auftraten.
Konsolen-Eingabestreams Programmierer mit etwas mehr Erfahrung werden bei Java die Fähigkeit vermissen, textliche oder numerische Eingaben während einer laufenden Applikation von der Konsole einzulesen. Es gibt keine Eingabemethode, die man mit den Ausgabemethoden System.out.print() und System.out.println() vergleichen könnte. Sie können jedoch gepufferte Eingabestreams verwenden, um zu Konsoleneingaben zu gelangen. Die Klasse System, Teil des Pakets java.lang, hat eine Klassenvariable namens in, die ein Objekt von InputStream ist. Dieses Objekt erhält Eingaben von der Tastatur über den Stream. Sie verwenden diesen Stream genauso wie jeden anderen Eingabestream. Die folgende Anweisung erzeugt einen neuen gepufferten Eingabestream, der mit dem System.in-Eingabestream assoziiert ist. BufferedInputStream command = new BufferedInputStream(System.in);
465
Mit Eingaben und Ausgaben arbeiten
Das nächste Projekt, die Klasse ConsoleInput, enthält eine Klassenmethode, mit der Sie in jeder beliebigen Java-Applikation Konsoleneingaben empfangen können. Geben Sie den Text von Listing 15.4 ein, und speichern Sie die Datei als ConsoleInput.java. 1: import java.io.*; 2: 3: public class ConsoleInput { 4: public static String readLine() { 5: StringBuffer response = new StringBuffer(); 6: try { 7: BufferedInputStream buff = new 8: BufferedInputStream(System.in); 9: int in = 0; 10: char inChar; 11: do { 12: in = buff.read(); 13: inChar = (char) in; 14: if (in != -1) { 15: response.append(inChar); 16: } 17: } while ((in != -1) & (inChar != ‘\n’)); 18: buff.close(); 19: return response.toString(); 20: } catch (IOException e) { 21: System.out.println("Exception: " + e.getMessage()); 22: return null; 23: } 24: } 25: 26: public static void main(String[] arguments) { 27: System.out.print("\nWhat is your name? "); 28: String input = ConsoleInput.readLine(); 29: System.out.println("\nHello, " + input); 30: } 31: }
Die Klasse ConsoleInput hat eine main()-Methode, die ihre Verwendung demonstriert. Wenn Sie das Programm kompiliert und als Applikation gestartet haben, sollte die Ausgabe ungefähr so aussehen: What is your name? Amerigo Vespucci Hello, Amerigo Vespucci
466
Einen Stream filtern
Datenstreams Wenn Sie mit Daten arbeiten müssen, die nicht als Bytes oder Zeichen repräsentiert werden können, können Sie Daten-Eingabe- und -Ausgabestreams verwenden. Diese Streams filtern einen existenten Bytestream so, dass die primitiven Typen boolean, byte, double, float, int, long und short direkt aus dem Stream gelesen oder in ihn geschrieben werden können. Ein Daten-Eingabestream wird mit dem DataInputStream(InputStream)-Konstruktor erzeugt. Das Argument sollte ein existenter Eingabestream sein, z. B. ein gepufferter Eingabestream oder ein Datei-Eingabestream. Umgekehrt hat ein Daten-Ausgabestream den Konstruktor DataOutputStream(OutputStream), der den assoziierten Ausgabestream angibt. Die folgende Liste nennt die Lese- und Schreibmethoden, die sich auf Daten-Eingabebzw. Ausgabestreams beziehen:
readBoolean(), writeBoolean(boolean)
readByte(), writeByte(integer)
readDouble(), writeDouble(double)
readFloat(), writeFloat(float)
readInt(), writeInt(int)
readLong(), writeLong(long)
readShort(), writeShort(int)
Die Eingabemethoden geben jeweils den primitiven Datentyp aus, der im Namen der Methode vorkommt. Zum Beispiel gibt die readFloat()-Methode einen float-Wert zurück. Es gibt auch die readUnsignedByte()- und readUnsignedShort()-Methoden, die vorzeichenlose Byte- und Short-Werte lesen. Diese Datentypen werden von Java nicht unterstützt, sodass sie als Integerwerte zurückgegeben werden. Vorzeichenlose Bytes haben Werte von 0 bis 255. Das ist ein Unterschied zum Byte-Variablentyp von Java, in dem Werte zwischen -127 und 128 gespeichert werden. Genauso laufen die Werte eines vorzeichenlosen short von 0 bis 65535, statt von -32768 bis 32767 wie bei Java-short. Nicht jede der verschiedenen Lesemethoden eines Datei-Eingabestreams gibt einen Wert zurück, der als Indikator dafür dienen kann, dass das Ende des Streams erreicht wurde. Alternativ können Sie warten, bis eine EOFException (EOF = end of file, Dateiende) ausgeworfen wird. Dies geschieht, sobald eine Lesemethode das Ende des Streams erreicht. Die
467
Mit Eingaben und Ausgaben arbeiten
Schleife, die die Daten liest, kann in einen try-Block eingeschlossen werden. Das zugehörige catch-Statement sollte sich nur um EOFException-Objekte kümmern. Sie können die Methode close() des Streams aufrufen und dann weitere Aufräumarbeiten im catch-Block erledigen. Dies wird im nächsten Projekt gezeigt. Die Listings 15.5 und 15.6 enthalten zwei Programme, die Datenstreams verwenden. Die Applikation WritePrimes schreibt die ersten 400 Primzahlen als Integer in eine Datei namens 400primes.dat. Die Applikation ReadPrimes liest die Integer aus dieser Datei und zeigt sie an. Listing 15.4: Listing 15.5: Der vollständige Quelltext von WritePrimes.java 1: import java.io.*; 2: 3: class WritePrimes { 4: public static void main(String[] arguments) { 5: int[] primes = new int[400]; 6: int numPrimes = 0; 7: // candidate: the number that might be prime 8: int candidate = 2; 9: while (numPrimes < 400) { 10: if (isPrime(candidate)) { 11: primes[numPrimes] = candidate; 12: numPrimes++; 13: } 14: candidate++; 15: } 16: 17: try { 18: // Write output to disk 19: FileOutputStream file = new 20: FileOutputStream("400primes.dat"); 21: BufferedOutputStream buff = new 22: BufferedOutputStream(file); 23: DataOutputStream data = new 24: DataOutputStream(buff); 25: 26: for (int i = 0; i < 400; i++) 27: data.writeInt(primes[i]); 28: data.close(); 29: } catch (IOException e) { 30: System.out.println("Error -- " + e.toString()); 31: } 32: } 33:
468
Einen Stream filtern
34: 35: 36: 37: 38: 39: 40: 41: 42: }
public static boolean isPrime(int checkNumber) { double root = Math.sqrt(checkNumber); for (int i = 2; i <= root; i++) { if (checkNumber % i == 0) return false; } return true; }
Listing 15.5: Der vollständige Quelltext von ReadPrimes.java 1: import java.io.*; 2: 3: class ReadPrimes { 4: public static void main(String[] arguments) { 5: try { 6: FileInputStream file = new 7: FileInputStream("400primes.dat"); 8: BufferedInputStream buff = new 9: BufferedInputStream(file); 10: DataInputStream data = new 11: DataInputStream(buff); 12: 13: try { 14: while (true) { 15: int in = data.readInt(); 16: System.out.print(in + " "); 17: } 18: } catch (EOFException eof) { 19: buff.close(); 20: } 21: } catch (IOException e) { 22: System.out.println("Error -- " + e.toString()); 23: } 24: } 25: }
Den größten Teil der WritePrimes-Applikation nimmt der Code ein, der die ersten vierhundert Primzahlen findet. Sobald Sie das Integer-Array mit den ersten 400 Primzahlen haben, schreiben Sie es in den Zeilen 17–31 in einen Daten-Ausgabestream. Diese Applikation ist ein Beispiel für das mehrfache Filtern eines Streams. Dieser Stream wird in drei Schritten erzeugt:
Ein Datei-Ausgabestream wird erzeugt, der mit einer Datei namens 400primes.dat assoziiert ist.
469
Mit Eingaben und Ausgaben arbeiten
Ein neuer gepufferter Ausgabestream wird mit dem Dateistream assoziiert.
Ein neuer Daten-Ausgabestream wird mit dem gepufferten Stream assoziiert.
Die Methode writeInt() des Datenstreams wird benutzt, um Primzahlen in die Datei zu schreiben. Die Applikation ReadPrimes ist einfacher, weil sie nichts mit der Primzahlensuche zu tun hat – sie liest lediglich Integer mithilfe eines Daten-Eingabestreams aus einer Datei. Die Zeilen 6–11 von ReadPrimes sind fast identisch mit Anweisungen in der Applikation WritePrimes, außer dass Eingabe- statt Ausgabeklassen Verwendung finden. Der try-catch-Block, der sich um die EOFException-Objekte kümmert, befindet sich in den Zeilen 13–20. Das eigentliche Laden der Daten findet im try-Block statt. Die Anweisung while(true) erzeugt eine endlose Schleife. Dies ist jedoch unproblematisch, denn es kommt automatisch zu einer EOFException, wenn während des Einlesens der Daten das Ende des Streams erreicht wird. Die Methode readInt() in Zeile 15 liest Integer aus dem Stream. Die letzten Ausgabezeilen der ReadPrimes-Applikation sollten bei Ihnen ungefähr so aussehen: 2137 2141 2143 2153 2161 2179 2203 2207 2213 2221 2237 2239 2243 22 51 2267 2269 2273 2281 2287 2293 2297 2309 2311 2333 2339 2341 2347 2351 2357 2371 2377 2381 2383 2389 2393 2399 2411 2417 2423 2437 2 441 2447 2459 2467 2473 2477 2503 2521 2531 2539 2543 2549 2551 255 7 2579 2591 2593 2609 2617 2621 2633 2647 2657 2659 2663 2671 2677 2683 2687 2689 2693 2699 2707 2711 2713 2719 2729 2731 2741
15.5 Zeichenstreams Da Sie nun wissen, wie man mit Bytestreams verfährt, besitzen Sie bereits fast das gesamte Wissen, um auch mit Zeichenstreams umzugehen. Man verwendet Zeichenstreams für alle Texte im ASCII-Zeichensatz oder in Unicode. Unicode ist ein internationaler Zeichensatz, der ASCII beinhaltet. Beispiele für Dateien, bei denen man mit einem Zeichenstream arbeitet, sind reine Textdateien, HTML-Dokumente oder Java-Quelldateien. Die Klassen, mit denen man diese Streams schreibt und liest, sind alle Unterklassen von Reader und Writer. Diese sollten für alle Texteingaben benutzt werden, anstatt direkt Bytestreams einzusetzen.
470
Zeichenstreams
Textdateien lesen FileReader ist die wichtigste Klasse, die man verwendet, um Zeichenstreams aus einer Datei zu lesen. Diese Klasse erbt von InputStreamReader, die einen Bytestream liest und die Bytes in Integerwerte umwandelt, die Unicode-Zeichen repräsentieren.
Ein Zeichen-Eingabestream wird mit einer Datei durch den Konstruktor FileReader(String) assoziiert. Der String gibt die Datei an, und er kann neben dem Dateinamen auch eine Pfadangabe umfassen. Die folgende Anweisung erzeugt einen neuen FileReader namens look und assoziiert ihn mit einer Textdatei namens index.txt: FileReader look = new FileReader("index.txt");
Wenn Sie einen FileReader haben, können Sie mit folgenden Methoden Zeichen aus der Datei lesen:
read() – gibt das nächste Zeichen im Stream als Integer zurück
read(char[], int, int) – schreibt Zeichen in das angegebene Zeichen-Array mit dem angegebenen Startpunkt und der Zahl der zu lesenden Zeichen.
Die zweite Methode funktioniert wie ähnliche Methoden für Byte-Eingabestream-Klassen. Statt das nächste Zeichen zurückzugeben, gibt sie entweder die Zahl der gelesenen Zeichen zurück oder -1, falls keine Zeichen vor dem Erreichen des Streamendes gelesen werden konnten. Die folgende Methode lädt eine Textdatei mit dem Filereader-Objekt text und zeigt seine Zeichen an: FileReader text = new FileReader("readme.txt"); int inByte; do { inByte = text.read(); if (inByte != -1) System.out.print( (char)inByte ); } while (inByte != -1); System.out.println(""); text.close();
Da die read()-Methode eines Zeichenstreams einen Integer zurückgibt, müssen Sie diesen vor der Anzeige in ein Zeichen casten, in ein Array speichern oder mit ihm einen String erzeugen. Jedes Zeichen hat einen Zahlencode, der seine Position im Unicode-Zeichensatz angibt. Der aus dem Stream gelesene Integer entspricht diesem Zahlencode. Um eine Textzeile zeichenweise zu lesen, können Sie die BufferedReader-Klasse zusammen mit einem FileReader benutzen.
471
Mit Eingaben und Ausgaben arbeiten
Die BufferedReader-Klasse liest einen Zeichen-Eingabestream und puffert ihn aus Effizienzgründen. Sie müssen ein existentes Reader-Objekt haben, um eine gepufferte Version erzeugen zu können. Mit den folgenden Konstruktoren kann ein BufferedReader erzeugt werden:
BufferedReader(Reader) – erzeugt einen gepufferten Zeichenstream, der mit dem angegebenen Reader-Objekt assoziiert ist, wie z. B. FileReader.
BufferedReader(Reader, int) – erzeugt einen gepufferten Zeichenstream, der mit dem angegebenen Reader und einem Puffer der Größe int assoziiert ist.
Ein gepufferter Zeichenstream kann mithilfe der Methoden read() und read(char[], int, int) gelesen werden, die für FileReader beschrieben wurden. Sie können mit readLine() eine Textzeile lesen. Die Methode readLine() gibt ein String-Objekt zurück, das die nächste Textzeile des Streams enthält, wobei die Zeichen, die das Zeilenende angeben, weggelassen werden. Wenn das Streamende erreicht ist, ist der Wert des zurückgegebenen Strings null. Das Ende einer Zeile wird folgendermaßen erklärt:
mit einem Zeilenvorschub-Zeichen ('\n')
mit einem Wagenrücklauf-Zeichen ('\r')
mit einem Wagenrücklauf, gefolgt von einem Zeilenvorschub (»\r\n")
Das Projekt in Listing 15.7 ist eine Java-Applikation, die ihre eigene Quelldatei mit einem gepufferten Zeichenstream liest. Listing 15.6: Der vollständige Quelltext von ReadSource.java 1: import java.io.*; 2: 3: public class ReadSource { 4: public static void main(String[] arguments) { 5: try { 6: FileReader file = new 7: FileReader("ReadSource.java"); 8: BufferedReader buff = new 9: BufferedReader(file); 10: boolean eof = false; 11: while (!eof) { 12: String line = buff.readLine(); 13: if (line == null) 14: eof = true; 15: else 16: System.out.println(line);
472
Zeichenstreams
17: 18: 19: 20: 21: 22: 23: }
}
} buff.close(); catch (IOException e) { System.out.println("Error -- " + e.toString());
} }
Den größten Teil dieses Programms kennen Sie bereits aus früheren Projekten des heutigen Tages:
Zeile 6–7: Eine Eingabequelle wird erzeugt – das FileReader-Objekt, das mit der Datei ReadSource.java assoziiert ist. Zeile 8–9: Ein Puffer-Filter wird mit dieser Eingabequelle assoziiert – das BufferedReader-Objekt buff.
Zeile 11–17: Eine readLine()-Methode wird innerhalb einer while-Schleife verwendet, um die Textdatei zeilenweise zu lesen. Die Schleife endet, sobald die Methode den Wert null zurückgibt.
Die Ausgabe der Applikation ReadSource ist die Textdatei ReadSource.java.
Textdateien schreiben Die Klasse FileWriter dient dazu, einen Zeichenstream in eine Datei zu schreiben. Sie ist eine Unterklasse von OutputStreamWriter, die ein Verhalten zur Umwandlung von Unicode-Zeichencodes in Bytes hat. Es gibt zwei FileWriter-Konstruktoren: FileWriter(String) und FileWriter(String, boolean). Der String gibt den Namen der Datei an, in die der Stream gelenkt werden soll, und kann auch einen Pfad beinhalten. Das optionale boolesche Argument muss true sein, wenn die Datei an eine existente Datei angehängt werden soll. Wie bei anderen Klassen, die Streams schreiben, müssen Sie sorgfältig darauf achten, nicht aus Versehen ein bestehendes File zu überschreiben, wenn Sie Daten anhängen wollen. Man kann drei Methoden von FileWriter verwenden, um Daten in einen Stream zu schreiben:
write(int) – ein Zeichen schreiben
write(char[], int, int) – Zeichen aus dem angegebenen Zeichen-Array schreiben,
wobei zusätzlich der Startpunkt und die Zahl der zu schreibenden Zeichen angegeben werden
write(String, int, int) – Zeichen aus dem angegebenen String schreiben, wobei zusätzlich der Startpunkt und die Zahl der zu schreibenden Zeichen angegeben wird
473
Mit Eingaben und Ausgaben arbeiten
Das folgende Beispiel schreibt einen Zeichenstream in eine Datei unter Verwendung der Klasse FileWriter und der Methode write(int): FileWriter letters = new FileWriter("alphabet.txt"); for (int i = 65; i < 91; i++) letters.write( (char)i ); letters.close();
Mit der close()-Methode wird der Stream geschlossen, nachdem alle Zeichen an die Zieldatei geschickt worden sind. Es folgt die Datei alphabet.txt, die dieser Code erzeugt: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Die Klasse BufferedWriter kann benutzt werden, um einen gepufferten Zeichenstream zu schreiben. Die Objekte dieser Klasse werden mit den Konstruktoren BufferedWriter(Writer) oder BufferedWriter(Writer, int) erzeugt. Das Argument Writer kann eine beliebige Zeichen-Ausgabestream-Klasse sein, wie z. B. FileWriter. Das optionale zweite Argument ist ein Integer, der die Größe des zu benutzenden Puffers angibt. BufferedWriter hat dieselben drei Ausgabemethoden wie FileWriter: write(int), write(char[], int, int) und write(String, int, int).
Eine weitere Ausgabemethode ist newLine(), die das (oder die) bevorzugte(n) ZeilenendeZeichen der benutzten Plattform an das Programm sendet. Die verschiedenen Zeilenende-Markierungen können zu Konvertierungsproblemen führen, wenn Dateien von einem Betriebssystem zu einem anderen transferiert werden, wenn also z. B. ein Windows-XP-Benutzer eine Datei auf einen Linux-Webserver hochlädt. Wenn Sie newLine() statt eines Literals wie '\n' benutzen, ist ihr Programm viel besser zwischen verschiedenen Plattformen übertragbar. Die Methode close() wird aufgerufen, um den gepufferten Zeichenstream zu schließen und sicherzustellen, dass alle gepufferten Daten an das Ziel des Streams geschickt wurden.
15.6 Dateien und Dateinamenfilter In allen bisherigen Beispielen wurde ein String verwendet, um auf die Datei zu verweisen, die in die Streamoperation involviert war. Dies ist meist ausreichend für ein Programm, das Dateien und Streams benutzt, doch wenn Sie Dateien kopieren, umbenennen usw., sollten Sie ein File-Objekt benutzen. File, das ebenfalls Teil des Pakets java.io ist, repräsentiert eine Datei oder einen Pfad. Die folgenden File-Konstruktoren können benutzt werden:
474
Dateien und Dateinamenfilter
File(String) – erzeugt ein File-Objekt mit dem angegebenen Ordner. Es ist kein
Dateiname angegeben, also bezieht sich dies lediglich auf einen Ordner.
File(String, String) – erzeugt ein File-Objekt mit dem angegebenen Pfad und dem
angegebenen Namen.
File(File, String) – erzeugt ein File-Objekt, dessen Pfad durch das angegebene File und dessen Name durch den angegebenen String festgelegt wird.
Sie können mehrere Methoden auf ein File-Objekt anwenden. Die Methode exists() gibt einen booleschen Wert zurück, der angibt, ob die Datei unter dem Namen und dem Pfad existiert, der bei der Erzeugung des File-Objekts angegeben wurde. Wenn die Datei existiert, können Sie mit der length()-Methode einen long-Integer erhalten, der die Größe der Datei in Bytes angibt. Die Methode renameTo(File) benennt die Datei in den Namen um, der durch das FileArgument angegeben wird. Ein boolescher Wert wird zurückgegeben, der angibt, ob die Operation erfolgreich war. Die Methoden delete() und deleteOnExit() werden aufgerufen, um eine Datei oder einen Ordner zu löschen. Die Methode delete() führt einen sofortigen Löschversuch durch und gibt einen booleschen Wert zurück, der angibt, ob das Löschen funktioniert hat. Die Methode deleteOnExit() wartet mit dem Löschversuch, bis der Rest des Programms abgeschlossen ist. Diese Methode gibt keinen Wert zurück – sie könnten mit der Information nichts mehr anfangen –, und das Programm muss irgendwann beendet sein, damit sie zur Ausführung kommt. Die Methode mkdir() wird benutzt, um einen Ordner zu erzeugen, der durch das FileObjekt angegeben wird, als dessen Methode sie aufgerufen wird. Sie gibt einen booleschen Wert zurück, der angibt, ob alles funktioniert hat. Es gibt keine entsprechende Methode zum Löschen der Ordner, weil man delete() sowohl für Ordner als auch für Dateien benutzen kann. Wie bei allen Operationen mit Files ist bei diesen Methoden größte Sorgfalt erforderlich, damit nicht die falschen Dateien oder Ordner gelöscht und keine Daten vernichtet werden. Es gibt keine Methode, um ein gelöschtes File oder Verzeichnis wieder herzustellen. Alle diese Methoden werfen eine SecurityException aus, wenn das Programm nicht die nötige Sicherheitsstufe hat, um die fragliche Operation durchzuführen. Dies sollte in einem try-catch-Block oder einer throws-Klausel in der Methoden-Deklaration abgehandelt werden. Das Programm von Listing 15.8 wandelt den gesamten Text einer Datei in Großbuchstaben um. Das File wird mittels eines gepufferten Eingabestreams zeichenweise eingelesen. Nachdem das Zeichen in einen Großbuchstaben umgewandelt worden ist, wird es mit einem gepufferten Ausgabestream in eine temporäre Datei geschickt. File-Objekte werden
475
Mit Eingaben und Ausgaben arbeiten
anstelle von Strings zur Angabe der involvierten Dateien benutzt, wodurch man die Dateien umbenennen und löschen kann. Listing 15.7: Der vollständige Quelltext von AllCapsDemo.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39:
476
import java.io.*; public class AllCapsDemo { public static void main(String[] arguments) { AllCaps cap = new AllCaps(arguments[0]); cap.convert(); } } class AllCaps { String sourceName; AllCaps(String sourceArg) { sourceName = sourceArg; } void convert() { try { // Create file objects File source = new File(sourceName); File temp = new File("cap" + sourceName + ".tmp"); // Create input stream FileReader fr = new FileReader(source); BufferedReader in = new BufferedReader(fr); // Create output stream FileWriter fw = new FileWriter(temp); BufferedWriter out = new BufferedWriter(fw); boolean eof = false; int inChar = 0; do { inChar = in.read(); if (inChar != -1) {
Zusammenfassung
40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: }
char outChar = Character.toUpperCase( (char)inChar ); out.write(outChar); } else eof = true; } while (!eof); in.close(); out.close();
} }
boolean deleted = source.delete(); if (deleted) temp.renameTo(source); catch (IOException e) { System.out.println("Error -- " + e.toString()); catch (SecurityException se) { System.out.println("Error -- " + se.toString());
} }
Nachdem Sie das Programm kompiliert haben, benötigen Sie ein Textfile, das sie komplett in Großbuchstaben umwandeln können. Sie können z. B. eine Kopie von AllCapsDemo.java erstellen und sie TempFile.java nennen. Der Name des zu konvertierenden Files muss bei Ausführung von AllCapsDemo in der Kommandozeile angegeben werden, wie im folgenden SDK-Beispiel: java AllCapsDemo TempFile.java
Dieses Programm erzeugt keinerlei Ausgabe. Laden Sie die umgewandelte Datei in einen Texteditor, um das Ergebnis der Applikation zu sehen.
15.7 Zusammenfassung Sie haben heute gelernt, mit Streams in zwei Richtungen zu arbeiten: Wie man mit einem Eingabestream Daten in ein Programm einliest und wie man mit einem Ausgabestream Daten aus einem Programm sendet. Sie haben Bytestreams für mehrere Arten von Daten, die keinen Text enthalten, und Zeichenstreams für Text benutzt. Wir haben Filter mit Streams assoziiert, um die Art und Weise zu verändern, in der Informationen durch einen Stream weitergereicht wurden, oder um die Informationen selbst zu ändern. Die Lektionen von heute decken die meisten Klassen des Pakets java.io ab, es gibt jedoch auch andere Arten von Streams, die Sie interessieren könnten. So genannte Piped Streams benutzt man, um Daten zwischen verschiedenen Threads auszutauschen, und Byte-ArrayStreams können Programme mit dem Speicher des Computers verbinden.
477
Mit Eingaben und Ausgaben arbeiten
Da die Streamklassen bei Java so eng zusammenhängen, besitzen Sie bereits größtenteils das Wissen, das Sie für diese anderen Streamtypen brauchen. Die Konstruktoren sowie Lese- und Schreibmethoden sind weitgehend identisch. Streams sind eine wichtige Ergänzung der Funktionalität Ihrer Java-Programme, da sie eine Verbindung zu allen Arten von Daten herstellen, mit denen Sie arbeiten möchten. Morgen werden Sie mit Streams Java-Objekte lesen und schreiben.
15.8 Workshop Fragen und Antworten F
Ich benutze ein C-Programm, das eine Datei aus Integern und anderen Daten erstellt. Kann ich dieses File mit einem Java-Programm lesen? A
F
Das geht schon, aber Sie müssen vorher überprüfen, ob Ihr C-Programm Integer in derselben Weise darstellt, wie dies ein Java-Programm tun würde. Wie Sie sich erinnern, können alle Daten als einzelnes Byte oder als Folge von Bytes dargestellt werden. Unter Java wird ein Integer dargestellt, indem man vier Bytes im big-endianFormat anordnet. Sie können den Integerwert errechnen, indem Sie die Bytes von links nach rechts kombinieren. Ein C-Programm auf einem Intel-PC dürfte Integer im little-endian-Format repräsentieren, d. h., dass die Bytes von rechts nach links angeordnet werden müssten, um das Ergebnis zu erhalten. Sie müssten sich mit BitShifting befassen, um ein solches Datenfile verwenden zu können.
Die Klasse FileWriter hat eine write(int)-Methode, mit der man ein Zeichen an eine Datei schickt. Sollte das nicht write(char) sein? A
Die Datentypen char und int sind in mancherlei Hinsicht austauschbar – Sie können ein int in einer Methode benutzten, die char erwartet, und umgekehrt. Das ist möglich, da jedes Zeichen durch einen Zahlencode repräsentiert wird, der ein Integerwert ist. Wenn Sie die write()-Methode mit einem int aufrufen, gibt sie das Zeichen aus, das mit diesem Integerwert assoziiert ist. Wenn Sie eine write()Methode aufrufen, können Sie einen int-Wert in einen char-Wert casten, um sicherzustellen, dass er wie intendiert verwendet wird.
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
478
Workshop
Fragen 1. Was passiert, wenn Sie einen FileOutputStream mit einer Referenz auf ein existentes File erstellen? (a) Eine Ausnahme wird ausgeworfen. (b) Die Daten, die Sie in den Stream schreiben, werden an die existente Datei angehängt. (c) Die existente Datei wird durch die Daten ersetzt, die Sie in den Stream schreiben. 2. Welche beiden primitiven Typen sind austauschbar, wenn Sie mit Streams arbeiten? (a) byte und boolean (b) char und int (c) byte und char 3. Wie lauten bei Java der Maximalwert einer byte-Variable und der Maximalwert eines vorzeichenlosen Bytes in einem Stream? (a) jeweils 255 (b) jeweils 127 (c) 127 für die byte-Variable und 255 für das vorzeichenlose Byte
Antworten 1. c. Darauf müssen Sie achten, wenn Sie Ausgabestreams verwenden – die Gefahr ist groß, dass man existente Dateien löscht. 2. b. Da ein char Java-intern als Integerwert dargestellt wird, können Sie die beiden oft austauschbar in Methodenaufrufen und anderen Anweisungen benutzen. 3. c. Der primitive Datentyp byte hat Werte zwischen -128 und 127, während ein vorzeichenloses Byte Werte zwischen 0 und 255 annehmen kann.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. import java.io.*;
479
Mit Eingaben und Ausgaben arbeiten
public class Unknown { public static void main(String[] arguments) { String command = ""; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try { command = br.readLine(); } catch (IOException e) { } } }
Wird dieses Programm eine Zeile Konsoleneingabe in einem String-Objekt namens command speichern? a. Ja. b. Nein, weil man zum Lesen von Konsoleneingaben einen gepufferten Eingabestream braucht. c. Nein, weil das Kompilieren fehlschlagen wird. d. Nein, weil mehr als eine Zeile Konsoleninput gelesen wird. Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 15, und klicken Sie auf den Link »Certification Practice«.
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Schreiben Sie eine modifizierte Version des HexRead-Programms von Tag 7, das zweistellige Hexadezimalsequenzen aus einer Textdatei liest und ihre dezimalen Äquivalente anzeigt.
Schreiben Sie ein Programm, das eine Datei ausliest, um die Zahl ihrer Bytes festzustellen. Überschreiben Sie dann alle Bytes mit Nullen (0). Aus offensichtlichen Gründen sollten Sie dieses Programm nicht mit Files testen, die Sie behalten wollen – alle Daten werden unwiederbringlich gelöscht.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Webseite zum Buch: http://www.java21pro.com.
480
Objekt-Serialisation und -Inspektion
6 1
Objekt-Serialisation und -Inspektion
Ein wesentliches Konzept objektorientierter Programmierung ist die Art und Weise, wie Daten repräsentiert werden. In einer objektorientierten Sprache wie Java repräsentiert ein Objekt zweierlei:
das Verhalten des Objekts
die Attribute – die Daten, die dieses Objekt von anderen Objekten unterscheiden
Dass Verhalten und Attribute kombiniert werden, ist ein großer Unterschied zu vielen anderen Programmiersprachen. Man hat ein Programm früher häufig als eine Reihe von Instruktionen definiert, die Daten manipulieren. Die Daten selbst sind davon getrennt, wie das z. B. bei einer Textverarbeitung der Fall ist. Die meisten Textverarbeitungen gelten als Programme, die dazu verwendet werden, um Textdokumente zu erstellen und zu verändern. Objektorientierte Programmierung und andere Techniken lassen die Trennlinie zwischen Programm und Daten verschwimmen. Ein Objekt in einer Sprache wie Java verbindet Befehle (Verhalten) mit Daten (Attributen). Heute lernen Sie drei Möglichkeiten kennen, wie ein Java-Programm davon profitieren kann:
Objekt-Serialisation: die Möglichkeit, ein Objekt mit einem Stream zu lesen bzw. zu schreiben
Reflexion: die Fähigkeit eines Objekts, Einzelheiten über ein anderes Objekt zu erfahren
Remote method invocation – die Möglichkeit, ein anderes Objekt nach seinen Eigenschaften zu befragen und seine Methoden aufzurufen
16.1 Objekt-Serialisation Wie Sie gestern gelernt haben, wickelt Java den Zugriff auf externe Daten mithilfe der Objektklasse namens Streams ab. Ein Stream ist ein Objekt, das Daten von einem Ort zu einem anderen überträgt. Manche Streams tragen Information von einer Quelle in ein Java-Programm, andere gehen in die entgegengesetzte Richtung und übertragen Daten aus einem Programm zu einem Ziel. Ein Stream, der die Daten einer Webseite in das Array eines Java-Programms einliest, ist ein Beispiel für die erste Gruppe. Ein Stream, der ein String-Array in eine Datei auf der Festplatte schreibt, ist ein Beispiel für die andere Gruppe. An Tag 15 haben wir zwei Typen von Streams kennen gelernt:
Bytestreams, die eine Serie von Integerwerten zwischen 0 und 255 lesen bzw. schreiben
Zeichenstreams, die textliche Daten lesen und schreiben
482
Objekt-Serialisation
Diese Streams trennen die Daten von der Java-Klasse, die mit ihnen arbeitet. Um die Daten später zu benutzen, müssen Sie die Daten durch einen Stream lesen und in eine Form konvertieren, die die Klasse benutzen kann, wie z. B. eine Reihe von Variablen und Objekten. Ein dritter Streamtyp namens Objektstreams ermöglicht, Daten als Teil eines Objekts zu repräsentieren (und nicht als etwas, was außerhalb eines Objekts liegt). Objektstreams sind wie Byte- oder Zeichenstreams Teil des Pakets java.io. Wenn man mit ihnen arbeitet, kommen viele der Techniken zum Einsatz, die Sie an Tag 15 kennen gelernt haben. Damit ein Objekt in ein Ziel wie eine Datei auf einer Festplatte gespeichert werden kann, muss es in eine serielle Form gebracht werden. Serielle Daten werden stückweise versandt, ähnlich wie eine Reihe von Autos auf einem Fließband. Vielleicht sagt Ihnen der serielle Anschluss etwas: Dort werden die Informationen als eine Serie von Bits nacheinander versandt. Eine andere Möglichkeit besteht darin, Daten parallel zu versenden: Dann wird mehr als ein Element gleichzeitig transferiert. Ein Objekt gibt an, dass man es mit Streams benutzen kann, indem es die SerializableSchnittstelle implementiert. Diese Schnittstelle ist Teil des Pakets java.io und unterscheidet sich in einem wesentlichen Punkt von anderen Schnittstellen, die Sie bereits kennen: Sie enthält keine Methoden, die implementierende Klassen übernehmen müssten. Der einzige Zweck von Serializable besteht darin, anzuzeigen, dass Objekte dieser Klasse in serieller Form gespeichert und ausgelesen werden können. Objekte können auf die Festplatte eines Einzelplatzrechners oder aber über ein Netzwerk wie das Internet serialisiert werden, auch wenn dabei verschiedene Betriebssysteme ins Spiel kommen. Sie können ein Objekt auf einem Windows-Computer erstellen, es an einen UNIX-Rechner serialisieren und es dann zurück auf den ursprünglichen WindowsComputer laden, ohne dass dabei Fehler auftreten würden. Java kommt mit den verschiedenen Formaten dieser Systeme zum Datenspeichern klar, wenn Objekte serialisiert werden. Bei der Objekt-Serialisation spielt Persistenz eine Rolle – die Fähigkeit eines Objekts, außerhalb des Programms, das es erzeugte, zu existieren und zu funktionieren. Normalerweise ist ein nicht serialisiertes Objekt nicht persistent. Wenn das Programm abbricht, das das Objekt verwendet, endet die Existenz des Objekts. Serialisation ermöglicht Persistenz, denn das gespeicherte Objekt dient weiterhin einem Zweck, auch wenn kein Java-Programm läuft. Das gespeicherte Objekt enthält Informationen, die in ein Programm geladen werden können, sodass das Objekt dann wieder existieren kann.
483
Objekt-Serialisation und -Inspektion
Wenn ein Objekt in serieller Form in einen Stream gespeichert wird, werden alle Objekte, auf die es Referenzen hat, ebenfalls gespeichert. Dadurch wird die Arbeit mit Serialisation leichter: Sie können einen Objektstream erzeugen, der sich um zahlreiche Objekte gleichzeitig kümmert. Sie können auch einige Variablen des Objekts von der Serialisation ausschließen. Damit spart man Speicherplatz oder nimmt sicherheitsrelevante Informationen von der Speicherung aus. Wie Sie später noch sehen werden, geschieht dies durch die Verwendung des Modifiers transient.
Objekt-Ausgabestreams Man schreibt ein Objekt in einen Stream mit der ObjectOutputStream-Klasse. Ein Objekt-Ausgabestream wird mit dem ObjectOutputStream(OutputStream)-Konstruktor erzeugt. Die Argumente für diesen Konstruktor können folgende sein:
ein Ausgabestream, der das Ziel repräsentiert, in das das Objekt in serieller Form gespeichert werden soll
ein Filter, der mit dem Ausgabestream, der zum Ziel führt, assoziiert ist
Wie bei anderen Streams können Sie mehr als einen Filter zwischen den Ausgabestream und den Objekt-Ausgabestream setzen. Der folgende Code erzeugt einen Ausgabestream und einen assoziierten Objekt-Ausgabestream: FileOutputStream disk = new FileOutputStream( "SavedObject.dat"); ObjectOutputStream obj = new ObjectOutputStream(disk);
Der Objekt-Ausgabestream, der in diesem Beispiel erzeugt wurde, heißt obj. Die Methoden der obj-Klasse können benutzt werden, um serialisierbare Objekte und andere Informationen in eine Datei namens SavedObject.dat zu schreiben. Nachdem Sie einen Objekt-Ausgabestream erzeugt haben, können Sie in ihn ein Objekt schreiben, indem Sie die writeObject(Object)-Methode des Streams aufrufen. Die folgende Anweisung ruft diese Methode von disk auf, dem Stream aus dem letzten Beispiel: disk.writeObject(userData);
Diese Anweisung schreibt ein Objekt namens userData in den Objekt-Ausgabestream disk. Die von userData repräsentierte Klasse muss serialisierbar sein, damit dies funktioniert.
484
Objekt-Serialisation
Ein Objekt-Ausgabestream kann auch zum Schreiben anderer Informationen benutzt werden, wenn Sie folgende Methoden einsetzen:
write(int) – schreibt den angegebenen Integer (0–255) in den Stream.
write(byte[]) – schreibt das angegebene Byte-Array.
write(byte[], int, int) – schreibt einen Teil des angegebenen Byte-Arrays. Das
zweite Argument gibt das erste zu schreibende Array-Element an, und das letzte Argument repräsentiert die Zahl der folgenden Elemente, die geschrieben werden sollen.
writeBoolean(boolean) – schreibt den angegebenen boolean.
writeByte(int) – schreibt den angegebenen Integer als Byte-Wert.
writeBytes(string) – schreibt den angegebenen String als Serie von Bytes.
writeChar(int) – schreibt das angegebene Zeichen.
writeChars(String) – schreibt den angegebenen String als Serie von Zeichen.
writeDouble(double) – schreibt den angegebenen double.
writeFloat(float) – schreibt den angegebenen float.
writeInt(int) – schreibt den angegebenen int, der jeder beliebige int-Wert sein
kann.
writeLong(long) – schreibt den angegebenen long.
writeShort(short) – schreibt den angegebenen short.
Der Konstruktor ObjectOutputStream und alle Methoden, die Daten in einen Objekt-Ausgabestream schreiben, werfen IOException-Objekte aus. Um diese muss man sich in einem try-catch-Block oder mit einer throws-Klausel kümmern. Listing 16.1 enthält eine Java-Applikation, die aus zwei Klassen besteht: ObjectToDisk und Message. Die Klasse Message repräsentiert eine Nachricht, die man einer anderen Person schicken könnte, z. B. eine E-Mail oder eine kurze Nachricht in einem privaten Chat. Die Klasse hat die Objekte from und to, in denen die Namen des Senders und Empfängers gespeichert werden, ein now-Objekt zur Speicherung eines Date-Werts, der die Sendezeit angibt, und ein text-Array mit String-Objekten, die die Nachricht selbst speichern. Es gibt ferner einen int namens lineCount, der die Zeilenanzahl der Nachricht speichert. Wenn man ein Programm entwirft, das elektronische Nachrichten übermittelt und empfängt, ist es sinnvoll, einen Stream zum Speichern dieser Nachrichten auf Festplatte zu benutzen. Die Information, die diese Nachricht darstellt, muss in irgendeiner Form gespeichert vorliegen, wenn sie von einem Ort zu einem anderen übermittelt wird. Unter Umständen muss sie auch gespeichert werden, bis der Empfänger sie lesen kann.
485
Objekt-Serialisation und -Inspektion
Man kann Nachrichten speichern, indem man jedes Nachrichtenelement einzeln in einen Byte- oder Zeichenstream schreibt. Im Beispiel der Message-Klasse könnten die Objekte from und to als Strings in einen Stream und das text-Objekt könnte als ein String-Array geschrieben werden. Das Objekt now macht etwas mehr Mühe, weil man ein Date-Objekt nicht so einfach in einen Zeichenstream schreiben kann. Aber es ließe sich natürlich in eine Serie von Integerwerten umwandeln, die jeden Teil des Datums repräsentieren: Stunde, Minute, Sekunde usw. Diese könnten dann in den Stream geschrieben werden. Die Verwendung eines Objekt-Ausgabestreams ermöglicht es, Message-Objekte abzuspeichern, ohne dass man sie erst in eine andere Form überführen müsste. Die Klasse ObjectToDisk in Listing 16.1 erzeugt ein Message-Objekt, legt die Werte für seine Variablen fest und speichert es über einen Objekt-Ausgabestream in eine Datei namens Message.obj. Listing 16.1: Der vollständige Quelltext von ObjectToDisk.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:
486
import java.io.*; import java.util.*; public class ObjectToDisk { public static void main(String[] arguments) { Message mess = new Message(); String author = "Sam Wainwright, London"; String recipient = "George Bailey, Bedford Falls"; String[] letter = { "Mr. Gower cabled you need cash. Stop.", "My office instructed to advance you up to twenty-five", "thousand dollars. Stop. Hee-haw and Merry Christmas." } ; Date now = new Date(); mess.writeMessage(author, recipient, now, letter); try { FileOutputStream fo = new FileOutputStream( "Message.obj"); ObjectOutputStream oo = new ObjectOutputStream(fo); oo.writeObject(mess); oo.close(); System.out.println("Object created successfully."); } catch (IOException e) { System.out.println("Error -- " + e.toString()); } } } class Message implements Serializable { int lineCount; String from, to;
Objekt-Serialisation
30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: }
Date when; String[] text; void writeMessage(String inFrom, String inTo, Date inWhen, String[] inText) { text = new String[inText.length]; for (int i = 0; i < inText.length; i++) text[i] = inText[i]; lineCount = inText.length; to = inTo; from = inFrom; when = inWhen; }
Sie sollten folgende Ausgabe sehen, nachdem Sie die Applikation ObjectToDisk kompiliert und ausgeführt haben: Object created successfully.
Objekt-Eingabestreams Man liest ein Objekt aus einem Stream mit der Klasse ObjectInputStream. Wie bei anderen Streams gilt, dass der Umgang mit einem Objekt-Eingabestream sehr dem mit dem analogen Ausgabestream gleicht. Der Hauptunterschied ist die Richtung des Datenflusses. Ein Objekt-Eingabestream wird mit dem ObjectInputStream(InputStream)-Konstruktor erzeugt. Dieser Konstruktor wirft zwei Ausnahmen aus: IOException und StreamCorruptionException. IOException, die oft bei Streamklassen vorkommt, wird ausgeworfen, wenn während des Datentransfers ein Eingabe/Ausgabe-Fehler auftrat. StreamCorruptionException kommt nur bei Objektstreams vor und gibt an, dass die Daten im Stream kein serialisiertes Objekt darstellen. Ein Objekt-Eingabestream kann aus einem Eingabestream oder einem gefilterten Stream erstellt werden. Der folgende Code erzeugt einen Eingabestream und einen assoziierten Objekt-Eingabestream: try { FileInputStream disk = new FileInputStream( "SavedObject.dat"); ObjectInputStream obj = new ObjectInputStream(disk);
487
Objekt-Serialisation und -Inspektion
} }
catch (IOException ie) { System.out.println("IO error -- " + ie.toString()); catch (StreamCorruptionException se) { System.out.println("Error – data not an object.");
}
Dieser Objekt-Eingabestream wird so aufgesetzt, dass er aus einem Objekt liest, das in einer Datei namens SavedObject.dat gespeichert ist. Wenn die Datei nicht existiert oder aus irgendeinem Grund nicht von der Platte gelesen werden kann, wird eine IOException ausgeworfen. Wenn die Datei kein serialisiertes Objekt ist, weist eine ausgeworfene StreamCorruptionException auf dieses Problem hin. Man kann ein Objekt aus einem Objekt-Eingabestream mit der Methode readObject() lesen, die ein Objekt zurückgibt. Dieses Objekt kann sofort in die Klasse gecastet werden, in die es gehört, wie in folgendem Beispiel: WorkData dd = (WorkData)disk.readObject();
Diese Anweisung liest ein Objekt aus dem Objektstream disk und castet es in ein Objekt der Klasse WorkData. Neben IOException wirft diese Methode OptionalDataException und ClassNotFoundException aus. OptionalDataException bedeutet, dass der Stream keine Daten eines serialisierten Objekts enthält, was es unmöglich macht, aus dem Stream ein Objekt zu lesen. ClassNotFoundException tritt auf, wenn das aus dem Stream ausgelesene Objekt zu einer
Klasse gehört, die unauffindbar ist. Wenn Objekte serialisiert werden, wird die Klasse selbst nicht in den Stream gespeichert. Stattdessen wird der Name der Klasse in den Stream gespeichert, und der Java-Interpreter lädt die Klasse, sobald das Objekt aus dem Stream geladen ist. Andere Informationen können aus einem Objekt-Eingabestream mit folgenden Methoden gelesen werden:
read() – liest das nächste Byte aus dem Stream, das dann als int zurückgegeben wird.
read(byte[], int, int) – liest Bytes in das angegebene Byte-Array. Das zweite Argument gibt das erste Array-Element an, in das ein Byte gespeichert werden soll, und das letzte Argument repräsentiert die Zahl der folgenden Elemente, die gelesen und im Array gespeichert werden sollen.
readBoolean() – liest einen boolean-Wert aus dem Stream.
readByte() – liest einen byte-Wert aus dem Stream.
readChar() – liest einen char-Wert aus dem Stream.
readDouble() – liest einen double-Wert aus dem Stream.
readFloat() – liest einen float-Wert aus dem Stream.
488
Objekt-Serialisation
readInt() – liest einen int-Wert aus dem Stream.
readLine() – liest einen String aus dem Stream.
readLong() – liest einen long-Wert aus dem Stream.
readShort() – liest einen short-Wert aus dem Stream.
readUnsignedByte() – liest einen vorzeichenlosen Byte-Wert und gibt ihn als int
zurück.
readUnsignedShort() – liest einen vorzeichenlosen Short-Wert und gibt ihn als int
zurück. Diese Methoden werfen jeweils eine IOException aus, wenn ein Eingabe/Ausgabe-Fehler aufritt, während der Stream gelesen wird. Wenn ein Objekt durch Lesen eines Objektstreams erzeugt wird, wird es vollständig anhand der Variablen- und Objekt-Informationen erzeugt, die in diesem Stream gespeichert sind. Es erfolgt kein Konstruktoraufruf, um Variablen zu erzeugen und sie mit Anfangswerten zu initialisieren. Es gibt keinen Unterschied zwischen diesem Objekt und dem ursprünglich erzeugten. Listing 16.2 enthält eine Java-Applikation, die ein Objekt aus einem Stream ausliest und dann dessen Variablen im Standardausgabegerät anzeigt. Die ObjectFromDisk-Applikation lädt das Objekt, das in die Datei message.obj serialisiert worden ist. Diese Klasse muss von dem Ordner aus ausgeführt werden, in dem auch message.obj und die Klasse Message gespeichert sind. Listing 16.2: Der vollständige Quelltext von ObjectFromDisk.java 1: import java.io.*; 2: import java.util.*; 3: 4: public class ObjectFromDisk { 5: public static void main(String[] arguments) { 6: try { 7: FileInputStream fi = new FileInputStream( 8: "message.obj"); 9: ObjectInputStream oi = new ObjectInputStream(fi); 10: Message mess = (Message) oi.readObject(); 11: System.out.println("Message:\n"); 12: System.out.println("From: " + mess.from); 13: System.out.println("To: " + mess.to); 14: System.out.println("Date: " + mess.when + "\n"); 15: for (int i = 0; i < mess.lineCount; i++) 16: System.out.println(mess.text[i]);
489
Objekt-Serialisation und -Inspektion
17: 18: 19: 20: 21: 22: }
}
oi.close(); catch (Exception e) { System.out.println("Error -- " + e.toString());
} }
Die Ausgabe dieses Programms sieht wie folgt aus: Message: From: Sam Wainwright, London To: George Bailey, Bedford Falls Date: Thu Jun 22 15:09:01 EDT 2002 Mr. Gower cabled you need cash. Stop. My office instructed to advance you up to twenty-five thousand dollars. Stop. Hee-haw and Merry Christmas.
Transiente Variablen Wenn Sie ein Objekt erzeugen, das serialisiert werden kann, müssen Sie sich überlegen, ob alle Instanzvariablen des Objekts gespeichert werden sollten. Einige Instanzvariablen müssen bei jedem Laden des Objekts ganz neu erstellt werden. Ein Beispiel wäre ein Objekt, das auf eine Datei oder einen Eingabestream verweist. Dieses Objekt müsste neu erstellt werden, wenn es Teil eines serialisierten Objekts ist, das von einem Objektstream geladen wurde. Es wäre also sinnlos, diese Information beim Serialisieren des Objekts mitzuspeichern. Man sollte Variablen, die sicherheitsrelevante Informationen beinhalten, vom Serialisieren ausschließen. Wenn ein Passwort, das einen Zugang schützt, in einem Objekt gespeichert wird, steigt die Gefährdung des Passworts, wenn dieses Objekt in eine Datei serialisiert wird. Das Passwort könnte auch abgefangen werden, wenn es Teil eines Objekts ist, das über einen Stream innerhalb eines Netzwerks wiederhergestellt wird. Ein dritter Grund, eine Variable nicht zu serialisieren, besteht darin, Festplattenspeicherplatz zu sparen. Wenn Werte ohne Serialisation erstellt werden können, sollten Sie die Variable aus dem Prozess ausschließen. Um eine Instanzvariable von der Serialisation auszuschließen, benutzen Sie den Modifier transient. Dieser Modifier ist Teil der Anweisung, die die Variable erzeugt und geht der
Klasse bzw. dem Datentyp der Variable voraus. Die folgende Anweisung erzeugt eine transiente Variable namens limit: public transient int limit = 55;
490
Klassen und Methoden mit Reflexion inspizieren
16.2 Klassen und Methoden mit Reflexion inspizieren An Tag 3 haben Sie gelernt, wie man Class-Objekte erzeugt, die die Klasse repräsentieren, zu der ein Objekt gehört. Jedes Objekt in Java erbt die getClass()-Methode, die die Klasse oder Schnittstelle dieses Objekts identifiziert. Die folgende Anweisung erzeugt ein ClassObjekt namens keyclass auf der Grundlage eines Objekts, auf das mit der Variable key verwiesen wird: Class keyClass = key.getClass();
Indem man die getName()-Methode eines Class-Objekts aufruft, kann man den Namen der Klasse herausfinden: String keyName = keyClass.getName();
Diese Features sind Teil der Java-Unterstützung von Reflexion, einer Technik, die einer Java-Klasse – z. B. einem von Ihnen geschriebenen Programm – ermöglicht, Einzelheiten über andere Klassen zu erfahren. Dank Reflexion kann ein Java-Programm eine Klasse laden, von der es nichts weiß, ihre Variablen, Methoden und Konstruktoren herausfinden und dann mit ihnen arbeiten.
Klassen inspizieren und erzeugen Die Klasse Class, die Teil des Pakets java.lang ist, dient dazu, Klassen, Schnittstellen und sogar primitive Typen zu inspizieren und zu erzeugen. Neben der Verwendung von getClass() können Sie Class-Objekte erzeugen, indem Sie .class an den Namen einer Klasse, einer Schnittstelle, eines Arrays oder eines primitiven Typs anhängen, wie in den folgenden Beispielen: Class Class Class Class
keyClass = KeyClass.class; thr = Throwable.class; floater = float.class; floatArray = float[].class;
Sie können Class-Objekte auch dadurch erstellen, dass Sie die Klassenmethode forName() mit einem Argument benutzen: einem String, der den Namen einer existenten Klasse enthält. Die folgende Anweisung erzeugt ein Class-Objekt, das ein JLabel repräsentiert, eine der Klassen des Pakets javax.swing: Class lab = Class.forName("javax.swing.JLabel");
Die Methode forName() wirft eine ClassNotFoundException aus, wenn die angegebene Klasse nicht gefunden werden kann. Setzen Sie also forName() in einen try-catch-Block oder kümmern Sie sich auf andere Weise um diese mögliche Ausnahme.
491
Objekt-Serialisation und -Inspektion
Um einen String zu erhalten, der den Namen der durch das Class-Objekt repräsentierten Klasse enthält, müssen Sie getName() von diesem Objekt aufrufen. Bei Klassen und Schnittstellen beinhaltet dieser Name nicht nur den Namen der Klasse, sondern auch einen Verweis auf das Paket, zu dem sie gehört. Bei primitiven Typen entspricht der Name dem Namen des Typs (also z. B. int, float oder double). Class-Objekte, die Arrays repräsentieren, verhalten sich ein wenig anders, wenn ihre getName()-Methode aufgerufen wird. Der Name beginnt mit einer geöffneten eckigen Klammer [ für jede Dimension des Arrays – float[] beginnt mit [, int[][] mit [[, KeyClass[][][] mit [[[ usw.
Wenn das Array primitiven Typs ist, ist der nächste Teil des Namens ein einzelnes Zeichen, das den Typ repräsentiert (sieh Tabelle 16.1): Zeichen
primitiver Typ
B
byte
C
char
D
double float
I
int
J
long
S
short
Z
boolean
Tabelle 16.1: Typidentifikationen für primitive Typen
Bei Objekt-Arrays folgt auf die eckige(n) Klammer(n) ein L und dann der Name der Klasse. Wenn Sie z. B. getName() von einem String[][]-Array aufrufen, wäre das Ergebnis [[Ljava.lang.String. Sie können mit der Class-Klasse auch neue Objekte erstellen. Rufen Sie die Methode newInstance() eines Class-Objekts auf, um das Objekt zu erzeugen, und casten Sie es in die richtige Klasse. Wenn Sie z. B. ein Class-Objekt namens thr haben, das die Schnittstelle Throwable repräsentiert, können Sie ein neues Objekt wie folgt erstellen: Throwable thr2 = (Throwable)thr.newInstance();
Die Methode newInstance() wirft mehrere Ausnahmen aus:
492
IllegalAccessException – Sie haben keinen Zugriff auf die Klasse. Entweder ist sie nicht public oder sie gehört zu einem anderen Paket.
Klassen und Methoden mit Reflexion inspizieren
InstantiationException – Sie können kein neues Objekt erstellen, da die Klasse abs-
trakt ist.
SecurityViolation – Sie haben keine Berechtigung, ein Objekt dieser Klasse zu erstellen.
Wenn newInstance() aufgerufen und keine Ausnahme ausgeworfen wird, wird das neue Objekt erzeugt, indem der Konstruktor der entsprechenden Klasse argumentenlos aufgerufen wird. Sie können diese Technik nicht verwenden, um ein neues Objekt zu erzeugen, das Argumente für seinen Konstruktor benötigt. Stattdessen müssen Sie eine newInstance()-Methode der Klasse Constructor benutzen, wie wir heute später noch sehen werden.
Mit den einzelnen Teilen der Klasse arbeiten Class ist zwar Teil des Pakets java.lang, die Unterstützung findet sich jedoch für Reflexion hauptsächlich im Paket java.lang.reflect, das die folgenden Klassen umfasst:
Field – verarbeitet und findet Informationen über Klassen- und Instanzvariablen.
Method – verarbeitet Klassen- und Instanzmethoden.
Constructor – verarbeitet Konstruktoren, die Spezialmethoden zur Erstellung neuer Instanzen von Klassen
Array – verarbeitet Arrays.
Modifier – decodiert Modifier-Informationen zu Klassen, Variablen und Methoden
(dies wurde an Tag 6 behandelt). Die Reflexionsklassen haben jeweils Methoden, um mit einem Element der Klasse zu arbeiten. Ein Method-Objekt enthält Informationen über eine einzelne Methode in einer Klasse. Um sich über alle Methoden einer Klasse kundig zu machen, erzeugen Sie ein Class-Objekt für diese Klasse und rufen dann die getDeclaredMethods()-Methode dieses Objekts auf. Ein Array von Method[]-Objekten wird zurückgegeben, das alle Methoden in der Klasse repräsentiert, die nicht von einer Superklasse ererbt wurden. Falls dies auf keine Methode zutrifft, ist die Länge des Arrays 0. Die Klasse Method hat mehrere Instanzmethoden:
getParameterTypes() – Diese Methode gibt ein Array von Class-Objekten zurück, die
alle Argumente repräsentieren, die in der Signatur der Methode vorkommen.
493
Objekt-Serialisation und -Inspektion
getReturnType() – Diese Methode gibt ein Class-Objekt zurück, das den Rückgabetyp der Methode repräsentiert, d. h., ob es eine Klasse oder ein primitiver Typ ist.
getModifiers() – Diese Methode gibt einen int-Wert zurück, der die Modifier repräsentiert, die sich auf diese Methode beziehen, d. h., ob sie public, private o. ä. ist.
Da die Methoden getParameterTypes() und getReturnType() Class-Objekte zurückgeben, können Sie die getName()-Methoden der einzelnen Objekte benutzen, um mehr über sie zu erfahren. Die einfachste Möglichkeit, den von getModifiers() zurückgegeben int zu verwenden, besteht darin, die Modifier-Klassenmethode toString() mit diesem Integer als Argument aufzurufen. Wenn Sie z. B. ein Method-Objekt namens current haben, können Sie seine Modifier mit folgendem Code anzeigen: int mods = current.getModifiers(); System.out.println(Modifier.toString(mods));
Die Constructor-Klasse hat einige Methoden mit der Method-Klasse gemeinsam, wie z. B. getModifiers() und getName(). Es fehlt getReturnType(), was nicht verwunderlich ist, denn schließlich haben Konstruktoren keine Rückgabetypen. Um alle mit einem Class-Objekt assoziierten Konstruktoren auszulesen, rufen Sie die getConstructors()-Methode dieses Objekts auf. Ein Array von Constructor-Objekten wird zurückgegeben. Um einen speziellen Konstruktor auszulesen, erzeugen Sie zunächst ein Array von ClassObjekten, das alle Argumente repräsentiert, die an den Konstruktor weitergegeben werden. Wenn das erledigt ist, rufen Sie getConstructors() mit diesem Class-Array als Argument auf. Wenn Sie beispielsweise einen KeyClass(String, int)-Konstruktor haben, können Sie ein Constructor-Objekt, das ihn repräsentiert, folgendermaßen erzeugen: Class kc = KeyClass.class; Class[] cons = new Class[2]; cons[0] = String.class; cons[1] = int.class; Constructor c = kc.getConstructor(cons);
Die Methode getConstructor(Class[]) wirft eine NoSuchMethodException aus, wenn es keinen Konstruktor mit Argumenten gibt, die mit dem Class[]-Array übereinstimmen würden. Wenn Sie ein Constructor-Objekt haben, können Sie seine newInstance(Object[])Methode aufrufen, um eine neue Instanz mithilfe dieses Konstruktors zu erzeugen.
494
Klassen und Methoden mit Reflexion inspizieren
Eine Klasse inspizieren Um das ganze Material zusammenzufassen, folgt nun Listing 16.3, eine kurze Java-Applikation namens SeeMethods, die mit Reflexion die Methoden einer Klasse inspiziert. Listing 16.3: Der vollständige Quelltext von SeeMethods.java 1: import java.lang.reflect.*; 2: 3: public class SeeMethods { 4: public static void main(String[] arguments) { 5: Class inspect; 6: try { 7: if (arguments.length > 0) 8: inspect = Class.forName(arguments[0]); 9: else 10: inspect = Class.forName("SeeMethods"); 11: Method[] methods = inspect.getDeclaredMethods(); 12: for (int i = 0; i < methods.length; i++) { 13: Method methVal = methods[i]; 14: Class returnVal = methVal.getReturnType(); 15: int mods = methVal.getModifiers(); 16: String modVal = Modifier.toString(mods); 17: Class[] paramVal = methVal.getParameterTypes(); 18: StringBuffer params = new StringBuffer(); 19: for (int j = 0; j < paramVal.length; j++) { 20: if (j > 0) 21: params.append(", "); 22: params.append(paramVal[j].getName()); 23: } 24: System.out.println("Method: " + methVal.getName() + "()"); 25: System.out.println("Modifiers: " + modVal); 26: System.out.println("Return Type: " + returnVal.getName()); 27: System.out.println("Parameters: " + params + "\n"); 28: } 29: } catch (ClassNotFoundException c) { 30: System.out.println(c.toString()); 31: } 32: } 33: }
Die Applikation SeeMethods zeigt Informationen über die öffentlichen Methoden der Klasse an, die Sie in der Kommandozeile angeben (oder von SeeMethods selbst, wenn Sie keine Klasse angeben). Um dieses Programm zu testen, geben Sie Folgendes in die Kommandozeile ein:
495
Objekt-Serialisation und -Inspektion
java SeeMethods java.util.Random
Wenn Sie diese Applikation mit java.util.Random ausführen, sieht die Ausgabe des Programms wie folgt aus: Method: next() Modifiers: protected synchronized Return Type: int Parameters: int Method: nextDouble() Modifiers: public Return Type: double Parameters: Method: nextInt() Modifiers: public Return Type: int Parameters: int Method: nextInt() Modifiers: public Return Type: int Parameters: Method: setSeed() Modifiers: public synchronized Return Type: void Parameters: long Method: nextBytes() Modifiers: public Return Type: void Parameters: [B Method: nextLong() Modifiers: public Return Type: long Parameters: Method: nextBoolean() Modifiers: public Return Type: boolean Parameters: Method: nextFloat() Modifiers: public
496
Klassen und Methoden mit Reflexion inspizieren
Return Type: float Parameters: Method: nextGaussian() Modifiers: public synchronized Return Type: double Parameters:
Mithilfe von Reflexion kann die Applikation SeeMethods jede Methode einer Klasse in Erfahrung bringen. In den Zeilen 7–10 der Applikation wird ein Class-Objekt erzeugt. Wenn bei der Ausführung von SeeMethods ein Klassenname als Kommandozeilenargument angegeben wurde, wird die Class.forName()-Methode mit diesem Argument aufgerufen. Ansonsten wird SeeMethods als Argument verwendet. Nachdem das Class-Objekt erzeugt wurde, wird seine getDeclaredMethods()-Methode in Zeile 11 benutzt, um alle in dieser Klasse enthaltenen Methoden herauszufinden (mit Ausnahme der von Superklassen ererbten Methoden). Diese Methoden werden als Array von Method-Objekten gespeichert. Die for-Schleife in den Zeilen 12–28 geht durch alle Methoden in der Klasse und speichert ihren Rückgabewert, ihre Modifier und Argumente, die dann angezeigt werden. Die Anzeige des Rückgabetyps ist ganz einfach: Die getReturnType()-Methoden der einzelnen Klassen werden jeweils in einem Class-Objekt in Zeile 14 gespeichert, und die Namen der einzelnen Objekte werden in Zeile 26 angezeigt. Wenn die getModifiers()-Methode einer Methode in Zeile 15 aufgerufen wird, wird ein Integer zurückgegeben, der alle Modifier repräsentiert, die bei dieser Methode eingesetzt werden. Die Klassenmethode Modifier.toString() erwartet einen Integer als Argument und gibt die Namen aller Modifier zurück, die damit assoziiert sind. Die Zeilen 19–23 gehen durch das Array von Class-Objekten, die die Argumente repräsentieren, die mit einer Methode assoziiert sind. Die Namen der einzelnen Argumente werden in Zeile 22 zu einem StringBuffer-Objekt namens params hinzugefügt. Reflexion wird meistens von Tools wie Klassenbrowsern oder Debuggern benutzt, um damit mehr über die Objektklasse zu erfahren, durch die gebrowst oder die debuggt wird. Man benötigt sie auch für JavaBeans, bei denen die Fähigkeit eines Objekts, ein anderes Objekt zu fragen, was es tun kann (und es dann anzuweisen, etwas zu tun), bei der Erstellung großer Applikationen wichtig ist. Mehr über JavaBeans erfahren Sie an Tag 19. Reflexion ist eine fortgeschrittene Technik, die Sie vielleicht nicht so häufig in Ihren Programmen brauchen. Sie wird dann wichtig, wenn Sie mit Objekt-Serialisation, JavaBeans und anderen Programmen arbeiten, die Zugriff auf Java-Klassen während der Laufzeit benötigen.
497
Objekt-Serialisation und -Inspektion
16.3 RMI (Remote Method Invocation) RMI wird zur Erstellung von Java-Anwendungen benutzt, die mit anderen Java-Anwendungen über ein Netzwerk kommunizieren können. Genauer gesagt, ermöglicht RMI einer Java-Anwendung, Zugriff auf Methoden und Variablen innerhalb anderer Java-Anwendungen zu haben, die in unterschiedlichen Java-Umgebungen oder auf unterschiedlichen Systemen laufen können, und über die Netzwerkverbindung Objekte weiter- und zurückzugeben. RMI ist ein höher entwickelter Mechanismus zur Kommunikation zwischen verteilten Java-Objekten als eine einfache Socket-Verbindung es sein könnte, da der Mechanismus und die Protokolle, durch die Sie zwischen den Objekten kommunizieren, definiert und standardisiert sind. Sie können mit einem anderen Java-Programm mit RMI kommunizieren, ohne das Protokoll zu kennen. Eine andere Form der Kommunikation zwischen Objekten nennt sich RPC (Remote Procedure Calls), mit der Sie über eine Netzverbindung in anderen Programmen Methoden aufrufen oder Prozeduren ausführen können. Obwohl RPC und RMI viele Gemeinsamkeiten haben, besteht der Hauptunterschied darin, dass RPC nur Prozeduraufrufe über die Leitung sendet, wobei die Argumente so weitergeleitet oder beschrieben werden, dass sie auf der anderen Seite rekonstruierbar sind. RMI dagegen tauscht ganze Objekte über das Netz aus und eignet sich daher besser für ein vollkommen objektorientiert verteiltes Objektmodell. Das RMI-Konzept könnte Visionen von auf der ganzen Welt fröhlich miteinander kommunizierenden Objekten aufkommen lassen, normalerweise wird RMI jedoch in einer traditionellen Client-Server-Situation benutzt: Eine einzelne Server-Anwendung erhält Verbindungen und Anfragen von einer Anzahl Clients. RMI ist der Mechanismus, der es Client und Server ermöglicht, miteinander zu kommunizieren.
Die RMI-Architektur Die für RMI gesteckten Ziele waren, ein verteiltes Objektmodell in Java zu integrieren, ohne dabei die Sprache oder das bestehende Objektmodell auseinander zu reißen und die Interaktion mit einem Remote-Objekt so einfach wie mit einem lokalen Objekt zu gestalten. Einem Programmierer sollte Folgendes möglich sein:
Remote-Objekte auf exakt dieselbe Weise wie lokale Objekte zu verwenden (sie Variablen zuweisen, sie als Argumente an Methoden weitergeben usw.)
Methoden in Remote-Objekten auf dieselbe Weise aufzurufen, wie lokale Aufrufe erfolgen
498
RMI (Remote Method Invocation)
Darüber hinaus beinhaltet RMI einen höher entwickelten Mechanismus, um Methoden von Remote-Objekten aufzurufen, um ganze Objekte oder Teile von Objekten entweder mit Referenz oder mit Wert weiterzugeben sowie zusätzliche Ausnahmen für die Bearbeitung von Netzwerkfehlern, die beim Durchführen von Remote-Operationen vorkommen können. Zum Erreichen dieser Ziele besitzt RMI mehrere Ebenen, und ein einzelner Methodenaufruf durchläuft viele dieser Ebenen, um an seinen Bestimmungsort zu gelangen (siehe Abbildung 16.1). Es gibt drei Ebenen:
Die Ebenen »Stub« und »Skeleton« auf dem Client bzw. Server agieren auf beiden Seiten als Stellvertreter-Objekte, indem sie die Tatsache, dass der Methodenaufruf entfernt erfolgt, vor der eigentlichen Implementierungsklasse verbergen. Sie können also in Ihrer Client-Anwendung Remote-Methoden auf exakt dieselbe Art und Weise wie lokale Methoden aufrufen; das Stub-Objekt ist ein lokaler Stellvertreter für das Remote-Objekt.
Die Ebene »Remote Reference Layer« ist für die Handhabung des Packens eines Methodenaufrufs und seiner Parameter und Rückgabewerte für den Transport über das Netz zuständig.
Die Transportebene ist die eigentliche Netzverbindung von einem System zu einem anderen. Client
Server
Applikation
Applikation
Stub-Ebene
Skeleton Ebene
Remote ReferenceEbene
Remote ReferenceEbene
Transport Ebene
Netzwerk
Transport Ebene
Abbildung 16.1: RMI-Ebenen
Weil man drei RMI-Ebenen hat, kann man die einzelnen Ebenen unabhängig voneinander kontrollieren und implementieren. Stubs und Skeletons ermöglichen es Client- und Server-Klassen, sich so zu verhalten, als ob die vorliegenden Objekte lokal seien, und genau dieselben Java-Spracheigenschaften zum Zugriff auf diese Objekte zu verwenden. Die Ebene »Remote Reference Layer« isoliert die Verarbeitung des Remote-Objekts in einer eigenen Ebene, die dann unabhängig von den Anwendungen, die von ihr abhängig sind, optimiert und reimplementiert werden kann. Schließlich wird die Ebene »Network Transport Layer« unabhängig von den beiden anderen benutzt, sodass Sie unterschiedliche Typen von Socket-Verbindungen für RMI benutzen können (TCP, UDP oder TCP mit einem anderen Protokoll wie beispielsweise SSL).
499
Objekt-Serialisation und -Inspektion
Wenn eine Client-Anwendung einen Remote Method Call durchführt, gelangt der Aufruf über Stub zu der Referenzebene, die die Argumente bei Bedarf packt. Sie gibt den Aufruf dann über die Netzebene an den Server weiter, wo die serverseitige Referenzebene die Argumente entpackt und sie an Skeleton und dann an die Server-Implementierung weiterleitet. Die Ausgabewerte für den Aufruf der Methode machen dann den Weg in umgekehrter Reihenfolge zurück zur Client-Seite. Das Packen und Weitergeben von Methodenargumenten ist einer der interessanten Aspekte von RMI, da mithilfe von Serialisation Objekte in eine Form konvertiert werden müssen, in der sie über das Netz weitergegeben werden können. Wenn ein Objekt serialisiert werden kann, kann RMI es als einen Methodenparameter oder Ausgabewert benutzen. Als Methodenparameter oder Rückgabewerte verwendete Java-Remote-Objekte werden wie lokale Objekte als Referenz übergeben. Andere Objekte werden allerdings kopiert. Beachten Sie, dass dieses Verhalten die Art und Weise beeinflusst, in der Sie Ihre Java-Programme schreiben, wenn diese Programme Remote Method Calls benutzen – Sie können beispielsweise ein Array nicht als ein Argument an eine Remote-Methode weitergeben, das Array von dem Remote-Objekt ändern lassen und erwarten, dass die lokale Kopie modifiziert wird. Dies stellt einen Unterschied zur Verhaltensweise von lokalen Objekten dar, wo alle Objekte als Referenzen weitergegeben werden.
RMI-Anwendungen erstellen Um eine Anwendung zu erstellen, die RMI einsetzt, benutzen Sie die Klassen und Schnittstellen, die im Paket java.rmi definiert sind. Dazu gehören die Folgenden:
java.rmi.server für serverseitige Klassen
java.rmi.registry, das die Klassen zur Lokalisierung und Registrierung der RMI-Ser-
ver auf dem lokalen System enthält
java.rmi.dgc für Garbage-Collection von verteilten Objekten
Das java.rmi-Paket selbst enthält die allgemeinen RMI-Schnittstellen, -Klassen und -Ausnahmen. Um eine RMI-basierte Client-Server-Anwendung zu implementieren, definieren Sie zuerst eine Schnittstelle, die alle Methoden enthält, die Ihr Remote-Objekt unterstützen wird. Die Methoden in dieser Schnittstelle müssen alle eine throws RemoteException-Anweisung beinhalten, die etwaige Netzprobleme abdeckt, die die Kommunikation zwischen Client und Server stören können. Listing 16.4 zeigt Ihnen eine einfache Schnittstelle, die mit einem Remote-Objekt benutzt werden kann.
500
RMI (Remote Method Invocation)
Listing 16.4: Der vollständige Quelltext von PiRemote.java 1: 2: 3: 4: 5: 6: 7:
package com.prefect.pi; import java.rmi.*; interface PiRemote extends Remote { double getPi() throws RemoteException; }
Diese RMI-Schnittstelle muss Teil eines Pakets sein, damit sie für ein Remote-Client-Programm zugänglich ist. Wenn man einen Paketnamen verwendet, werden Java-Compiler und -Interpreter heikler bezüglich der Lokation der Java- und Klassendateien eines Programms. Der Hauptordner eines Pakets sollte ein Ordner des CLASSPATH Ihres Systems sein. Jeder Teil eines Paketnamens dient zur Erstellung eines Unterordners. Wenn sich auf Ihrem System der Ordner C:\j2sdk1.4 befindet, könnte die Datei PiRemote.java in einen Ordner namens C:\j2sdk1.4\com\prefect\pi gespeichert werden. Wenn Sie keinen Ordner haben, der denselben Namen hat wie das Paket, dann sollten Sie ihn erstellen. Diese Schnittstelle tut überhaupt nichts und benötigt eine Klasse, die sie implementiert. Im Augenblick können Sie sie kompilieren, indem Sie das folgende Kommando von dem Ordner aus eingeben, in dem sich PiRemote.java befindet: javac PiRemote.java
Zwar ist bei der Kompilation der Datei der Paketname erforderlich, aber nicht bei der Kompilation der Schnittstelle. Der nächste Schritt besteht in der Implementierung der Remote-Schnittstelle in einer serverseitigen Anwendung, die in der Regel die UnicastRemoteObject-Klasse ableitet. Sie können die Methoden in der Remote-Schnittstelle innerhalb dieser Klasse implementieren und auch einen Security-Manager für diesen Server erstellen und installieren (damit sich nicht irgendwelche beliebigen Clients verbinden können und es keine unautorisierten Methodenaufrufe gibt). Sie können natürlich den Security-Manager so konfigurieren, dass er verschiedene Operationen genehmigt oder nicht genehmigt. Die Java-Klassenbibliothek enthält eine Klasse namens RMISecurityManager, die für diesen Zweck eingesetzt werden kann. In der Server-Anwendung »registrieren« Sie auch die Remote-Anwendung, die diese an einen Host und einen Port bindet. Listing 16.5 enthält eine Java-Serverapplikation, die die Schnittstelle PiRemote implementiert:
501
Objekt-Serialisation und -Inspektion
Listing 16.5: Der vollständige Quelltext von Pi.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:
package com.prefect.pi; import import import import
java.net.*; java.rmi.*; java.rmi.registry.*; java.rmi.server.*;
public class Pi extends UnicastRemoteObject implements PiRemote { public double getPi() throws RemoteException { return Math.PI; } public Pi() throws RemoteException { } public static void main(String[] arguments) { System.setSecurityManager(new RMISecurityManager()); try { Pi p = new Pi(); Naming.bind("//Default:1010/Pi", p); } catch (Exception e) { System.out.println("Error -- " + e.toString()); e.printStackTrace(); } } }
Im Aufruf der Methode bind() in Zeile 23 identifiziert der Text Default:1010 den Computernamen und den Port für die RMI-Registrierung. Wenn Sie diese Applikation von einem Webserver aus ausführen, würde Default durch eine URL ersetzt werden. Sie müssen Default in den Namen Ihres Computers umändern. Benutzer von Windows 95 und 98 finden ihren Systemnamen, indem sie auf Start/Einstellungen/Systemsteuerung/Netzwerk klicken. Klicken Sie dann auf die Identifikationsregisterkarte, um den Computernamen zu sehen. Auf Windows XP klicken Sie mit der rechten Maustaste auf Arbeitsplatz, wählen Eigenschaften und klicken dann auf die Registerkarte Computername. Auf der Clientseite implementieren Sie eine einfache Applikation, die die Remote-Schnittstelle benutzt und Methoden in dieser Schnittstelle aufruft. Eine Klasse namens Naming (in
502
RMI (Remote Method Invocation)
java.rmi) ermöglicht dem Client eine transparente Verbindungsherstellung zum Server. Listing 16.6 zeigt OutputPi.java.
Listing 16.6: Der vollständige Quelltext von OutputPi.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
package com.prefect.pi; import java.rmi.*; import java.rmi.registry.*; public class OutputPi { public static void main(String[] arguments) { System.setSecurityManager( new RMISecurityManager()); try { PiRemote pr = (PiRemote)Naming.lookup( "//Default:1010/Pi"); for (int i = 0; i < 10; i++) System.out.println("Pi = " + pr.getPi()); } catch (Exception e) { System.out.println("Error -- " + e.toString()); e.printStackTrace(); } } }
Jetzt können Sie diese Programme mit dem Standard-Compiler von Java kompilieren, allerdings muss noch ein weiterer Schritt ausgeführt werden: Benutzen Sie das Kommandozeilenprogramm rmic zur Erzeugung der Ebenen Stub und Skeleton, damit RMI zwischen den beiden Seiten des Prozesses arbeiten kann. Um die Stub- und Skeleton-Dateien für dieses Projekt zu erzeugen, gehen Sie in den Hauptordner Ihrer Pakete und geben folgendes Kommando ein: rmic com.prefect.pi.Pi
Zwei Dateien werden erzeugt: Pi_Stub.class und Pi_Skel.class. Das Programm rmiregistry wird schließlich dazu benutzt, die Serveranwendung mit dem Netzwerk zu verbinden und es an einen Port anzubinden, sodass Remote-Verbindungen hergestellt werden können. Das Programm rmiregistry funktioniert nicht korrekt, wenn sich die Pi_Stub.class und die Pi_Skel.class im CLASSPATH Ihres Systems befinden. Dies liegt daran, dass das Programm davon ausgeht, dass Sie keine Remote-Implementierungen dieser Dateien benötigen, wenn sie lokal gefunden werden können.
503
Objekt-Serialisation und -Inspektion
Am einfachsten lässt sich dieses Problem vermeiden, indem Sie rmiregistry ausführen, nachdem sie Ihren CLASSPATH kurzfristig abschalten. Dies kann man unter Windows erledigen, indem man eine neue MS-DOS-Eingabeaufforderung öffnet und folgendes Kommando eingibt: set CLASSPATH=
Da die Applikationen von Client und Server den Port 1010 benutzen, müssen Sie das rmiregistry-Programm mit folgendem Kommando starten: start rmiregistry 1010
Nachdem Sie die RMI-Registrierung gestartet haben, sollten Sie das Serverprogramm Pi ausführen. Weil diese Applikation Teil eines Pakets ist, müssen Sie ihren vollen Paketnamen angeben, wenn Sie die Applikation im Java-Interpreter ausführen. Außerdem müssen Sie angeben, wo die Klassendateien, die mit der Applikation assoziiert sind, gefunden werden können, und zwar auch für Pi_Stub.class und Pi_Skel.class. Dies geschieht dadurch, dass die java.rmi.server.codebase-Eigenschaft festgelegt wird. Wenn die Klassendateien der Applikation unter http://www.java21pro.com/java/ gespeichert wären, könnte man mit folgendem Kommando die Applikation vom selben Ordner aus starten, der auch Pi.class enthält: java -Djava.rmi.server.codebase=http://www.java21pro/java/com.prefect.pi.Pi
Zuletzt wird das Clientprogramm OutputPi gestartet. Gehen Sie in den Ordner, der OutputPi.class enthält, und geben Sie Folgendes ein: java com.prefect.pi.OutputPi
Das Programm erzeugt die folgende Ausgabe: Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi
= = = = = = = = = =
3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793 3.141592653589793
RMI und Sicherheit Auf manchen Systemen erzeugt RMI Sicherheitsfehler, wenn Sie versuchen, die Programme Pi oder OutputPi zu starten.
504
Zusammenfassung
Wenn Sie AccessControlException-Fehlermeldungen erhalten, die mit Aufrufen der Methoden Naming.bind() und Naming.lookup() zusammenhängen, müssen Sie Ihr System so konfigurieren, dass es diese RMI-Aufrufe korrekt ausführen kann. Eine Möglichkeit besteht darin, eine einfache Datei zu gestalten, die die niedrigsten Sicherheitseinstellungen für Java enthält und diese Datei zur Festlegung der java.security.policy-Eigenschaft zu benutzen, wenn Sie Pi und OutputPi ausführen. Listing 16.7 enthält eine Textdatei, die für diesen Zweck verwendet werden kann. Erzeugen Sie diese Datei mithilfe eines Texteditors und speichern Sie sie als policy.txt in denselben Ordner wie OutputPi.class und Pi.class. Listing 16.7: Der vollständige Quelltext von policy.txt 1: grant { 2: permission java.security.AllPermission; 3: // alles wird erlaubt 4: } ;
Man benutzt derartige Sicherheitseinstellungsdateien, um Zugang zu Systemressourcen zu gewähren bzw. zu verwehren. In diesem Beispiel werden alle Befugnisse erteilt, was verhindert, dass der AccessControlException-Fehler auftritt, wenn Sie die RMI-Client- und -Serverprogramme ausführen. Die -Djava.security.policy=policy.txt-Option kann beim Java-Interpreter benutzt werden. Das folgende Beispiel zeigt, wie dies aussehen kann: java -Djava.rmi.server.codebase=http://www.java21days.com/java/ Djava.security.policy=policy.txt com.prefect.pi.Pi java -Djava.security.policy=policy.txt com.prefect.pi.OutputPi
16.4 Zusammenfassung Java war schon immer eine netzwerkzentrierte Sprache, schließlich laufen bereits seit Version 1.0 Applets in Browsern. Die heutigen Themen zeigen, wie sich die Sprache in zwei Richtungen ausweitet. Objekt-Serialisation zeigt, dass mit Java erzeugte Objekte ein Leben nach dem Java-Programm haben. Sie können Objekte in einem Programm erzeugen und sie auf ein Speichermedium wie eine Festplatte sichern, um sie später, lange nach Beendigung des ursprünglichen Programms, wiederherzustellen.
505
Objekt-Serialisation und -Inspektion
RMI zeigt, dass die Methodenaufrufe von Java über einen Computer hinausreichen. Indem Sie die RMI-Techniken und -Kommandozeilen-Tools nutzen, können Sie Java-Programme erzeugen, die mit anderen Programmen arbeiten können, unabhängig davon, wo sich diese befinden, ob nun in einem anderen Zimmer oder auf einem anderen Kontinent. Beide Features können für ausgefeilte Netzwerkapplikationen benutzt werden, ObjektSerialisation ist jedoch auch für viele andere Aufgaben nützlich. Bereits in Ihren ersten Programmen werden Sie vielleicht darauf zurückgreifen; Persistenz ist eine gute Möglichkeit, um Elemente eines Programms zur späteren Verwendung zu speichern.
16.5 Workshop Fragen und Antworten F
Sind Objektstreams mit den Writer- und Reader-Klassen assoziiert, die man für den Umgang mit Zeichenstreams braucht? A
Die Klassen ObjectInputStream und ObjectOutputStream sind unabhängig von den Bytestream- und Zeichenstream-Superklassen im Paket java.io, obwohl sie ähnlich wie viele Byte-Klassen funktionieren. Es sollte keinen Anlass geben, Writer- oder Reader-Klassen in Verbindung mit Objektstreams zu verwenden, da Sie dasselbe mit den Objektstream-Klassen und ihren Superklassen (InputStream und OutputStream) erledigen können.
F
Werden private-Variablen und -Objekte gespeichert, wenn sie Teil eines Objekts sind, das serialisiert wird? A
Ja, sie werden abgespeichert. Wie Sie sich sicher noch erinnern, werden keine Konstruktoren aufgerufen, wenn ein Objekt mittels Serialisation in ein Programm geladen wird. Deswegen werden alle Variablen und Objekte, die nicht als transient erklärt wurden, abgespeichert, damit das Objekt nichts verliert, was für seine Funktion notwendig ist.
Das Abspeichern von private-Variablen und -Objekten kann unter bestimmten Umständen ein Sicherheitsrisiko darstellen, insbesondere dann, wenn die Variable zum Speichern eines Passworts oder anderer sensibler Daten dient. Die Verwendung von transient verhindert, dass eine Variable oder ein Objekt serialisiert wird.
506
Workshop
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Was wird zurückgegeben, wenn Sie getName() eines Class-Objekts aufrufen, das ein String[]-Array repräsentiert? (a) java.lang.String (b) [Ljava.lang.String (c) [java.lang.String 2. Was ist Persistenz? (a) die Fähigkeit eines Objekts, weiterzuexistieren, nachdem das Programm, das es erzeugt hat, beendet wurde (b) ein wichtiges Konzept der Objekt-Serialisation (c) die Fähigkeit, sich durch 16 Tage eines Programmierlehrbuchs zu beißen und immer noch entschlossen zu sein, diese Fragen am Ende des Kapitels zu beantworten 3. Welche Class-Methode benutzt man, um ein neues Class-Objekt mithilfe eines Strings mit dem Namen der Klasse zu erzeugen? (a) newInstance() (b) forName() (c) getName()
Antworten 1. b.Die eckige Klammer gibt die Tiefe des Arrays an, das L gibt an, dass es ein Array mit Objekten ist, und der folgende Klassenname ist selbsterklärend. 2. a und b. Wenn Sie gewusst haben, dass das lateinische Fremdwort »Persistenz« auf gut Deutsch »Hartnäckigkeit« heißt, dann ist auch Antwort c korrekt! 3. b. Wenn die Klasse nicht gefunden wird, wird eine ClassNotFoundException ausgeworfen.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen.
507
Objekt-Serialisation und -Inspektion
Gegeben sei: public class ClassType { public static void main(String[] arguments) { Class c = String.class; try { Object o = c.newInstance(); if (o instanceof String) System.out.println("True"); else System.out.println("False"); } catch (Exception e) { System.out.println("Error"); } } }
Was wird ausgegeben? a. True b. False c. Error d. Nichts, denn das Programm lässt sich so nicht kompilieren. Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 16, und klicken Sie auf den Link »Certification Practice«.
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Benutzen Sie Reflexion, um ein Java-Programm zu schreiben, das einen Klassennamen als Kommandozeilenargument erwartet und überprüft, ob es eine Applikation ist – alle Applikationen haben eine main()-Methode mit den Modifiern public static, void als Rückgabewert und String[] als einziges Argument.
Erstellen Sie ein Programm, das ein neues Objekt mithilfe von Class-Objekten sowie der newInstance()-Methode erzeugt und es anschließend auf die Festplatte serialisiert.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Webseite zum Buch: http://www.java21pro.com.
508
Kommunikation über das Internet
7 1
Kommunikation über das Internet
Java wurde ursprünglich als Sprache zur Steuerung eines Netzes interaktiver ConsumerGeräte entwickelt. Das Verbinden von Maschinen war einer der Hauptzwecke der Sprache, als sie entworfen wurde, und ist bis heute so geblieben. Das Paket java.net ermöglicht über ein Netzwerk mit Ihren Java-Programmen zu kommunizieren. Das Paket bietet eine plattformübergreifende Abstraktionsebene für einfache Netzwerkoperationen, darunter Verbindungsaufbau zu Dateien und ihre Übertragung. Dazu werden Standard-Webprotokolle verwendet und elementare, Unix-artige Sockets erzeugt. In Verbindung mit den Eingabe- und Ausgabestreams wird das Lesen und Schreiben von Dateien über ein Netzwerk genauso einfach wie das Schreiben und Lesen von Dateien auf einer lokalen Festplatte. Das Paket java.nio, eine Neuerung von Java 2 Version 1.4, ist eine Erweiterung der InputOutput-Klassen von Java, die die Netzwerkfähigkeiten der Sprache komplettiert. Heute werden Sie Java-Programme schreiben, die in der Lage sind, über ein Netzwerk zu kommunizieren. Sie werden Applikationen erstellen, die ein Dokument über das World Wide Web laden, ein Programm, das einen beliebten Internetservice nachahmt, und ein Client/Server-Netzwerkprogramm.
17.1 Netzwerkprogrammierung in Java Vernetzung (Networking) ist die Eigenschaft verschiedener Computer, sich miteinander zu verbinden und untereinander Informationen auszutauschen.
Unter Java benutzt man zum einfachen Vernetzen Klassen des Pakets java.net, die das Verbinden und den File-Austausch über HTTP und FTP sowie die Arbeit auf einer niedrigeren Ebene mit elementaren, Unix-artigen Sockets unterstützen. Am einfachsten kann man die Netzwerkfähigkeiten von Java mit Applikationen benutzen, da diese nicht denselben Standard-Sicherheitseinstellungen wie Applets unterworfen sind. Applets können sich mit keinem anderen Netzwerkcomputer verbinden als mit dem, der den Server hostet, von dem sie heruntergeladen wurden. Dieser Abschnitt beschreibt drei einfache Wege, wie Sie mit Systemen im Netz kommunizieren können:
510
eine Webseite oder eine andere Ressource mit einer URL von einem Applet aus laden
Netzwerkprogrammierung in Java
die Verwendung der Socket-Klassen Socket und ServerSocket, die das Öffnen von Standard-Socket-Verbindungen zu Hosts und das Lesen und Schreiben über solche Verbindungen ermöglichen
der Aufruf von getInputStream(), einer Methode, die eine Verbindung zu einer URL herstellt und das Einlesen von Daten über diese Verbindung ermöglicht
Links in Applets erstellen Da Applets innerhalb von Browsern laufen, ist es oft nützlich, den Browser anzuweisen, eine neue Webseite zu laden. Bevor Sie irgendetwas laden können, müssen Sie eine neue Instanz der Klasse URL erzeugen, die die Adresse der zu ladenden URL repräsentiert. URL steht für Uniform Resource Locator und bezeichnet die einzigartige Adresse jedes Dokuments und jeder anderen Ressource, die über das Internet verfügbar ist. URL ist Teil des Pakets java.net, weswegen Sie das Paket importieren oder die Klasse in
Ihren Programmen mit vollem Namen ansprechen müssen. Um ein neues URL-Objekt zu erzeugen, benutzen Sie einen der vier folgenden Konstruktoren:
URL(String) – erzeugt ein URL-Objekt aus einer vollständigen Internetadresse wie http://www.java21pro.com oder ftp://ftp.netscape.com.
URL(URL, String) – erzeugt ein URL-Objekt aus der Basisadresse, die die übergebene URL liefert, und einem relativen Pfad, den der String darstellt. Bei der Angabe der Adresse können Sie getDocumentBase() für die URL der Seite mit Ihrem Applet oder getCodeBase() für die URL der Klassendatei des Applets aufrufen. Der relative Pfad
wird an die Basisadresse angehängt.
URL(String, String, int, String) – erzeugt ein neues URL-Objekt aus einem Protokoll (wie z. B. »http« oder »ftp«), einem Host-Namen (wie "www.cnn.com" oder "web.archive.org"), Portnummer (80 für HTTP) und einem Pfad- oder Dateinamen. URL(String, String, String) – ist bis auf das Fehlen der Portnummer identisch mit
dem vorherigen Konstruktor. Wenn Sie den URL(String)-Konstruktor benutzen, müssen Sie sich um MalformedURLException-Objekte kümmern. Eine Möglichkeit ist ein try-catch-Block wie der folgende: try { URL load = new URL("http://www.samspublishing.com"); } catch (MalformedURLException e) { System.out.println("Bad URL"); }
511
Kommunikation über das Internet
Wenn Sie Ihr URL-Objekt haben, übergeben Sie es dem Browser, indem Sie die showDocument()-Methode der AppletContext-Klasse in Ihrem Applet aufrufen. AppletContext ist eine Schnittstelle, die die Umgebung repräsentiert, in der ein Applet läuft – den Browser und die Webseite, in die es eingebettet ist. Rufen Sie getAppletContext() in Ihrem Applet auf, um ein AppletContext-Objekt zu erhalten, dessen showDocument(URL)-Methode Sie dann aufrufen: AppletContext ct = getAppletContext(); ct.showDocument(load);
Diese Zeilen veranlassen einen Browser, der ein Applet anzeigt, das Dokument mit der durch load repräsentierten URL zu laden und anzuzeigen. Listing 17.1 beinhaltet zwei Klassen: WebMenu und eine Hilfsklasse namens WebButton. Das WebMenu-Applet zeigt drei Buttons an, die Links zu Webseiten haben (Abbildung 17.1). Wenn man auf einen Button klickt, wird das Dokument von den Adressen geladen, auf die diese Buttons verweisen.
Abbildung 17.1: Das Applet WebMenu
Listing 17.1: Der vollständige Quelltext von WebMenu.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
512
import import import import
java.net.*; java.awt.*; java.awt.event.*; javax.swing.*;
public class WebMenu extends JApplet implements ActionListener { WebButton[] choices = new WebButton[3]; public void init() { choices[0] = new WebButton("Obscure Store", "http://www.obscurestore.com/"); choices[1] = new WebButton("Need to Know", "http://www.ntk.net/"); choices[2] = new WebButton("Bleat", "http://www.lileks.com/bleats");
Netzwerkprogrammierung in Java
17: FlowLayout flo = new FlowLayout(); 18: Container pane = getContentPane(); 19: pane.setLayout(flo); 20: for (int i = 0; i < choices.length; i++) { 21: choices[i].addActionListener(this); 22: pane.add(choices[i]); 23: } 24: setContentPane(pane); 25: } 26: 27: public void actionPerformed(ActionEvent evt) { 28: WebButton clicked = (WebButton)evt.getSource(); 29: try { 30: URL load = new URL(clicked.address); 31: getAppletContext().showDocument(load); 32: } catch (MalformedURLException e) { 33: showStatus("Bad URL:" + clicked.address); 34: } 35: } 36: } 37: 38: class WebButton extends JButton { 39: String address; 40: 41: WebButton(String iLabel, String iAddress) { 42: super(iLabel); 43: address = iAddress; 44: } 45: }
Sie können dieses Applet testen, indem Sie folgenden Code in eine HTML-Seite einfügen:
Dieses Applet muss in einem Browser (nicht im Appletviewer) geladen werden, damit die Buttons neue Seiten laden können. Da das Applet Event-HandlingTechniken benutzt, die nach Java 1.0 eingeführt wurden, benötigen Sie einen Browser mit Java-Plug-In. Dieses Projekt besteht aus zwei Klassen: WebMenu, das das Applet implementiert, und WebButton, einer Benutzerschnittstellenkomponente, die die JButton-Klasse ableitet, um eine Instanzvariable mit der Internetadresse hinzuzufügen.
513
Kommunikation über das Internet
Das Applet erzeugt drei WebButton-Instanzen (Zeilen 10–15) und speichert sie in ein Array. Jeder Button erhält einen Namen, der als Label benutzt wird, und eine Internetadresse, die als String, nicht als URL, gespeichert wird. Nachdem die Buttons erzeugt sind, wird ein Container erstellt, der die Content-Pane des Applets repräsentiert und so eingerichtet wird, dass er den Layout-Manager FlowLayout verwendet (Zeilen 1–719). In den Zeilen 20–23 wird jedem Button ein ActionListener hinzugefügt, bevor er in den Container gelegt wird. In Zeile 24 wird der Container als Content-Pane des Applets festgelegt. Aufgrund dieser Listeners wird die actionPerformed()-Methode in den Zeilen 27–35 aufgerufen, wenn ein Button gedrückt wird. Die Methode stellt fest, welcher Button angeklickt wurde, und benutzt dann die address-Variable dieses Buttons zur Erzeugung eines neuen URL-Objekts. Sobald Sie das URL-Objekt haben, kann durch den Aufruf der showDocument()-Methode in Zeile 31 der Browser aufgefordert werden, diese Webseite ins aktuelle Fenster zu laden. Sie können eine URL auch in ein neues Browserfenster oder einen bestimmten Frame laden. Für ein neues Fenster rufen Sie showDocument(URL, String) mit "_blank" als zweitem Argument auf. Für einen Frame setzen Sie den FrameNamen als zweites Argument ein. Da die Webseiteninformation im Applet gespeichert ist, müssen Sie die Klasse erneut kompilieren, wenn Sie eine Adresse hinzufügen, löschen oder ändern. Besser wäre es, die Namen der Internetseiten und die URLs als Parameter in einem HTML-Dokument zu speichern, was an Tag 14 erklärt wurde.
Öffnen von Webverbindungen Wie Sie bei der Arbeit mit Applets gesehen haben, ist es nicht schwer, eine Webseite oder etwas anderes mit einer URL zu laden. Ist die betreffende Datei im Internet gespeichert und über die üblichen URL-Formen (http, ftp usw.) zugänglich, können Sie die URL-Klasse benutzen, um die Datei in Ihrem Java-Programm zu verwenden. Aus Sicherheitsgründen können Applets standardmäßig nur zu dem Host, von dem sie ursprünglich geladen wurden, Verbindungen herstellen. Das bedeutet beispielsweise bei einem Applet, das auf einem System namens www.java21pro.com gespeichert ist, dass Ihr Applet nur mit diesem Host (und dem gleichen Hostnamen) eine Verbindung herstellen kann. Befindet sich die Datei, die das Applet abrufen möchte, auf dem gleichen System, sind URL-Verbindungen die einfachste Möglichkeit, es auszulesen.
514
Netzwerkprogrammierung in Java
Diese Sicherheitseinschränkung wird die Art und Weise ändern, in der Sie Applets schreiben und testen. Da wir uns noch nicht mit Netzverbindungen beschäftigt haben, war es uns möglich, alle Tests auf der lokalen Platte durch einfaches Öffnen der HTML-Dateien oder mit dem Appletviewer durchzuführen. Dies ist mit Applets, die Netzverbindungen öffnen, nicht möglich. Damit diese Applets richtig funktionieren, müssen Sie eine der folgenden Aktionen durchführen:
Lassen Sie Ihren Browser auf der gleichen Maschine laufen, auf der Ihr Webserver läuft. Wenn Sie keinen Zugriff auf Ihren Webserver haben, besteht normalerweise die Möglichkeit, einen Webserver auf Ihrer lokalen Maschine zu installieren und damit zu arbeiten.
Zum Testen laden Sie jedes Mal Ihre Klasse und HTML-Dateien auf Ihren Webserver. Starten Sie dann das Applet auf der Webseite, anstatt es lokal laufen zu lassen.
Sie werden es merken, wenn Sie etwas falsch machen. Bei dem Versuch, ein Applet oder eine Datei von unterschiedlichen Servern zu laden, erhalten Sie eine Sicherheitsausnahme und dazu zahlreiche andere Fehlermeldungen. Deswegen sollten Sie nach Möglichkeit mit Applikationen arbeiten, wenn Sie Internetverbindungen erstellen und Netzressourcen benutzen.
Einen Stream über das Internet öffnen Wie Sie an Tag 15 gelernt haben, gibt es verschiedene Möglichkeiten, um Informationen über einen Stream in Ihr Java-Programm zu ziehen. Die zu benutzenden Klassen und Methoden hängen davon ab, um welche Information es sich handelt und was Sie damit anstellen wollen. Ressourcen, die Sie von Ihren Java-Programmen aus erreichen können, sind z. B. Textdokumente im WWW, unabhängig davon, ob es sich um HTML-Dateien oder Nur-TextDateien handelt. Sie können mit einem Prozess in vier Schritten ein Textdokument aus dem Netz laden und es zeilenweise lesen: 1. Erzeugen Sie ein URL-Objekt, das die WWW-Adresse der Ressource repräsentiert. 2. Erzeugen Sie ein URLConnection-Objekt, das diese URL lädt und eine Verbindung zur hostenden Seite herstellt. 3. Verwenden Sie die getInputStream()-Methode dieses URLConnection-Objekts, um einen InputStreamReader zu erzeugen, der einen Datenstream von dieser URL liest. 4. Verwenden Sie diesen InputStreamReader, um ein BufferedReader-Objekt zu erzeugen, das Zeichen aus einem Inputstream lesen kann.
515
Kommunikation über das Internet
Es findet viel Interaktion zwischen Punkt A – dem Internetdokument – und Punkt B – Ihrem Java-Programm – statt. Mithilfe der URL wird eine URL-Verbindung hergestellt, mit der ein InputStreamReader erzeugt wird, mit dem wiederum ein gepufferter InputStreamReader erzeugt wird. Da mögliche Ausnahmen aufgefangen werden müssen, wird das Ganze noch etwas komplizierter. Da dies alles etwas verwirrend ist, gehen wir am besten ein Beispielprogramm Schritt für Schritt durch. Die Applikation GetFile aus Listing 17.2 verwendet diese Technik, um eine Verbindung zu einer Webseite herzustellen und ein HTML-Dokument von dort zu lesen. Sobald das Dokument vollständig geladen ist, wird es in einem Textbereich dargestellt. Listing 17.2: Der vollständige Quelltext von GetFile.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33:
516
import import import import import
javax.swing.*; java.awt.*; java.awt.event.*; java.net.*; java.io.*;
public class GetFile { public static void main(String[] arguments) { if (arguments.length == 1) { PageFrame page = new PageFrame(arguments[0]); page.show(); } else System.out.println("Usage: java GetFile url"); } } class PageFrame extends JFrame { JTextArea box = new JTextArea("Getting data ..."); URL page; public PageFrame(String address) { super(address); setSize(600, 300); JScrollPane pane = new JScrollPane(box); getContentPane().add(pane); WindowListener l = new WindowAdapter() { public void windowClosing(WindowEvent evt) { System.exit(0); } } ; addWindowListener(l); try {
Netzwerkprogrammierung in Java
34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: }
}
page = new URL(address); getData(page); catch (MalformedURLException e) { System.out.println("Bad URL: " + address);
} } void getData(URL url) { URLConnection conn = null; InputStreamReader in; BufferedReader data; String line; StringBuffer buf = new StringBuffer(); try { conn = this.page.openConnection(); conn.connect(); box.setText("Connection opened ..."); in = new InputStreamReader(conn.getInputStream()); data = new BufferedReader(in); box.setText("Reading data ..."); while ((line = data.readLine()) != null) buf.append(line + "\n");
}
box.setText(buf.toString()); catch (IOException e) { System.out.println("IO Error:" + e.getMessage());
} }
Um die GetFile-Applikation auszuführen, geben Sie eine URL als einziges Kommandozeilenargument an, z. B.: java GetFile http://tycho.usno.navy.mil/cgi-bin/timer.pl
Jede beliebige URL kann eingesetzt werden – tippen Sie http://www.mut.de für die Website von Markt+Technik oder http://random.yahoo.com/bin/ryl für einen Zufallslink aus dem Yahoo-Verzeichnis ein. Unser Beispiel lädt eine Seite von der offiziellen Zeitmessungssite des U.S. Naval Observatory (Abbildung 17.2). Zwei Drittel von Listing 17.2 dienen dazu, das Grundgerüst der Applikation zu erstellen, eine Benutzerschnittstelle zu kreieren und ein gültiges URL-Objekt zu erzeugen. Neu in diesem Projekt ist die getData()-Methode der Zeilen 41–63, die Daten aus der Ressource an der URL lädt und diese in einem Textfeld anzeigt.
517
Kommunikation über das Internet
Abbildung 17.2: Die GetFile-Applikation
Zuerst werden drei Objekte initialisiert: URLConnection, InputStreamReader und BufferedReader. Diese werden zusammen eingesetzt, um Daten aus dem Internet in die Java-Applikation einzulesen. Ferner werden zwei Objekte erzeugt, in denen die Daten gespeichert werden, wenn sie ankommen – ein String und ein StringBuffer. Die Zeilen 48–49 öffnen eine URL-Verbindung, damit ein Inputstream über diese Verbindung laufen kann. Zeile 52 benutzt die getInputStream()-Methode dieser URL-Verbindung, um einen neuen InputStreamReader zu erzeugen. Zeile 53 benutzt diesen InputStreamReader, um einen gepufferten InputStreamReader zu erzeugen – ein BufferedReader-Objekt namens data. Wenn Sie den gepufferten Reader haben, können Sie mit seiner readLine()-Methode eine Textzeile aus dem Inputstream lesen. Der gepufferte Reader lagert die Zeichen bei ihrem Eintreffen in einen Zwischenspeicher und holt sie heraus, sobald sie angefragt werden. Die while-Schleife in den Zeilen 56–57 liest das Internetdokument zeilenweise und hängt jede Zeile an das StringBuffer-Objekt, das zur Aufnahme des Texts der Seite erzeugt wurde. Wir benutzen einen String-Buffer und keinen String, weil man einen String in dieser Weise nicht zur Laufzeit modifizieren kann. Nachdem alle Daten gelesen sind, wandelt Zeile 59 den String-Buffer mit der toString()Methode in einen String um und setzt dann das Ergebnis in das Textfeld des Programms, indem sie die append(String)-Methode der Komponente aufruft. Eine try-catch-Anweisung umgibt den Code, der die Netzwerkverbindung öffnet, aus der Datei liest und den String erzeugt.
Sockets Für Netzwerkapplikationen, die über das hinausgehen, was die Klassen URL und URLConnection bieten (z. B. für andere Protokolle oder allgemeinere Netzwerkapplikationen), verfügt Java über die Klassen Socket und ServerSocket als Abstraktion von standardmäßigen TCPSocket-Programmiertechniken.
518
Netzwerkprogrammierung in Java
Java bietet ebenfalls Möglichkeiten der Verwendung von Datagram-Sockets (UDP), auf die hier allerdings nicht eingegangen werden soll. Wenn Sie sich für Datagramme interessieren, finden Sie entsprechende Informationen in der JavaKlassenbibliotheken-Dokumentation des java.net-Pakets. Sie können diese Dokumentation auf der Java-Website von Sun unter http://java.sun.com/j2se/ 1.4/docs/api/ herunterladen. Die Socket-Klasse bietet eine clientseitige Socket-Schnittstelle, die mit Unix-StandardSockets vergleichbar ist. Um eine Verbindung herzustellen, legen Sie eine neue Instanz von Socket an (wobei der hostName der Host ist, zu dem die Verbindung herzustellen ist, und portNumber die Portnummer ist): Socket connection = new Socket(hostName, portNumber);
Nach der Erstellung eines Sockets sollten Sie seinen Timeout-Wert festlegen, der angibt, wie lange die Applikation warten wird, bis Daten eingehen. Dies geschieht durch Aufruf der setSoTimeout(int)-Methode mit der abzuwartenden Zeit in Millisekunden als einzigem Argument: connection.setSoTimeout(50000);
Durch den Gebrauch dieser Methode warten alle Versuche, Daten aus dem Socket zu lesen, der durch connection repräsentiert wird, höchstens 50.000 Millisekunden (= 50 Sekunden). Wird das Timeout erreicht, wird eine InterruptedIOException ausgeworfen. Das ermöglicht Ihnen mithilfe eines try-catch-Blocks, entweder den Socket zu schließen oder noch einmal zu versuchen, aus ihm zu lesen. Wenn Sie in einem Programm mit Sockets kein Timeout festlegen, kann es vorkommen, dass es komplett hängen bleibt, während es auf nicht ankommende Daten wartet. Man vermeidet dieses Problem in der Regel dadurch, dass man Netzwerkoperationen in einen eigenen Thread legt und sie separat vom Rest des Programms ablaufen lässt. Wie das bei Animationen funktioniert, wurde an Tag 7 erklärt. Nachdem Sie den Socket geöffnet haben, können Sie Ein- und Ausgabestreams verwenden, um über diesen Socket zu lesen und zu schreiben: BufferedInputStream bis = new BufferedInputStream(connection.getInputStream()); DataInputStream in = new DataInputStream(bis); BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream()); DataOutputStream out= new DataOutputStream(bos);
519
Kommunikation über das Internet
Da Sie keine Namen für diese Objekte benötigen – sie werden lediglich zur Erzeugung eines Streams oder eines Stream-Readers verwendet –, kann man sich Arbeit sparen, indem man mehrere Anweisungen kombiniert. Dieses Beispiel benutzt ein Socket-Objekt namens sock: DataInputStream in = new DataInputStream( new BufferedInputStream( sock.getInputStream()));
Der Aufruf von sock.getInputStream() gibt einen Eingabestream zurück, der mit diesem Socket assoziiert ist. Dieser Stream wird zur Erzeugung eines BufferedInputStream verwendet, und der BufferedInputStream wiederum dient zur Erzeugung eines DataInputStream. Als Variablen bleiben nur sock und in, die noch gebraucht werden, während Sie Daten aus der Verbindung empfangen und wenn Sie sie danach schließen. Die Zwischenobjekte – ein BufferedInputStream und ein InputStream – werden nur einmal benötigt. Wenn Sie mit dem Socket fertig sind, schließen Sie ihn mit der close()-Methode. Dies schließt auch alle Ein- und Ausgabestreams, die Sie für diesen Socket eingerichtet haben. Also in unserem Beispiel: connection.close();
Socket-Programmierung kann für zahlreiche Dienste benutzt werden, die über TCP/IPVerbindungen laufen, z. B. Telnet, SMTP (E-Mail), NNTP (Usenet) und Finger. Dieser letzte Dienst, Finger, ist ein Protokoll, mit dem man ein System über einen seiner User befragt. Wenn ein Systemadministrator einen Finger-Server aufsetzt, ermöglicht er einem Rechner mit Internetanschluss, Nachfragen bezüglich User-Informationen zu beantworten. Anwender können Informationen über sich bereitstellen, indem sie .planDateien erstellen, die anderen übermittelt werden, wenn sie mit Finger anfragen. Aufgrund von Sicherheitsbedenken wird Finger in letzter Zeit nicht mehr häufig verwendet. Doch vor der Einführung des WWW war Finger die häufigste Form, in der Internetbenutzer Informationen über sich und ihre Aktivitäten publik machten. Sie konnten Finger auf der Account eines Freundes an einer anderen Universität anwenden und so herausfinden, ob er online war, und sein aktuelles .plan-File lesen. Es gibt heute noch eine Community, die persönliche Nachrichten über Finger verbreitet – die Spieleprogrammierer. Die GameFinger-Website, die als Gateway zwischen Web und Finger agiert, hat Links zu Dutzenden solcher Nostalgiker: http://finger.planetquake.com/. Die Applikation Finger ist ein rudimentärer Finger-Client und soll nur zur Übung der Socket-Programmierung dienen (Listing 17.3):
520
Netzwerkprogrammierung in Java
Listing 17.3: Der vollständige Quelltext von Finger.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38:
import java.io.*; import java.net.*; import java.util.*; public class Finger { public static void main(String[] arguments) { String user; String host; if ((arguments.length == 1) && (arguments[0].indexOf("@") > -1)) { StringTokenizer split = new StringTokenizer(arguments[0], "@"); user = split.nextToken(); host = split.nextToken(); } else { System.out.println("Usage: java Finger user@host"); return; } try { Socket digit = new Socket(host, 79); digit.setSoTimeout(20000); PrintStream out = new PrintStream(digit.getOutputStream()); out.print(user + "\015\012"); BufferedReader in = new BufferedReader( new InputStreamReader(digit.getInputStream())); boolean eof = false; while (!eof) { String line = in.readLine(); if (line != null) System.out.println(line); else eof = true; } digit.close(); } catch (IOException e) { System.out.println("IO Error:" + e.getMessage()); } } }
Wenn Sie eine Finger-Anfrage machen, müssen Sie einen Benutzernamen, gefolgt von einem @-Zeichen und dem Hostnamen, angeben, was dem Format einer E-Mail-Adresse entspricht. Ein Beispiel aus dem realen Leben ist [email protected] , die FingerAdresse des Gründers von id Software, John Carmack. Sie können sein .plan-File anfordern, wenn Sie die Finger-Applikation wie folgt starten:
521
Kommunikation über das Internet
java Finger [email protected]
Wenn johnc einen Account auf dem idsoftware.com-Finger-Server hat, wird das Programm seine .plan-Datei ausgeben und eventuell auch noch andere Informationen. Der Server gibt Ihnen auch Bescheid, falls der Benutzer nicht gefunden werden kann. Die GameFinger-Seite hat Adressen anderer Spieldesigner mit .plan-Dateien, wie Chris Hargrove ([email protected] ), Kenn Hoekstra ([email protected] ) und Pat Lipo ([email protected] ). Die Finger-Applikation benutzt die StringTokenizer-Klasse, um eine Adresse im Benutzer@Host-Format in zwei String-Objekte zu zerlegen: user und host (Zeilen 10–13). Die folgenden Socket-Aktivitäten finden in den folgenden Zeilen statt:
Zeilen 19–20: Ein neuer Socket wird erstellt. Dabei wird der Hostname und Port 79 (der traditionelle Finger-Port) benutzt, und ein Timeout von 20 Sekunden wird festgelegt.
Zeile 21: Mit dem Socket wird ein OutputStream erzeugt, der ein neues PrintStreamObjekt versorgt.
Zeile 22: Das Finger-Protokoll verlangt, dass der Benutzername durch den Socket geschickt wird, gefolgt von einem Wagenrücklauf (\015) und einem Zeilenvorschub (\012). Das geschieht durch Aufruf der print()-Methode des neuen PrintStream.
Zeilen 23–24: Nachdem der Benutzername gesendet wurde, muss ein Eingabestream für den Socket erstellt werden, damit er Eingaben vom Finger-Server entgegennehmen kann. Ein BufferedReaderstream namens in wird erzeugt, in dem mehrere StreamErzeugungsanweisungen kombiniert werden. Dieser Stream ist besonders gut für Finger-Input geeignet, da er zeilenweise lesen kann.
Zeilen 26-32: Das Programm liest in einer Schleife Zeilen aus dem gepufferten Reader. Das Ende der Ausgabe vom Server führt dazu, dass in.readLine() null zurückgibt, was die Schleife beendet.
Dieselben Techniken, mit denen über einen Socket mit einem Finger-Server kommuniziert wird, können benutzt werden, um Verbindungen zu anderen, wichtigeren Internetdiensten herzustellen. Sie könnten die Applikation in einen Telnet- oder Webclient verwandeln, indem Sie in Zeile 19 den Port ändern und einige andere kleine Modifikationen vornehmen.
Socket-Server Serverseitige Sockets funktionieren auf ähnliche Weise, mit Ausnahme der accept()Methode. Ein Server-Socket richtet sich nach einem TCP-Port, um eine Client-Verbindung aufzubauen; wenn sich ein Client mit diesem Port verbindet, akzeptiert die accept()-
522
Netzwerkprogrammierung in Java
Methode eine Verbindung von diesem Client. Durch Verwendung von Client- und Server-Sockets können Sie Applikationen entwickeln, die miteinander über das Netz kommunizieren. Um einen Server-Socket zu erstellen und an einen Port anzubinden, legen Sie eine neue Instanz von ServerSocket mit der Portnummer als Argument des Konstruktors an: ServerSocket servo = new ServerSocket(8888);
Um auf diesen Port zu horchen (und bei Anfrage eine Verbindung von Clients entgegenzunehmen), benutzen Sie die accept()-Methode: servo.accept();
Sobald die Socket-Verbindung aufgebaut ist, können Sie Ein- und Ausgabestreams verwenden, um vom Client zu lesen und an ihn zu schreiben. Im nächsten Abschnitt implementieren wir eine einfache Socket-basierte Anwendung. Um das Verhalten der Socket-Klassen zu erweitern – beispielsweise um es Netzwerkverbindungen zu ermöglichen, über eine Firewall oder einen Proxy zu arbeiten –, können Sie die abstrakten Klassen SocketImpl und die Schnittstelle SocketImplFactory verwenden, um eine neue Transportebene-Socket-Implementierung zu erstellen. Dieses Design stimmt mit dem ursprünglichen Konzept für die Java-Socket-Klassen überein, d. h. diesen Klassen zu ermöglichen, auf fremde Systeme mit anderen Transportmechanismen portierbar zu sein. Das Problem dieses Mechanismus besteht darin, dass, während er in einfachen Fällen funktioniert, er nicht erlaubt, zusätzlich zu TCP andere Protokolle hinzuzufügen (z. B. einen Verschlüsselungsmechanismus wie SSL zu realisieren) oder mehrere Socket-Implementierungen zur Java-Laufzeit zu haben. Deshalb wurden Sockets nach Java 1.0 so geändert, dass die Klassen Socket und ServerSocket nicht final und erweiterbar sind. Sie können jetzt Subklassen dieser Klassen erstellen, die entweder die Standard-Socket-Implementierung benutzen oder eine von Ihnen selbst kreierte. Dies gestaltet die Netzwerkfähigkeiten wesentlich flexibler.
Eine Serverapplikation entwerfen Im Folgenden betrachten wir das Beispiel eines Java-Programms, das die Socket-Klassen zur Realisierung einer einfachen netzwerkbasierten Serveranwendung benutzt. Die Applikation TimeServer verbindet sich mit jedem Client, der eine Verbindung zum Port 4413 herstellt, gibt die aktuelle Zeit aus und schließt die Verbindung. Damit eine Applikation als Server dienen kann, muss sie mindestens einen Port auf dem Hostrechner auf Clientverbindungen überwachen. In diesem Projekt wurde willkürlich Port 4413 ausgewählt, man könnte jedoch auch jede beliebige Zahl zwischen 1024 und 65535 wählen.
523
Kommunikation über das Internet
Die Internet Assigned Numbers Authority kontrolliert die Verwendung der Ports 0 bis 1023, doch es gibt informelle Ansprüche auf höhere Ports. Wenn Sie eine Portnummer für Ihre eigene Client/Server-Applikation auswählen, sollten Sie sich erkundigen, welche Ports von anderen eingesetzt werden. Suchen Sie im Web nach Erwähnungen des von Ihnen ausgewählten Ports. Sie können auch in Suchmaschinen mit den Suchbegriffen »Registered Port Numbers« oder »Well-Known Port Numbers« suchen, um Listen belegter Ports zu finden. Zudem finden Sie unter http://www.sockets.com/services.htm eine gute Übersicht zur Verwendung der Ports. Wird ein Client entdeckt, erzeugt der Server ein Date-Objekt, das das aktuelle Datum und die Zeit repräsentiert, und sendet es dem Client als String. Bei Informationsaustausch zwischen Server und Client leistet der Server fast die ganze Arbeit. Der Job des Clients besteht darin, eine Verbindung zum Server herzustellen und vom Server empfangene Nachrichten darzustellen. Es wäre kein großer Aufwand, einen Client für dieses Projekt zu entwickeln. Doch jede Telnet-Applikation kann als Client agieren, sofern sie zu einem beliebigen Port verbinden kann. Bei Windows ist eine Kommandozeilenapplikation namens telnet enthalten, die Sie für diesen Zweck benutzen können. In Listing 17.4 finden Sie den vollständigen Quelltext der Serverapplikation. Listing 17.4: Der vollständige Quelltext von TimeServer.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
524
import java.io.*; import java.net.*; import java.util.*; public class TimeServer extends Thread { private ServerSocket sock; public TimeServer() { super(); try { sock = new ServerSocket(4413); System.out.println("TimeServer running ..."); } catch (IOException e) { System.out.println("Error: couldn't create socket."); System.exit(1); } } public void run() { Socket client = null;
Netzwerkprogrammierung in Java
21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: }
while (true) { if (sock == null) return; try { client = sock.accept(); BufferedOutputStream bos = new BufferedOutputStream( client.getOutputStream()); PrintWriter os = new PrintWriter(bos, false); String outLine; Date now = new Date(); os.println(now); os.flush(); os.close(); client.close(); } catch (IOException e) { System.out.println("Error: couldn't connect to client."); System.exit(1); } } } public static void main(String[] arguments) { TimeServer server = new TimeServer(); server.start(); }
Den Server testen Die TimeServer-Applikation muss laufen, damit der Client sich mit dem Server verbinden kann. Dazu müssen Sie zuerst den Server starten: java TimeServer
Der Server wird nur eine Zeile ausgeben, wenn er erfolgreich läuft: TimeServer running ...
Wenn der Server läuft, können Sie sich mit ihm mithilfe eines Telnet-Programms verbinden. Bei Windows wird dieses Programm beispielsweise mitgeliefert.
525
Kommunikation über das Internet
Um Telnet unter Windows 95, 98, Me, NT oder 2000 zu starten, klicken Sie auf Start, dann Ausführen. Tippen Sie nun »telnet« in das Textfeld und drücken Sie auf Return. Ein Telnet-Fenster öffnet sich. Sie stellen mit diesem Programm eine Telnet-Verbindung her, indem Sie das Menükommando Verbinden/Netzwerksystem wählen. Ein Dialogfenster öffnet sich (Abbildung 17.3). Geben Sie »localhost« in Hostname-Feld ein, »4413« ins Portfeld und lassen Sie den vorgegebenen Wert »vt100« im Terminaltypfeld stehen.
Abbildung 17.3: Eine Telnet-Verbindung herstellen
Um Telnet unter Windows XP zu verwenden, klicken Sie auf Start/Ausführen, geben dann direkt »telnet localhost 4413« in das Textfeld ein und drücken die Eingabetaste. Der Hostname localhost repräsentiert Ihren eigenen Computer, also den Rechner, auf dem die Applikation läuft. Man kann dies zum Testen von Serverapplikatonen einsetzen, bevor man sie ins Internet stellt. Je nach Konfiguration der Internetverbindungen auf Ihrem System ist es möglich, dass Sie erst eine Internetverbindung herstellen müssen, bevor eine erfolgreiche Socket-Verbindung zwischen dem Telnet-Client und der TimeServer-Applikation hergestellt werden kann. Wenn sich der Server auf einem anderen Computer mit Internetanschluss befindet, tragen Sie den Hostnamen des Computers oder seine IP-Adresse statt »localhost« ein. Wenn Sie Telnet verwenden, um eine Verbindung zur TimeServer-Applikation herzustellen, zeigt sie die aktuelle Zeit des Servers an und schließt die Verbindung. Die Ausgabe des Telnet-Programms sollte ungefähr so aussehen: Thu Jun 13 18:41:10 EST 2002 Connection to host lost. Press any key to continue...
526
Das Paket java.nio
17.2 Das Paket java.nio Java 2 Version 1.4 beinhaltet das neue Paket java.nio, eine Gruppe von Klassen, die die Netzwerkfähigkeiten der Sprache erweitern. Das neue Ein-/Ausgabepaket erleichtert folgende Aktionen: Lesen und Schreiben von Daten; Umgang mit Dateien, Sockets und Speicher; Behandlung von Text. Darüber hinaus gibt es zwei verwandte Pakete, die häufig benutzt werden, wenn man mit den neuen Ein-/Ausgabefeatures arbeitet: java.nio.channels und java.nio.charset.
Buffers Das Paket java.nio bietet Unterstützung für Buffers, Objekte, die Datenstreams repräsentieren, die bereits im Speicher abgelegt sind. Man verwendet Buffers häufig, um die Performance von Programmen zu verbessern, die Eingaben lesen oder Ausgaben schreiben. Sie ermöglichen einem Programm, viele Daten im Speicher abzulegen, wo sie schneller verwendet oder modifiziert werden können. Es gibt für jeden primitiven Datentyp von Java einen Buffer:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Jede dieser Klassen hat eine statische Methode namens wrap(), mit der man einen Buffer aus einem Array des korrespondierenden Datentyps erzeugen kann. Das einzige Argument der Methode ist das Array. Zum Beispiel erzeugen die folgenden Anweisungen ein Integer-Array und einen IntBuffer, der die Integer im Speicher als Buffer ablegt: int[] temperatures = { 90, 85, 87, 78, 80, 75, 70, 79, 85, 92, 99 }; IntBuffer tempBuffer = IntBuffer.wrap(temperatures);
Ein Buffer verfolgt seine Verwendung und speichert die Position, an der der nächste Wert geschrieben bzw. gelesen wird. Nach der Erzeugung eines Buffers kann man seine get()-
527
Kommunikation über das Internet
Methode verwenden, um den Wert an der aktuellen Position im Buffer auszulesen. Die folgenden Anweisungen erweitern das letzte Beispiel und zeigen alles im Integer-Buffer an: for (int i = 0; tempBuffer.remaining() > 0; i++) System.out.println(tempBuffer.get());
Man kann einen Buffer auch dadurch erzeugen, dass man einen leeren Buffer einrichtet und dann Daten hineinlegt. Um einen Buffer zu erzeugen, ruft man die statische Methode allocate(int) der Bufferklasse auf, wobei die Buffergröße als Argument übergeben wird. Es gibt fünf put()-Methoden, mit denen man Daten in einem Buffer speichert (oder bereits dort befindliche Daten ersetzt). Die Argumente für diese Methoden hängen von der Art des Buffers ab. Bei einem Integer-Buffer gibt es folgende Argumente:
put(int) – speichert den Integer an der aktuellen Position im Buffer und inkrementiert dann die Position.
put(int, int) – speichert an der angegebenen Position (das erste Argument) einen
Integer (das zweite Argument) im Buffer.
put(int[]) – speichert alle Elemente des Integer-Arrays im Buffer, beginnend mit der ersten Position des Buffers.
put(int[], int, int) – speichert einen Integer-Array ganz oder teilweise im Buffer.
Das zweite Argument legt die Position im Buffer fest, wo der erste Integer des IntegerArrays gespeichert werden soll. Das dritte Argument gibt die Anzahl der Elemente des Arrays an, die im Buffer gespeichert werden sollen.
put(intBuffer) – speichert den Inhalt eines Integer-Buffers in einem anderen Buffer, beginnend mit der ersten Position des Buffers.
Wenn Sie Daten in einem Buffer schreiben, ist es oft wichtig, die aktuelle Position zu kennen, sodass Sie wissen, wo der nächste Wert gespeichert wird. Um die aktuelle Position herauszufinden, rufen Sie die position()-Methode des Buffers auf. Es wird ein Integer zurückgegeben, der die Position angibt. Ist dieser 0, dann stehen Sie am Anfang des Buffers. Rufen Sie die Methode position(int) auf, um die Position auf das übergebene Argument abzuändern. Eine weitere wichtige Position, die man beim Gebrauch von Buffers im Auge behalten muss, ist das Limit – der letzte Platz im Buffer, der Daten enthält. Man muss sich um das Limit nicht kümmern, wenn der Buffer immer voll ist; in diesem Fall wissen Sie, dass an der letzten Position des Buffers etwas gespeichert ist. Wenn es jedoch sein könnte, dass Ihr Buffer weniger Daten enthält, als Sie als Größe festgelegt haben, dann sollten Sie die flip()-Methode des Buffers aufrufen, nachdem Sie Daten in den Buffer geschrieben haben. Damit wird die aktuelle Position auf den Anfang und das Limit auf das Ende der soeben geschriebenen Daten festgelegt.
528
Das Paket java.nio
Wir werden heute noch einen Byte-Buffer benutzen, um Daten zu speichern, die wir von einer Webseite im Internet herunterladen. Hier ist man auf die flip()-Methode angewiesen, weil man nicht wissen kann, wie viele Daten die Webseite enthalten wird. Wenn der Buffer 1.024 Byte groß ist und die Seite 1.500 Byte hat, führt der erste Leseversuch dazu, dass 1.024 Byte in den Buffer geladen werden und ihn komplett füllen. Der zweite Versuch füllt den Buffer mit nur 476 Byte und lässt den Rest leer. Wenn Sie danach flip() aufrufen, wird die aktuelle Position auf den Anfang und das Limit auf 476 festgelegt. Der folgende Code erzeugt ein Array von Fahrenheit-Temperaturen, wandelt sie in Celsius um und speichert die Celsius-Werte in einem Buffer: int[] temps = { 90, 85, 87, 78, 80, 75, 70, 79, 85, 92, 99 }; IntBuffer tempBuffer = IntBuffer.allocate(temperatures.length); for (int i = 0; i < temps.length; i++) { float celsius = ( (float)temps[i] – 32 ) / 9 * 5; tempBuffer.put( (int)celsius ); }; tempBuffer.position(0); for (int i = 0; tempBuffer.remaining() > 0; i++) System.out.println(tempBuffer.get());
Nachdem die Position des Buffers auf den Anfang zurückgesetzt wurde, wird der Inhalt des Buffers angezeigt.
Byte-Buffers Die Buffer-Methoden, die Sie bislang gelernt haben, können Sie mit Byte-Buffers benutzen, doch Byte-Buffers bieten einige zusätzliche Methoden. Byte-Buffers haben Methoden, um Daten zu speichern und auszulesen, die nicht in Bytes vorliegen:
putChar(char) – speichert zwei Bytes im Buffer, die den angegebenen char-Wert reprä-
sentieren.
putDouble(double) – speichert acht Bytes im Buffer, die den double-Wert repräsentieren.
putFloat(float) – speichert vier Bytes im Buffer, die den float-Wert repräsentieren.
putInt(int) – speichert vier Bytes im Buffer, die den int-Wert repräsentieren.
putLong(long) – speichert acht Bytes im Buffer, die den long-Wert repräsentieren.
putShort(short) – speichert zwei Bytes im Buffer, die den short-Wert repräsentieren.
529
Kommunikation über das Internet
Alle diese Methoden setzen mehr als ein Byte in den Buffer und bewegen die aktuelle Position um die entsprechende Anzahl an Bytes vorwärts. Des Weiteren gibt es eine Gruppe von Methoden, die umgekehrt Nicht-Bytes aus einem Byte-Buffer lesen: getChar(), getDouble(), getFloat(), getInt(), getLong() und getShort().
Zeichensätze Zeichensätze finden sich im java.nio.charset-Paket und sind eine Reihe von Klassen, mit denen man Daten zwischen Byte-Buffers und Zeichen-Buffers umwandeln kann. Es gibt drei Hauptklassen:
Charset – ein Unicode-Zeichensatz mit einem unterschiedlichen Byte-Wert für jedes einzelne Zeichen im Satz
Decoder – eine Klasse, die eine Reihe von Bytes in eine Reihe von Zeichen umwandelt
Encoder – eine Klasse, die eine Reihe von Zeichen in eine Reihe von Bytes umwandelt
Ehe Sie Konvertierungen zwischen Byte- und Zeichen-Buffers vornehmen können, müssen Sie erst ein Charset-Objekt erzeugen, das den Zeichen die entsprechenden Bytewerte zuordnet. Um ein Charset zu erzeugen, rufen Sie die statische Methode forName(String) der Charset-Klasse auf, wobei der String den Namen der Zeichencodierung angibt. Java 2 Version 1.4 unterstützt acht Zeichencodierungen:
US-ACII – der 128-Zeichen-ASCII-Zeichensatz, der dem Basic-Latin-Block von Unicode entspricht (auch ISO646-US genannt)
ISO-8859-1 – der 256-Zeichen-ISO-Latin-Alphabet-No.-1.a.-Zeichensatz (auch ISOLATIN-1 genannt)
UFT-8 – ein Zeichensatz, der US-ASCII und das Universal Character Set (auch Unicode genannt) umfasst. Er umfasst Tausende von Zeichen, die von den verschiedenen Sprachen der Welt verwendet werden.
UFT-16BE – das Universal Character Set, repräsentiert von 16-Bit-Zeichen mit Bytes, die in Big-Endian-Bytefolge gespeichert sind.
UFT-16LE – das Universal Character Set, repräsentiert von 16-Bit-Zeichen mit Bytes, die in Little-Endian-Bytefolge gespeichert sind.
UFT-16 – das Universal Character Set, repräsentiert von 16-Bit-Zeichen, wobei die Bytefolge von einem optionalen Bytefolgenmarker angegeben wird.
530
Das Paket java.nio
Die folgende Anweisung erzeugt ein Charset-Objekt für den ISO-8859-1-Zeichensatz: Charset isoset = Charset.forName("ISO-8859-1");
Sobald Sie ein Charset-Objekt haben, können Sie damit Encoder und Decoder erzeugen. Rufen Sie die newDecoder()-Methode auf, um einen CharsetDecoder und die newEncoder()Methode um einen CharsetEncoder zu erzeugen. Um einen Byte-Buffer in einen Zeichen-Buffer umzuwandeln, rufen Sie die decode(ByteBuffer)-Methode des Decoders auf, die einen CharBuffer zurückgibt, der die gewünschten Zeichen beinhaltet. Um einen Zeichen-Buffer in einen Byte-Buffer umzuwandeln, rufen Sie die encode(CharBuffer)-Methode des Encoders auf. Es wird ein ByteBuffer zurückgegeben, der die Bytewerte der Zeichen beinhaltet. Die folgenden Anweisungen wandeln einen Byte-Buffer namens netBuffer in einen Zeichenbuffer im ISO-8859-1-Zeichensatz um: Charset set = Charset.forName("ISO-8859-1"); CharsetDecoder decoder = set.newDecoder(); netBuffer.position(0); CharBuffer netText = decoder.decode(netBuffer);
Bevor der Decoder benutzt wird, um den Zeichen-Buffer zu erzeugen, setzt der Aufruf von position(0) die aktuelle Position des netBuffer auf den Anfang zurück. Wenn man das erste Mal mit Buffers arbeitet, übersieht man dies leicht und verpasst so eine Menge der Daten, die im Buffer gewesen wären.
Channels Häufig assoziiert man Buffer mit einem Eingabe- oder Ausgabestream. Man kann einen Buffer mit Daten aus einem Eingabestream füllen oder ihn in einen Ausgabestream schreiben. Dazu verwendet man einen Channel, ein Objekt, das einen Buffer mit einem Stream verbindet. Channels sind Teil des Pakets java.nio.channels. Channels werden mit einem Stream assoziiert, indem man die getChannel()-Methode aufruft, über die manche Stream-Klassen des java.io-Pakets verfügen. Die Klassen FileInputStream und FileOutputStream haben getChannel()-Methoden, die ein FileChannel-Objekt zurückgeben. Dieser Datei-Channel kann benutzt werden, um die Daten in der Datei zu lesen, zu schreiben oder zu verändern. Die folgenden Anweisungen erzeugen einen Datei-Eingabestream und einen Channel, der mit dieser Datei assoziiert ist:
531
Kommunikation über das Internet
try { String source = "prices.dat"; FileInputStream inSource = new FileInputStream(source); FileChannel inChannel = inSource.getChannel(); } catch (FileNotFoundException fne) { System.out.println(fne.getMessage()); }
Nachdem Sie den Datei-Channel erzeugt haben, können Sie herausfinden, wie viele Bytes die Datei beinhaltet, indem sie ihre size()-Methode aufrufen. Das ist notwendig, wenn Sie einen Byte-Buffer erzeugen wollen, der den Inhalt der Datei speichern soll. Man liest mit der read(ByteBuffer, long)-Methode Bytes aus einem Channel in einen Byte-Buffer. Das erste Argument ist der Buffer. Das zweite Argument ist die aktuelle Position im Buffer, die festlegt, ab welcher Stelle der Inhalt der Datei gespeichert wird. Die folgenden Anweisungen ergänzen das letzte Beispiel, indem eine Datei mithilfe des inChannel-Datei-Channels in einen Byte-Buffer gelesen wird: long inSize = inChannel.size(); ByteBuffer data = ByteBuffer.allocate( (int)inSize ); inChannel.read(data, 0); data.position(0); for (int i = 0; data.remaining() > 0; i++) System.out.print(data.get() + " ");
Der Versuch, aus einem Channel zu lesen, führt zu einer IOException, wenn ein Problem auftritt. Zwar hat der Byte-Buffer dieselbe Größe wie die Datei, doch dies ist nicht verpflichtend. Wenn Sie die Datei in einen Buffer lesen, um sie dort zu verändern, können Sie ruhig einen größeren Buffer festlegen. Unser nächstes Projekt wird einige der neuen Input/Output-Technologien beinhalten, die Sie kennen gelernt haben: Buffers, Zeichensätze und Channels. Die Applikation ChangeBuffer liest eine kleine Datei in einen Byte-Buffer, zeigt den Inhalt des Buffers an, verwandelt ihn in einen Zeichen-Buffer und gibt dann die Zeichen aus. Geben Sie den Text aus Listing 17.5 ein, und speichern Sie ihn als ChangeBuffer.java. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
532
import import import import
java.nio.*; java.nio.channels.*; java.nio.charset.*; java.io.*;
public class ChangeBuffer { public static void main(String[] arguments) { try { // Byte-Daten in einen Byte-Buffer lesen String data = "friends.dat"; FileInputStream inData = new FileInputStream(data); FileChannel inChannel = inData.getChannel();
Das Paket java.nio
13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: }
long inSize = inChannel.size(); ByteBuffer source = ByteBuffer.allocate( (int)inSize ); inChannel.read(source, 0); source.position(0); System.out.println("Original byte data:"); for (int i = 0; source.remaining() > 0; i++) System.out.print(source.get() + " "); // Byte-Daten in Zeichendaten konvertieren source.position(0); Charset ascii = Charset.forName("US-ASCII"); CharsetDecoder toAscii = ascii.newDecoder(); CharBuffer destination = toAscii.decode(source); destination.position(0); System.out.println("\n\nNew character data:"); for (int i = 0; destination.remaining() > 0; i++) System.out.print(destination.get()); } catch (FileNotFoundException fne) { System.out.println(fne.getMessage()); } catch (IOException ioe) { System.out.println(ioe.getMessage()); } }
Nachdem Sie die Datei kompiliert haben, benötigen Sie friends.dat, eine kleine Datei mit Byte-Daten, die wir für diese Applikation verwenden. Sie können sie auf der Website zum Buch unter http://www.java21pro.com herunterladen. Öffnen Sie die Seite zu Tag 17, klicken Sie auf den Link »friends.dat«, und speichern Sie die Datei im selben Ordner wie ChangeBuffer.class. Sie können auch Ihre eigene Versuchsdatei erzeugen. Öffnen Sie einen Texteditor, geben Sie einen oder zwei Sätze ein, und speichern Sie das Ganze unter friends.dat ab. Wenn Sie die friends.dat von der Website zum Buch verwenden, sieht die Ausgabe der ChangeBuffer-Applikation wie folgt aus: Original byte data: 70 114 105 101 110 100 115 44 32 82 111 109 97 110 115 44 32 99 111 117 110 116 114 121 109 101 110 44 32 108 101 110 100 32 109 101 32 121 111 117 114 32 101 97 114 115 46 13 10 13 10 New character data: Friends, Romans, countrymen, lend me your ears.
533
Kommunikation über das Internet
Die Applikation ChangeBuffer verwendet die heute erklärten Techniken, um Daten zu lesen und als Bytes bzw. Zeichen zu repräsentieren. Doch Sie hätten dasselbe mit dem alten Ein-/Ausgabepaket java.io leisten können. Vielleicht fragen Sie sich deswegen bereits, warum es sich überhaupt lohnt, das neue Paket kennen zu lernen. Ein Grund ist, dass Sie mit Buffers große Mengen an Daten wesentlich schneller bearbeiten können. Einen weiteren Grund lernen Sie im nächsten Abschnitt kennen.
Netzwerk-Channels Das wichtigste Feature des Pakets java.nio ist vermutlich die Unterstützung für nicht blockierende Ein-/Ausgabe über eine Netzwerkverbindung. In Java bezieht sich blockierend auf eine Anweisung, die abgehandelt werden muss, bevor irgendetwas anderes im Programm geschehen kann. Die gesamte Socket-Programmierung, die Sie bislang betrieben haben, benutzte ausschließlich blockierende Methoden. Bei der TimeServer-Applikation z. B. passierte nichts anderes im Programm, nachdem die accept()-Methode des Server-Sockets aufgerufen wurde. Erst, wenn der Client die Verbindung hergestellt hat, endet die Blockade. Wie Sie sich vorstellen können, ist es problematisch, wenn ein Netzwerkprogramm warten muss, bis eine bestimmte Anweisung abgehandelt ist, denn einiges kann schief gehen. Verbindungen reißen ab. Server gehen offline. Eine Socket-Verbindung scheint abgestürzt zu sein, weil eine blockierende Anweisung auf irgendetwas wartet. Eine Applikation könnte beispielsweise Daten mittels einer HTTP-Verbindung lesen und diese Daten puffern. Wenn das Programm darauf wartet, dass der Buffer gefüllt wird, obwohl keine Daten mehr gesendet werden können, erscheint dies wie ein Absturz, weil die blockierende Anweisung niemals abgearbeitet wird. Mit dem Paket java.nio können Sie Netzwerkverbindungen erzeugen und mittels nicht blockierender Methoden lesen und schreiben. So funktioniert das Ganze:
Sie assoziieren einen Socket-Channel mit einem Eingabe- oder Ausgabestream.
Sie konfigurieren den Channel so, dass er die Art von Netzwerkereignissen erkennt, die Sie überwachen wollen: neue Verbindungen, Leseversuche über den Kanal, Schreibversuche.
Sie rufen eine Methode auf, um den Channel zu öffnen.
Da die Methode nicht blockierend ist, läuft das Programm weiter und kann sich um andere Dinge kümmern.
534
Das Paket java.nio
Wenn eines der überwachten Netzwerkereignisse eintritt, wird Ihr Programm durch den Aufruf einer mit dem Ereignis assoziierten Methode benachrichtigt.
Das funktioniert so ähnlich wie die Programmierung von Komponenten einer SwingBenutzerschnittstelle. Eine Schnittstellenkomponente wird mit einem oder mehreren Event-Listeners assoziiert und in einen Container gelegt. Wenn die Schnittstellenkomponente eine Eingabe erhält, die vom Listener überwacht wird, wird eine Ereignisbehandlungsmethode aufgerufen. Bis dies geschieht, kann das Programm andere Dinge erledigen. Um nicht blockierende Ein-/Ausgabe zu verwenden, müssen Sie mit Channels statt mit Streams arbeiten.
Nicht blockierende Socket-Clients Der erste Schritt bei der Entwicklung eines Clients besteht darin, ein Objekt zu erzeugen, das die Internetadresse repräsentiert, mit der man eine Verbindung herstellt. Diese Aufgabe wird von der neuen InetSocketAddress-Klasse im Paket java.net erledigt. Wenn der Server mit einem Hostnamen identifiziert wird, rufen Sie die InetSocketAddress(String, int)-Methode mit zwei Argumenten auf: dem Namen des Servers und der Portnummer. Wenn der Server mit einer IP-Nummer identifiziert wird, verwenden Sie die InetAddressKlasse in java.net, um den Host anzugeben. Rufen Sie die statische Methode InetAddress.getByName(String) mit der IP-Adresse des Hosts als Argument auf. Die Methode gibt ein InetAddress-Objekt zurück, das die Adresse repräsentiert, die Sie dann für einen Aufruf von InetSocketAddress(InetAddress, int) verwenden können. Das zweite Argument ist die Portnummer des Servers. Nicht blockierende Verbindungen benötigen einen Socket-Channel, eine weitere neue Klasse im java.nio-Paket. Rufen Sie die statische Methode open() der SocketChannelKlasse auf, um den Channel zu erzeugen. Ein Socket-Channel kann für blockierende bzw. nicht blockierende Kommunikation konfiguriert werden. Um einen nicht blockierenden Channel einzurichten, rufen Sie die configureBlocking(boolean)-Methode mit dem Argument false auf. true würde den Channel als blockierend einrichten. Sobald der Channel konfiguriert ist, können Sie seine connect()-Methode aufrufen, um die Socket-Verbindung herzustellen. Bei einem blockierenden Channel versucht die connect()-Methode, die Verbindung zum Server herzustellen und wartet bis zum Erfolg. Dann wird der Wert true zurückgegeben. Bei einem nicht blockierenden Channel gibt die connect()-Methode unmittelbar false zurück. Um herauszufinden, was auf dem Channel passiert und um auf Ereignisse reagieren zu können, müssen Sie ein Channel überwachendes Objekt namens Selector verwenden.
535
Kommunikation über das Internet
Ein Selector ist ein Objekt, das die Dinge verfolgt, die auf einem Socket-Channel passieren (oder auf einem anderen Channel im Paket, der eine Subklasse von SelectableChannel ist). Um einen Selector zu erzeugen, rufen Sie seine open()-Methode auf, z. B.: Selector monitor = Selector.open();
Wenn man einen Selector verwendet, muss man die Ereignisse angeben, die man überwachen will. Dies geschieht durch einen Aufruf der register(Selector, int, Object)Methode eines Channels. Die drei Argumente für register() sind:
das Selector-Objekt, das Sie zur Überwachung des Channels erzeugt haben
ein int-Wert, der die Ereignisse repräsentiert, die überwacht werden sollen (auch Selection Key genannt)
ein Object, das mit dem Key übergeben werden kann, ansonsten null
Anstelle von Integerwerten als zweitem Argument ist es einfacher, eine oder mehrere Klassenvariablen der Klasse SelectionKey zu verwenden: SelectionKey.OP_CONNECT, um Verbindungen zu überwachen, SelectionKey.OP_READ, um Leseversuche zu überwachen und SelectionKey.OP_WRITE, um Schreibversuche zu überwachen. Die folgenden Anweisungen erzeugen einen Selector, der einen Socket-Channel namens wire hinsichtlich Leseversuchen überwacht: Selector spy = Selector.open(); channel.register(spy, SelectionKey.OP_READ, null);
Um mehr als eine Key-Art zu überwachen, addieren Sie die SelectionKey-Klassenvariablen zusammen, z. B.: Selector spy = Selector.open(); channel.register(spy, SelectionKey.OP_READ + SelectionKey.OP_WRITE, null);
Sobald Channel und Selector eingerichtet sind, können Sie auf Ereignisse warten, indem Sie die select()- bzw. select(long)-Methode des Selectors aufrufen. Die Methode select() ist eine blockierende Methode, die so lange wartet, bis etwas auf dem Channel geschehen ist. Die Methode select(long) ist eine blockierende Methode, die wartet, bis etwas geschehen ist oder bis die angegebene Anzahl an Millisekunden verstrichen ist. Beide select()-Methoden geben die Zahl der Ereignisse zurück, die eingetreten sind, bzw. 0, wenn nichts passierte. Sie können eine while-Schleife verwenden und darin die select()-Methode aufrufen, um zu warten, bis etwas auf dem Channel geschieht.
536
Das Paket java.nio
Sobald ein Ereignis eingetreten ist, können Sie mehr über es herausfinden, indem Sie die selectedKeys()-Methode des Selectors aufrufen, die ein Set-Object zurückgibt, das Details über die einzelnen Ereignisse beinhaltet. Verwenden Sie dieses Set-Object wie jedes andere: Erzeugen Sie einen Iterator, der sich durch das Set bewegt und dabei die hasNext()- und next()-Methoden verwendet. Der Aufruf der next()-Methode des Sets gibt ein Objekt zurück, das in einen SelectionKey gecastet werden sollte. Dieses Objekt repräsentiert ein Ereignis, das auf dem Channel stattfand. Die Klasse SelectionKey beinhaltet drei Methoden mit denen man den Key in einem Clientprogramm identifizieren kann: isReadable(), isWriteable() und isConnectible(). Sie geben jeweils einen booleschen Wert zurück. Eine vierte Methode steht bei Servern zur Verfügung: isAcceptable(). Nachdem Sie einen Key aus einem Set ausgelesen haben, rufen Sie die remove()-Methode des Keys auf, um anzuzeigen, dass Sie sich darum kümmern. Abschließend müssen Sie noch den Channel herausfinden, auf dem das Ereignis stattfand. Rufen Sie die channel()-Methode des Keys auf, die den dazugehörigen SocketChannel zurückgibt. Wenn eines der Ereignisse eine Verbindung bezeichnet, müssen Sie sicherstellen, dass die Verbindung geschlossen wurde, bevor Sie den Channel verwenden. Rufen Sie die Methode isConnectionPending() des Keys auf, die true zurückgibt, wenn die Verbindung noch andauert, bzw. false, wenn sie beendet ist. Um mit einer Verbindung zu arbeiten, die noch andauert, rufen Sie die finishConnect()Methode des Sockets auf, die versucht, die Verbindung zu schließen. Wenn man einen nicht blockierenden Socket-Channel benutzen will, muss man zahlreiche neue Klassen aus den Paketen java.nio und java.net verwenden. Unser letztes Projekt für heute will Ihnen ein besseres Bild vermitteln, wie diese Klassen interagieren. Es handelt sich um LoadURL, eine Webapplikation, die einen nicht blockierenden Socket-Channel verwendet, um den Inhalt einer URL zu laden. Geben Sie den Text aus Listing 17.6 ein, speichern Sie ihn als LoadURL, und kompilieren Sie die Applikation. Listing 17.5: Der vollständige Quelltext von LoadURL.java 1: 2: 3: 4: 5:
import import import import import
java.nio.*; java.nio.channels.*; java.nio.charset.*; java.io.*; java.net.*;
537
Kommunikation über das Internet
6: import java.util.*; 7: 8: public class LoadURL { 9: 10: public LoadURL(String urlRequest) { 11: SocketChannel sock = null; 12: try { 13: URL url = new URL(urlRequest); 14: String host = url.getHost(); 15: String page = url.getPath(); 16: InetSocketAddress address = new InetSocketAddress(host, 80); 17: Charset iso = Charset.forName("ISO-8859-1"); 18: CharsetDecoder decoder = iso.newDecoder(); 19: CharsetEncoder encoder = iso.newEncoder(); 20: 21: ByteBuffer byteData = ByteBuffer.allocate(16384); 22: CharBuffer charData = CharBuffer.allocate(16384); 23: 24: sock = SocketChannel.open(); 25: sock.configureBlocking(false); 26: sock.connect(address); 27: 28: Selector listen = Selector.open(); 29: sock.register(listen, SelectionKey.OP_CONNECT + 30: SelectionKey.OP_READ); 31: 32: while (listen.select(500) > 0) { 33: Set keys = listen.selectedKeys(); 34: Iterator i = keys.iterator(); 35: while (i.hasNext()) { 36: SelectionKey key = (SelectionKey) i.next(); 37: i.remove(); 38: SocketChannel keySock = (SocketChannel) key.channel(); 39: if (key.isConnectable()) { 40: if (keySock.isConnectionPending()) { 41: keySock.finishConnect(); 42: } 43: CharBuffer httpReq = CharBuffer.wrap( 44: "GET " + page + "\n\r\n\r"); 45: ByteBuffer request = encoder.encode(httpReq); 46: keySock.write(request); 47: } else if (key.isReadable()) { 48: keySock.read(byteData); 49: byteData.flip(); 50: charData = decoder.decode(byteData); 51: charData.position(0);
538
Das Paket java.nio
52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: }
System.out.print(charData); byteData.clear(); charData.clear(); } } } sock.close(); } catch (MalformedURLException mue) { System.out.println(mue.getMessage()); } catch (UnknownHostException uhe) { System.out.println(uhe.getMessage()); } catch (IOException ioe) { System.out.println(ioe.getMessage()); } } public static void main(String arguments[]) { LoadURL app = new LoadURL(arguments[0]); }
Starten Sie die LoadURL-Applikation mit einem Argument: entweder mit der URL einer Webseite, einer XML-Datei oder einer anderen Textdatei im Web: java LoadURL http://www.jabbercentral.org/rss.php
Die Ausgabe der Applikation ist entweder der Inhalt der Datei oder eine Fehlermeldung, die vom Webserver erzeugt wurde. Das vorherige Beispiel lädt eine XML-Datei, die die aktuellen Schlagzeilen der Instant-Messaging-Newssite »Jabber Central« enthält. Die Applikation LoadURL verwendet die Techniken, die heute bereits in zahlreichen Beispielen zum Einsatz kamen. Ein Ausnahme: In den Zeilen 13–15 wird ein URL-Objekt erzeugt, das die URL verwendet, die der Applikation als Argument übergeben wurde. Das ist für die Verwendung des Sockets nicht unbedingt notwendig, aber es ist eine bequeme Möglichkeit, die Gültigkeit einer benutzerübergebenen URL zu überprüfen und sie in den Host der URL und die angefragte Datei aufzuteilen. Die Applikation erzeugt einen nicht blockierenden Socket-Channel und registriert zwei Arten von Keys, auf die der Selector achten soll: Verbindungs- und Lese-Ereignisse. Die while-Schleife, die in Zeile 32 beginnt, überprüft, ob der Selector Keys erhalten hat. Ist dies der Fall, dann gibt select(500) die Zahl der Keys zurück, und die Anweisungen im Inneren der Schleife werden ausgeführt.
539
Kommunikation über das Internet
Wenn sich der Key auf eine Verbindung bezieht, überprüft die Applikation in Zeile 40, ob die Verbindung noch besteht. Wenn dies so ist, wird mittels eines Aufrufs von finishConnect() in Zeile 41 versucht, die Verbindung zu schließen. Sobald die Verbindung hergestellt ist, wird ein Zeichen-Buffer erzeugt, der die Anfrage nach einer Webdatei beinhalten soll. Die Anfrage verwendet HTTP, das Protokoll des World Wide Web. Auf das Kommando GET folgt ein Leerzeichen, der Name der angefragten Datei und ein paar Newlines und Wagenrückläufe, um das Ende der Anfrage anzuzeigen. Bevor die Anfrage gesendet werden kann, wird sie in Zeile 45 in einen Byte-Buffer umgewandelt. Der Aufruf von write(ByteBuffer) in Zeile 46 sendet die Anfrage über den Socket-Channel. Wenn sich der Key auf das Lesen von Daten bezieht, verwendet die Applikation den 16KByte-Buffer und den Zeichen-Buffer, die in den Zeilen 21–22 erzeugt wurden. Diese Buffers wurden zur Wiederverwendung erzeugt. Einer der Vorteile von Buffers besteht darin, dass man weniger Objekte benötigt, was den Programmablauf beschleunigt. In den Zeilen 48–49 werden bis zu 16K in den byteData-Buffer gelesen. Der Aufruf von flip() bewirkt zweierlei: Er legt die aktuelle Position auf den Anfang des Buffers fest und bestimmt als Limit das letzte Byte, das in den Buffer gelesen wurde. Das Limit ist bei Position 16384, wenn ganze 16K vom Socket gelesen wurden, oder bei einer kleineren Position, wenn das Ende der Datei erreicht wurde und weniger Daten vom Webserver geliefert worden waren. In Zeile 50 wird der charData-Zeichenbuffer geladen, indem der Byte-Buffer mittels des ISO-8859-1-Zeichensatzes, der in den Zeilen 17–19 bestimmt wurde, decodiert wird. Der Aufruf von position(0) in Zeile 51 bewegt die aktuelle Position auf den Anfang des Buffers, und sein Inhalt wird in Zeile 52 angezeigt. In den Zeilen 54–55 werden die Buffers geleert, sodass sie wiederverwendet werden können, wenn weitere Daten vom Socket gelesen werden.
17.3 Zusammenfassung Die Netzwerkprogrammierung hat viele Anwendungen, die Sie in Ihren Programmen nutzen können. Sie haben es vielleicht nicht bemerkt, aber die Projekte GetFile und LoadURL waren sehr rudimentäre Webbrowser. Diese Applikationen können den Text einer Webseite in ein Java-Programm laden und anzeigen, aber natürlich ignorieren sie die HTMLTags komplett und zeigen nur den reinen Text an, den der Webserver geliefert hat. Heute haben Sie gelernt, wie Sie URLs, URL-Verbindungen und Eingabestreams gemeinsam verwenden, um Daten aus dem World Wide Web in Ihre Programme zu bekommen.
540
Workshop
Sie haben eine Socket-Applikation erstellt, die die grundlegenden Elemente des FingerProtokolls implementiert – eine Möglichkeit, um Benutzerinformationen im Internet einzuholen. Sie haben auch gelernt, wie Client- und Serverprogramme in Java mithilfe der alten blockierenden Techniken geschrieben wurden, die vor Java 1.4 allein verfügbar waren. Ferner wurden Sie in die neuen nicht blockierenden Techniken eingeführt, die nun mit dem Paket java.nio zur Verfügung stehen. Damit Sie die nicht blockierenden Techniken benutzen können, mussten Sie die fundamentalen Klassen des neuen Netzwerkpakets von Java kennen lernen: Buffers, ZeichenEncoder und -Decoder, Socket-Channels und Selectors. Morgen arbeiten wir mit JavaSound, einem weiteren Paket, das erst seit kurzer Zeit Teil der Sprache Java ist.
17.4 Workshop Fragen und Antworten F
Wie kann ich die Übertragung eines HTML-Formulars in einem Java-Applet nachahmen? A
Derzeit ist das in Applets schwierig. Die beste (und einfachste) Möglichkeit ist die Verwendung der GET-Notation, um den Browser zu veranlassen, den Formularinhalt für Sie zu übermitteln. HTML-Formulare können auf zwei Arten übermittelt werden: durch Verwendung der GET-Anfrage oder mit POST. Wenn Sie GET verwenden, werden die Informationen Ihres Formulars in der URL codiert. Das kann etwa wie folgt aussehen: http://www.blah.com/cgi-bin/myscript?foo=1&bar=2&name=Laura
Da das Formular in der URL codiert ist, können Sie ein Java-Applet schreiben, das ein Formular simuliert, Eingaben vom Benutzer anfordern und dann ein neues URL-Objekt mit den Formulardaten erstellen. Dann geben Sie diese URL mit getAppletContext() und showDocument() an den Browser weiter. Der Browser übermittelt dann die Formularergebnisse für Sie. Bei einfachen Formularen ist dies ausreichend. F
Wie kann ich POST für die Formularübermittelung benutzen? A
Sie müssen simulieren, was ein Browser macht, um Formulare mit POST übersenden zu können. Erzeugen Sie ein URL-Objekt für die Formularübermittlungsadresse wie z. B. http://www.cadenhead.info/cgi-bin/mail2rogers.cgi, und rufen
541
Kommunikation über das Internet
Sie dann die openConnection()-Methode dieses Objekts auf, um ein URLConnection-Objekt zu erzeugen. Rufen Sie die setDoOutput()-Methode auf, um anzuzeigen, dass Sie Daten an diese URL senden wollen. Senden Sie dann eine Serie von Namen-Wert-Paaren mit den Daten an diese Verbindung, wobei das KaufmannsUnd (&) als Trenner fungiert. Das Formular mail2rogers.cgi ist z. B. ein CGI-Programm, das E-Mail an einen der beiden Autoren dieses Buches, Rogers Cadenhead, schickt. Es übermittelt name, subject, email, comments, who, rcode und scode. Wenn Sie einen PrintWriter-Stream namens pw erstellen, der mit diesem CGI-Programm verbunden ist, können Sie mit folgender Anweisung Informationen verschicken: pw.print("[email protected] &" + "comments=Your+POST+example+works.+I+owe+you+$1,000&" + "who=preadm&rcode=2java21&scode=%2Fmailsent.html");
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Welche Netzwerkaktion ist in einem Applet mit Standardsicherheitseinstellungen nicht erlaubt? (a) eine Grafik von dem hostenden Server laden (b) eine Grafik von einem fremden Server laden (c) eine Webseite von einem anderen Server in den Browser laden, der das Applet beinhaltet 2. Welches Programm fragt beim Finger-Protokoll Informationen über einen Benutzer an? (a) der Client (b) der Server (c) Beide können anfragen. 3. Welche Methode benutzt man am besten, um Daten von einer Webseite in eine JavaApplikation zu laden? (a) einen Socket erstellen und dann einen Eingabestream von diesem Socket erzeugen (b) aus diesem Objekt eine URL und eine URLConnection erstellen (c) die Seite mit der Applet-Methode showDocument() laden
542
Workshop
Antworten 1. b. Applets können nur zu dem Computer Verbindungen herstellen, von dem sie selbst stammen. 2. a. Der Client fragt Informationen an und der Server schickt eine Antwort. So funktionieren traditionellerweise Client/Server-Applikationen. Manche Programme können allerdings gleichzeitig als Server und Client fungieren. 3. b. Socket-Verbindungen sind gut für Verbindungen auf einem sehr niedrigen Level, z. B. um ein neues Protokoll zu implementieren. Bei existierenden Protokollen wie HTTP gibt es besser geeignete Klassen – in diesem Fall URL und URLConnection.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Gegeben sei: import java.nio.*; public class ReadTemps { public ReadTemps() { int[] temperatures = { 78, 80, 75, 70, 79, 85, 92, 99, 90, 85, 87 }; IntBuffer tempBuffer = IntBuffer.wrap(temperatures); int[] moreTemperatures = { 65, 44, 71 }; tempBuffer.put(moreTemperatures); System.out.println("First int: " + tempBuffer.get()); } }
Was gibt diese Applikation aus, wenn man sie ausführt? a. First int: 78 b. First int: 71 c. First int: 70 d. keine der drei Antworten Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 17, und klicken Sie auf den Link »Certification Practice«.
543
Kommunikation über das Internet
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Modifizieren Sie das WebMenu-Programm so, dass es 10 URLs erstellt, die mit http:// www. beginnen und mit .com enden, und dazwischen drei zufällig gewählte Buchstaben oder Zahlen haben (also z. B. http://www.mcp.com, http://www.cbs.com oder http:// www.eod.com). Benutzen Sie diese URLs für 10 WebButton-Objekte auf einem Applet.
Erstellen Sie ein Programm, das Finger-Anfragen akzeptiert, nach einem .plan-File sucht, das zum angefragten Benutzernamen passt, und es, falls vorhanden, zurückschickt. Ansonsten soll das Programm "user not found" zurückschicken.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Website zum Buch: http://www.java21pro.com.
544
JavaSound
8 1
JavaSound
Alle Elemente, die ein Programm visuell attraktiv machen können – Benutzerschnittstelle, Grafiken, Bilder und Animation –, verwenden Klassen der Pakete Swing und Abstract Windowing Toolkit. Um Programme akustisch attraktiv zu machen, unterstützt Java 2 sowohl Applet-Methoden, die seit Erscheinen der Sprache verfügbar waren, als auch eine umfangreiche neue Klassenbibliothek namens JavaSound. Heute machen Sie Java-Programme auf zwei verschiedene Arten hörbar: Zuerst benutzen Sie Methoden der Applet-Klasse, der Superklasse aller Java-Applets. Sie können mit diesen Methoden Sounddateien in zahlreichen Formate, u. a. WAV, AU und MIDI, laden und abspielen. Anschließend arbeiten Sie mit JavaSound – zahlreichen Paketen, mit denen Sie Sound abspielen, aufnehmen und bearbeiten können.
18.1 Klänge laden und verwenden Java unterstützt das Abspielen von Sounddateien über die Applet-Klasse. Sie können einen Sound einmal oder wiederholt als Schleife abspielen. Vor Java 2 wurde nur ein Klangformat unterstützt: 8 kHz Mono AU in Mu-Law-Codierung (benannt nach dem griechischen Buchstaben µ). Wenn Sie Sounds nutzen wollten, die in anderen Formaten als WAV vorlagen, mussten Sie diese in das Mu-Law-AU-Format konvertieren, was oft mit einem Qualitätsverlust verbunden war. Java 2 bietet eine weitaus umfassendere Audiounterstützung. Sie können digitalisierte Klänge in folgenden Formaten laden und abspielen: AIFF, AU und WAV. Zusätzlich werden drei Formate auf MIDI-Basis unterstützt: Typ 0 MIDI, Typ 1 MIDI und RMF. Die stark erweiterte Unterstützung von Sound kann mit Audiodaten in 8 und 16 Bit, in Mono oder Stereo, und mit Sampling-Raten von 8 kHz bis 48 kHz umgehen. Die einfachste Möglichkeit, einen Sound zu laden und abzuspielen, bietet die play()Methode der Klasse Applet. Die play()-Methode ist der getImage()-Methode sehr ähnlich. Sie kann ebenfalls in folgenden Formen verwendet werden:
play() mit einem Argument, einem URL-Objekt: lädt und spielt den an dieser URL angegebenen Audioclip ab.
play() mit zwei Argumenten, einer Basis-URL und einer Pfadangabe: lädt und spielt diese Audiodatei ab. Das erste Argument ist wie bei getImage() häufig ein Aufruf von getDocumentBase() oder getCodeBase().
Die folgende Anweisung beispielsweise lädt die Datei zap.au und spielt sie ab. Sie befindet sich im selben Verzeichnis wie das Applet: play(getCodeBase(), "zap.au");
546
Klänge laden und verwenden
Die play()-Methode lädt die Klangdatei und spielt den Sound sobald wie möglich ab, nachdem der Aufruf erfolgt ist. Wenn das Soundfile nicht gefunden wird, erscheint keine Fehlermeldung, der Ton ist dann nur einfach nicht zu hören. Um einen bestimmten Sound wiederholt abzuspielen, ihn zu starten und zu stoppen oder ihn wiederholt als Schleife abzuspielen, müssen Sie ihn in ein AudioClip-Objekt laden, indem Sie die Methode getAudioClip() des Applets benutzen. AudioClip ist Teil des Pakets java.applet, Sie müssen es also importieren, bevor Sie es in einem Programm benutzen können. Die Methode getAudioClip() erhält wie die Methode play() ein oder zwei Argumente. Das erste (oder einzige Argument) ist ein URL-Argument, das die Sounddatei identifiziert, und das zweite ist eine Pfadangabe. Die folgende Anweisung lädt eine Sounddatei in ein clip-Objekt: AudioClip clip = getAudioClip(getCodeBase(), "audio/marimba.wav");
In diesem Beispiel beinhaltet der Dateiname eine Pfadangabe. marimba.wav wird also aus dem Unterordner audio geladen. Die Methode getAudioClip() kann nur innerhalb eines Applets aufgerufen werden. In Java 2 können Applikationen Sounddateien mit newAudioClip() laden, eine Klassenmethode der Klasse java.awt.Applet. Das vorherige Beispiel würde für eine Applikation modifiziert folgendermaßen aussehen: AudioClip clip = Applet.newAudioClip("audio/marimba.wav");
Nach der Erstellung eines AudioClip-Objekts können Sie die Methoden play() (spielt den Sound ab), stop() (hält das Abspielen an) und loop() (spielt wiederholt) aufrufen. Wenn die Methoden getAudioClip() oder newAudioClip() die Sounddatei nicht finden können, die durch das Argument angegeben wird, ist der Wert des AudioClip-Objekts null. Wenn man versucht, ein null-Objekt abzuspielen, erhält man einen Fehler. Testen Sie dies also, bevor Sie ein AudioClip-Objekt benutzen. Man kann mehrere Sounds gleichzeitig abspielen – sie werden während des Abspielens zusammengemixt. Wenn Sie eine Soundschleife in einem Applet benutzen, wird die Klangdatei nicht automatisch angehalten, wenn der laufende Thread des Applets gestoppt wird. Wenn der Surfer zu einer anderen Seite wechselt, wird der Sound weiter abgespielt, was Ihnen sicher keine Freunde in der Internetgemeinde verschaffen wird. Sie können dieses Problem lösen, indem Sie die stop()-Methode auf den Schleifensound dann anwenden, wenn der Thread des Applets gestoppt wird.
547
JavaSound
Listing 18.1 zeigt ein Applet, das zwei Klänge abspielt: einen Schleifensound namens train.wav und einen zweiten Sound namens whistle.wav, der alle fünf Sekunden wiedergegeben wird. Listing 18.1: Der vollständige Quelltext von Looper.java 1: import java.awt.*; 2: import java.applet.AudioClip; 3: 4: public class Looper extends javax.swing.JApplet implements Runnable { 5: AudioClip bgSound; 6: AudioClip beep; 7: Thread runner; 8: 9: public void init() { 10: bgSound = getAudioClip(getCodeBase(),"train.wav"); 11: beep = getAudioClip(getCodeBase(), "whistle.wav"); 12: } 13: 14: public void start() { 15: if (runner == null) { 16: runner = new Thread(this); 17: runner.start(); 18: } 19: } 20: 21: public void stop() { 22: if (runner != null) { 23: if (bgSound != null) 24: bgSound.stop(); 25: runner = null; 26: } 27: } 28: 29: public void run() { 30: if (bgSound != null) 31: bgSound.loop(); 32: Thread thisThread = Thread.currentThread(); 33: while (runner == thisThread) { 34: try { 35: Thread.sleep(9000); 36: if (beep != null) 37: beep.play(); 38: } catch (InterruptedException e) { } 39: }
548
JavaSound
40: } 41: 42: public void paint(Graphics screen) { 43: Graphics2D screen2D = (Graphics2D)screen; 44: screen2D.drawString("Playing Sounds ...", 10, 10); 45: } 46: }
Um das Looper-Applet zu testen, erzeugen Sie eine Webseite mit einem Applet-Fenster beliebiger Größe. Die Audiodateien train.wav und whistle.wav finden Sie auf der Website zum Buch (http://www.java21pro.com). Kopieren Sie sie in den Ordner \J21work auf Ihrem System. Wenn Sie das Applet ausführen, ist ein String die einzige visuelle Ausgabe. Sie sollten jedoch zwei verschiedene Klänge hören, während das Applet läuft. Die init()-Methode in den Zeilen 9-12 lädt die beiden Klangdateien. Hier wurde kein Versuch unternommen sicherzustellen, dass die Dateien auch tatsächlich wie erwartet geladen werden. Wenn sie nicht gefunden werden, erhalten die Instanzvariablen bgsound und beep den Wert null. Ein Test auf null-Werte in diesen Variablen erfolgt an anderer Stelle, bevor die Sounddateien benutzt werden, was in den Zeilen 30 und 36 erfolgt, wo die Methoden loop() und play() auf die AudioClip-Objekte angewendet werden. Die Zeilen 23–24 stoppen die Soundschleife, wenn der Thread gestoppt wird.
18.2 JavaSound Die neue Java-Version beinhaltet mehrere Pakete, die die Fähigkeiten der Sprache beim Abspielen und Erstellen von Sound stark erweitern. JavaSound, das seit Java 2 Version 1.3 offizieller Teil der Java-Klassenbibliothek ist, besteht hauptsächlich aus den folgenden Paketen:
javax.sound.midi – Klassen zum Abspielen, Aufnehmen und Bearbeiten von MIDI-
Sounddateien
javax.sound.sampled – Klassen zum Abspielen, Aufnehmen und Mixen aufgenomme-
ner Audiodateien Die JavaSound-Bibliothek unterstützt alle Audioformate, die in Applets und Applikationen abgespielt werden können: AIFF, AU, MIDI und WAV. Sie unterstützt zudem RMF-Dateien, ein Standard namens Rich Media Format.
549
JavaSound
MIDI-Dateien Das Paket javax.sound.midi bietet umfassende Unterstützung für MIDI-Musikdateien. MIDI steht für Musical Instrument Digital Interface und ist ein Format, um Sound als eine Folge von Noten und Effekten zu speichern, die von den synthetisierten Instrumenten des Computers abgespielt werden sollen. Im Gegensatz zu gesampelten Dateien, in denen echte Klänge für eine Computerreproduktion aufgenommen und digitalisiert wurden (z. B. WAV oder AU) ähnelt MIDI eher Synthesizer-Klängen als einer echten Aufnahme. MIDI-Dateien sind gespeicherte Anweisungen für den MIDI-Sequencer, die ihm vorgeben, wie er Sound produzieren soll, welche synthetisierten Instrumente er benutzen soll usw. Der Klang einer MIDI-Datei hängt von der Qualität und der Anzahl der Instrumente ab, die auf dem Computer oder dem Ausgabeinstrument zur Verfügung stehen. MIDI-Dateien sind viel kleiner als aufgenommener Sound und sind nicht für die Wiedergabe von Stimmen und ähnlichen Klängen geeignet. Trotzdem wird MIDI aufgrund seiner
Kompaktheit und seiner Klangeffekte vielfach benutzt – z. B. für Hintergrundmusik in Computerspielen, Muzak-artige Popsongs oder erste Präsentationen klassischer Kompositionen für Komponisten und Studenten. MIDI-Dateien werden mit einem Sequencer abgespielt. Das ist entweder ein Gerät oder ein
Programm, das eine Datenstruktur abspielt, die Sequenz genannt wird. Eine Sequenz besteht aus einer oder mehreren Spuren, die jeweils eine Folge von zeitcodierten MIDINoten- und Effekt-Anweisungen beinhalten. Diese Anweisungen nennt man MIDI-Events. Jedem Element der MIDI-Präsentation entspricht eine Schnittstelle oder eine Klasse im Paket javax.sound.midi: die Schnittstelle Sequencer und die Klassen Sequence, Track und MidiEvent. Zudem gibt es eine Klasse MidiSystem, durch die man auf die Computerressourcen zum MIDI-Abspielen und -Speichern zugreifen kann.
Eine MIDI-Datei abspielen Um eine MIDI-Datei mit JavaSound abzuspielen, müssen Sie ein Sequencer-Objekt erstellen, das auf den MIDI-Fähigkeiten des jeweiligen Systems basiert. Die Methode getSequencer() der Klasse MidiSystem gibt ein Sequencer-Objekt aus, das den Standard-Sequencer des Systems repräsentiert: Sequencer midi = MidiSystem.getSequencer();
Die Klassenmethode erzeugt eine Ausnahme – ein Objekt, das einen Fehler anzeigt –, wenn der Sequencer aus irgendeinem Grund nicht ansprechbar sein sollte. Es erfolgt eine MidiUnavailableException. Gestern hatten Sie bereits ein wenig mit Ausnahmen zu tun, als Sie Aufrufe von Thread.sleep() in try-catch-Blöcke einschlossen, weil die Methode eine InterruptedException erzeugt, wenn sie unterbrochen wird.
550
JavaSound
Sie können sich um die Ausnahme, die getSequencer() erzeugt, mit folgendem Code kümmern: try { Sequencer.midi = MidiSystem.getSequencer(); // Code, um die MIDI-Sequenz abzuspielen ... } catch (MidiUnavailableException exc) { System.out.println("Error: " + exc.getMessage()); }
Wenn der Sequencer in diesem Beispiel nicht verfügbar ist, wenn getSequencer() aufgerufen wird, fährt das Programm mit der nächsten Anweisung innerhalb des try-Blocks fort. Wenn auf den Sequencer aufgrund einer MidiUnavailableException nicht zugegriffen werden kann, führt das Programm den catch-Block aus und zeigt eine Fehlermeldung an. Etliche Methoden und Konstruktoren, die beim Abspielen einer MIDI-Datei verwendet werden, erzeugen Ausnahmen. Statt sie jeweils einzeln in try-catch-Blöcke einzuschließen, ist es einfacher, alle möglichen Fehler aufzufangen, indem man Exception, die Superklasse aller Ausnahmen, in der catch-Anweisung benutzt. try { Sequencer.midi = MidiSystem.getSequencer(); // Code, um die MIDI-Sequenz abzuspielen ... } catch (Exception exc) { System.out.println("Error: " + exc.getMessage()); }
Dieses Beispiel löst im catch-Block nicht nur MidiUnavailableException-Probleme. Wenn Sie zusätzliche Anweisungen zum Laden und Abspielen der MIDI-Sequenz in den tryBlock setzen, führen alle Ausnahmen, die von diesen Anweisungen hervorgerufen werden, zur Abarbeitung des catch-Blocks. Nachdem Sie ein Sequencer-Objekt erzeugt haben, das MIDI-Dateien abspielen kann, können Sie eine weitere Klassenmethode von MidiSystem aufrufen, um eine MIDI-Sequenz aus einer Datenquelle zu laden:
getSequence(File) – lädt eine Sequenz aus der angegeben Datei.
getSequence(URL) – lädt eine Sequenz von der angegebenen Internetadresse.
getSequence(InputStream) – lädt eine Sequenz aus dem angegebenen Eingabestream, der aus einer Datei, einem Eingabegerät oder einem anderen Programm kommen kann.
Um eine MIDI-Sequenz aus einer Datei zu laden, müssen Sie zuerst ein File-Objekt mithilfe des Dateinamens oder einer Referenz auf den Dateinamen und den Ordner erzeugen. Wenn die Datei im selben Ordner wie Ihr Java-Programm ist, dann können Sie es mit dem Konstruktor File(String) erzeugen. Die folgende Anweisung erzeugt ein File-Objekt für eine MIDI-Datei namens nevermind.mid: File sound = new File ("nevermind.mid");
551
JavaSound
Sie können auch relative Dateipfade verwenden, die Unterordner beinhalten: File sound = new File ("tunes/nevermind.mid");
Der File-Konstruktor erzeugt eine NullPointerException, wenn das Argument des Konstruktors einen null-Wert hat. Nachdem Sie ein File-Objekt für die MIDI-Datei haben, können Sie getSequence(File) aufrufen, um eine Sequenz zu erzeugen: File sound = new File("aboutagirl.mid"); Sequence seq = MidiSystem.getSequence(sound);
Falls alles funktioniert, wird die Klassenmethode getSequence() ein Sequence-Objekt zurückgeben. Wenn jedoch irgendetwas schief geht, können zwei verschiedene Fehler von der Methode erzeugt werden: InvalidMidiDataException, wenn das System nichts mit den MIDI-Daten anfangen kann (oder es sich nicht um MIDI-Daten handelt), und IOException, wenn die Datei-Eingabe unterbrochen wurde oder aus irgendeinem Grund nicht funktionierte. Wenn Ihr Programm bis zu diesem Punkt ohne Fehler lief, haben Sie nun einen MIDISequencer und eine Sequenz zum Abspielen. Jetzt können Sie die Datei abspielen – wenn Sie eine ganze MIDI-Datei abspielen wollen, müssen Sie sich nicht um Spuren oder MIDIEvents kümmern. Das Abspielen einer Sequenz erfolgt in den folgenden Schritten:
Aufruf der open()-Methode des Sequencers, damit sich das Gerät auf das Abspielen vorbereitet
Aufruf der start()-Methode des Sequencers, um mit dem Abspielen der Sequenz zu beginnen
Warten, bis die Sequenz abgespielt ist (oder der Benutzer das Abspielen abgebrochen hat)
Aufruf der close()-Methode des Sequencers, um das Gerät für andere Zwecke freizugeben
Von diesen Methoden erzeugt lediglich open() eine Ausnahme, MidiUnavailableException, falls der Sequencer nicht für das Abspielen vorbereitet werden kann. Der Aufruf von close() hält den Sequencer an, auch dann, wenn er im Augenblick eine oder mehrere Sequenzen abspielt. Sie können die Sequencer-Methode isRunning() (die einen boolean ausgibt) benutzen, um herauszufinden, ob er noch MIDI-Sequenzen abspielt (oder aufnimmt). Das folgende Beispiel wendet diese Methode auf ein Sequencer-Objekt namens playback an, das eine Sequenz geladen hat:
552
JavaSound
playback.open(); playback.start(); while (playback.isRunning()) { try { Thread.sleep(1000); } catch (InterruptedException e) { } playback.close();
}
Die while-Schleife sorgt dafür, dass der Sequencer nicht geschlossen wird, ehe die Sequenz vollständig abgespielt ist. Der Aufruf von Thread.sleep() innerhalb der Schleife verlangsamt sie, sodass isRunning() nur jede Sekunde (alle tausend Millisekunden) einmal überprüft wird – ansonsten würde das Programm viele Ressourcen damit verschwenden, um jede Sekunde unzählige Male isRunning() aufzurufen. Die Applikation PlayMidi in Listing 18.2 spielt eine MIDI-Sequenz aus einer Datei Ihres Systems ab. Die Applikation erzeugt einen Frame, in dem sich eine Benutzerschnittstellenkomponente namens MidiPanel befindet. Dieses Panel läuft in einem eigenen Thread und spielt die Datei ab. Listing 18.2: Der vollständige Quelltext von PlayMidi.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
import import import import
javax.swing.*; javax.sound.midi.*; java.awt.GridLayout; java.io.File;
public class PlayMidi extends JFrame { PlayMidi(String song) { super("Play MIDI Files"); setSize(180, 100); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); MidiPanel midi = new MidiPanel(song); JPanel pane = new JPanel(); pane.add(midi); setContentPane(pane); show(); } public static void main(String[] arguments) { if (arguments.length != 1) { System.out.println("Usage: java PlayMidi filename"); } else { PlayMidi pm = new PlayMidi(arguments[0]); }
553
JavaSound
25: } 26: } 27: 28: class MidiPanel extends JPanel implements Runnable { 29: Thread runner; 30: JProgressBar progress = new JProgressBar(); 31: Sequence currentSound; 32: Sequencer player; 33: String songFile; 34: 35: MidiPanel(String song) { 36: super(); 37: songFile = song; 38: JLabel label = new JLabel("Playing file ..."); 39: setLayout(new GridLayout(2, 1)); 40: add(label); 41: add(progress); 42: if (runner == null) { 43: runner = new Thread(this); 44: runner.start(); 45: } 46: } 47: 48: public void run() { 49: try { 50: File file = new File(songFile); 51: currentSound = MidiSystem.getSequence(file); 52: player = MidiSystem.getSequencer(); 53: player.open(); 54: player.setSequence(currentSound); 55: progress.setMinimum(0); 56: progress.setMaximum((int)player.getMicrosecondLength()); 57: player.start(); 58: while (player.isRunning()) { 59: progress.setValue((int)player.getMicrosecondPosition()); 60: try { 61: Thread.sleep(1000); 62: } catch (InterruptedException e) { } 63: } 64: progress.setValue((int)player.getMicrosecondPosition()); 65: player.close(); 66: } catch (Exception ex) { 67: System.out.println(ex.toString()); 68: } 69: } 70: }
554
JavaSound
Sie müssen den Namen einer MIDI-Datei als Kommandozeilenargument angeben, wenn Sie diese Applikation ausführen. Wenn Sie kein MIDI-File haben, finden Sie eines auf der Webseite zum Buch: http://www.java21pro.com. Öffnen Sie dort die Seite zum Tag 18. Im Internet finden Sie eine Vielzahl großer MIDI-Sammlungen. Am besten rufen Sie die Suchmaschine Google auf (http://www.google.de) und suchen den Begriff »MIDI files«. Google listet Websites in der Reihenfolge ihrer Beliebtheit auf, sodass Sie im Handumdrehen umfangreiche MIDI-Archive finden sollten. Das folgende Kommando führt die Applikation mit einer MIDI-Datei namens betsy.mid aus. Das ist ein Folksong aus dem 19. Jahrhundert, »Sweet Betsy from Pike«, den Sie auch auf der Webseite zum Buch finden. java PlayMidi betsy.mid
Abbildung 18.1 zeigt die Applikation während des Abspielens.
Abbildung 18.1: Die PlayMidi-Applikation spielt eine MIDI-Datei ab.
Die Applikation zeigt eine Fortschrittsanzeige an, auf der man ablesen kann, wie viel von der Sequenz bereits abgespielt wurde. Dies geschieht durch die Benutzerschnittstellenkomponente JProgressBar und zwei Sequencer-Methoden:
getMicrosecondLength() – die Gesamtlänge der derzeit geladenen Sequenz, als long-
Wert in Millisekunden
getMicrosecondPosition() – die Mikrosekunde, die die derzeitige Position in der Sequenz angibt, gleichfalls ein long-Wert
Eine Mikrosekunde entspricht einer millionstel Sekunde. Mit diesen Methoden lässt sich der Fortschritt des MIDI-Abspielens also bemerkenswert genau messen. Die Fortschrittsanzeige wird als Instanzvariable von MidiPanel in Zeile 12 erzeugt. Sie können zwar eine Fortschrittsanzeige mit einem Minimum und einem Maximum erzeugen, es gibt jedoch keine Möglichkeit, die Länge einer Sequenz vor ihrem Laden zu erfahren. Das Minimum der Fortschrittsanzeige wird in Zeile 55 auf 0 gesetzt und das Maximum in Zeile 56 auf die Mikrosekundenlänge der Sequenz. Die Argumente setMinimum() und setMaximum() der Fortschrittsanzeige benötigen Integer als Argumente. Daher konvertiert die Applikation die Mikrosekundenwerte von long in int. Da dabei Informationen verloren gehen, funktioniert die Fortschrittsanzeige für Dateien mit mehr als 2,14 Milliarden Mikrosekunden nicht richtig. Das entspricht 35,6 Minuten.
555
JavaSound
Die Methode run() der Zeilen 48–69 aus Listing 18.2 lädt den System-Sequencer und eine MIDI-Datei in eine Sequenz und spielt die Sequenz ab. Die while-Schleife in den Zeilen 58–63 benutzt die Methode isRunning() des Sequencers, um zu warten, bis die Datei abgespielt wurde, bevor etwas anderes geschieht. Die Schleife aktualisiert auch die Fortschrittsanzeige, indem sie deren setValue()-Methode mit der aktuellen Mikrosekundenposition der Sequenz aufruft. Wenn die Datei abgespielt und die while-Schleife beendet ist, wird die Mikrosekundenposition der Sequenz mit 0 angegeben, was in Zeile 64 genutzt wird, um die Fortschrittsanzeige auf ihren Minimalwert zurückzusetzen.
18.3 Sounddateien bearbeiten Bisher haben Sie JavaSound dazu benutzt, um Funktionalitäten zu verwenden, die bereits in den Audiomethoden der Applet-Klasse zur Verfügung standen, die neben den anderen unterstützten Formaten auch MIDI-Dateien abspielen kann. Die Stärke der Alternative JavaSound wird dann offensichtlich, wenn Sie Ihre Sounddateien bearbeiten wollen. Mithilfe der JavaSound-Pakete können Sie zahlreiche Aspekte der Präsentation und Aufnahme von Audiodateien verändern. Eine Eigenschaft, die man während des Abspielens einer MIDI-Datei verändern kann, ist die Geschwindigkeit, in der sie abgespielt wird. Um dies mit einem existenten SequencerObjekt zu tun, müssen Sie seine Methode setTempoFactor(float) aufrufen. Das Tempo wird als Float-Wert ab 0,0 angegeben. Jede MIDI-Sequenz hat ihr eigenes, festgelegtes Tempo, das durch den Wert 1,0 repräsentiert wird. Ein Tempo von 0,5 ist halb so schnell, 2,0 doppelt so schnell usw. Um das aktuelle Tempo auszulesen, rufen Sie getTempoFactor() auf, das einen float-Wert zurückgibt. Unser nächstes Projekt namens MidiApplet benutzt dieselbe Technik wie die PlayMidiApplikation, um eine MIDI-Datei zu laden und abzuspielen – ein Panel wird angezeigt, das die MIDI-Datei in einem eigenen Thread abspielt. Die MIDI-Datei wird mithilfe eines FileObjekts geladen und mit den Sequencer-Methoden open(), start() und close() abgespielt. Neu an diesem Projekt ist, dass die MIDI-Datei immer wieder abgespielt werden kann. Da dies ein Applet und keine Applikation ist, wird die abzuspielende MIDI-Datei als Parameter angegeben. Listing 18.3 beinhaltet ein mögliches HTML-Dokument, mit dem das Applet geladen werden könnte.
556
Sounddateien bearbeiten
Listing 18.3: Der Quelltext von MidiApplet.html 1: 2: <param name="file" value="camptown.mid"> 3:
Die MIDI-Datei für dieses Beispiel, eine MIDI-Version von Camptown Races, ist auf der Website zum Buch unter http://www.java21pro.com auf der Seite zu Tag 18 erhältlich. Natürlich können Sie auch jedes andere MIDI-File benutzen. Das Projekt MidiApplet hat drei Benutzerschnittstellenkomponenten, mit denen Sie kontrollieren können, wie die Datei abgespielt wird: Play- und Stop-Buttons sowie eine Dropdown-Liste, auf der man das Tempo auswählen kann. Abbildung 18.2 zeigt, wie das Programm im appletviewer aussieht.
Abbildung 18.2: Das Programm MidiApplet spielt »Camptown Races« ab.
Da Applets einen Sound auch dann weiter im Browser abspielen, wenn der Besucher auf eine andere Seite surft, muss man manuell eine Möglichkeit schaffen, um das Abspielen zu beenden. Wenn Sie Audio in einem eigenen Thread ausführen, können Sie die gleichen Techniken zum Stoppen des Threads benutzen, die Sie bei den Animationen kennen gelernt haben – führen Sie den Thread in einem Thread-Objekt aus, lassen Sie eine Schleife laufen, solange dieses Objekt und Thread.currentThread() dasselbe Objekt repräsentieren, und setzen Sie den runner auf null, wenn Sie den Thread stoppen wollen. Listing 18.4 enthält das Projekt MidiApplet. Die Länge des Programms erklärt sich in erster Linie aus der grafischen Benutzerschnittstelle und den Event-Handler-Methoden, um Eingaben des Benutzers empfangen zu können. Die JavaSound-Aspekte des Programms werden besprochen, nachdem Sie das Applet erstellt haben. Listing 18.4: Der vollständige Quelltext von MidiApplet.java 1: 2: 3: 4: 5: 6:
import import import import import
javax.swing.*; java.awt.event.*; javax.sound.midi.*; java.awt.GridLayout; java.io.File;
557
JavaSound
7: public class MidiApplet extends javax.swing.JApplet { 8: public void init() { 9: JPanel pane = new JPanel(); 10: MidiPlayer midi = new MidiPlayer(getParameter("file")); 11: pane.add(midi); 12: setContentPane(pane); 13: } 14: } 15: 16: class MidiPlayer extends JPanel implements Runnable, ActionListener { 17: 18: Thread runner; 19: JButton play = new JButton("Play"); 20: JButton stop = new JButton("Stop"); 21: JLabel message = new JLabel(); 22: JComboBox tempoBox = new JComboBox(); 23: float tempo = 1.0F; 24: Sequence currentSound; 25: Sequencer player; 26: String songFile; 27: 28: MidiPlayer(String song) { 29: super(); 30: songFile = song; 31: play.addActionListener(this); 32: stop.setEnabled(false); 33: stop.addActionListener(this); 34: for (float i = 0.25F; i < 7F; i += 0.25F) 35: tempoBox.addItem("" + i); 36: tempoBox.setSelectedItem("1.0"); 37: tempoBox.setEnabled(false); 38: tempoBox.addActionListener(this); 39: setLayout(new GridLayout(2, 1)); 40: add(message); 41: JPanel buttons = new JPanel(); 42: JLabel tempoLabel = new JLabel("Tempo: "); 43: buttons.add(play); 44: buttons.add(stop); 45: buttons.add(tempoLabel); 46: buttons.add(tempoBox); 47: add(buttons); 48: if (songFile == null) { 49: play.setEnabled(false); 50: } 51: } 52: 53: public void actionPerformed(ActionEvent evt) {
558
Sounddateien bearbeiten
54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99:
if (evt.getSource() instanceof JButton) { if (evt.getSource() == play) play(); else stop(); } else { String item = (String)tempoBox.getSelectedItem(); try { tempo = Float.parseFloat(item); player.setTempoFactor(tempo); message.setText("Playing " + songFile + " at " + tempo + " tempo"); } catch (NumberFormatException ex) { message.setText(ex.toString()); } } } void play() { if (runner == null) { runner = new Thread(this); runner.start(); play.setEnabled(false); stop.setEnabled(true); tempoBox.setEnabled(true); } } void stop() { if (runner != null) { runner = null; stop.setEnabled(false); play.setEnabled(true); tempoBox.setEnabled(false); } } public void run() { try { File song = new File(songFile); currentSound = MidiSystem.getSequence(song); player = MidiSystem.getSequencer(); } catch (Exception ex) { message.setText(ex.toString()); } Thread thisThread = Thread.currentThread();
559
JavaSound
100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: }
while (runner == thisThread) { try { player.open(); player.setSequence(currentSound); player.setTempoFactor(tempo); player.start(); message.setText("Playing " + songFile + " at " + tempo + " tempo"); while (player.isRunning() && runner != null) { try { Thread.sleep(1000); } catch (InterruptedException e) { } } message.setText(""); player.close(); } catch (Exception ex) { message.setText(ex.toString()); break; } } }
Führen Sie MidiApplet aus, indem Sie es in einem HTML-Dokument mithilfe des Appletviewer oder einem Webbrowser mit dem Java-Plug-In laden. Das Tempo der MIDI-Datei wird über eine Dropdown-Listenkomponente namens tempoBox gesteuert. Diese Komponente wird mit einer Reihe von Fließkommawerten zwischen 0.25 und 6.75 in den Zeilen 34–35 erzeugt. Die Methode addItem(Object) der Liste funktioniert nicht mit float-Werten. Deswegen werden letztere in Zeile 35 mit einem leeren String – Anführungszeichen ohne Text dazwischen – kombiniert. Dadurch wird das kombinierte Argument als String-Objekt an addItem()geschickt. Obwohl das Tempo mithilfe von tempoBox gesteuert werden kann, wird es in seiner eigenen Instanzvariable tempo gespeichert. Diese Variable wird in Zeile 23 mit einem Wert von 1.0, der Standardabspielgeschwindigkeit des Sequencers, initialisiert. Wenn die Dropdown-Liste, aus der der Besucher einen Wert auswählt, einen ActionListener assoziiert hat, dann wird die actionPerformed-Methode des Listeners aufgerufen. Die Methode actionPerformed() in den Zeilen 53–70 erledigt alle drei Formen von möglichen Benutzereingaben:
Ein Klick auf den »Play«-Button führt zum Aufruf der play()-Methode.
Ein Klick auf den »Stop«-Button führt zum Aufruf der stop()-Methode.
Wenn ein neuer Wert aus der Dropdown-Liste ausgewählt wird, wird dieser Wert zum neuen Tempo.
560
Sounddateien bearbeiten
Da alle Einträge in der tempoBox als Strings gespeichert werden, müssen Sie sie in Fließkommawerte konvertieren, bevor Sie mit ihnen das Tempo festlegen können. Das lässt sich mit der Klassenmethode Float.parseFloat() erledigen, die ganz ähnlich wie die Methode Integer.parseInt() funktioniert, die wir in den letzten zwei Wochen mehrfach bei der Arbeit mit Integern verwendet haben. Wie die andere Parse-Methode führt parseFloat() zu einem NumberFormatException-Fehler, wenn der String nicht in einen Fließkommawert umgewandelt werden kann. Bei der Erzeugung von tempoBox wurden nur solche Einträge vorgenommen, die sich problemlos in Fließkommawerte konvertieren ließen. Es ist also ausgeschlossen, dass eine NumberFormatException daraus resultieren kann, wenn mit dieser Komponente das Tempo festgelegt wird. Trotzdem erzwingt Java, dass man sich der Ausnahme in einem try-catch-Block annimmt. Zeile 63 ruft die setTempoFactor()-Methode des Sequencers mit dem vom Besucher ausgewählten Tempo auf. Die Umsetzung erfolgt unmittelbar, sodass Sie mit Modifikationen an der Wiedergabegeschwindigkeit mitunter psychedelische Effekte erzielen können. Nachdem in der run()-Methode Sequencer und Sequenz erzeugt worden sind, lässt die while-Schleife in den Zeilen 100–119 den Song spielen, bis der Thread-Objekt-runner auf null gesetzt wird. Eine zweite while-Schleife, die in diese verschachtelt ist, stellt sicher, dass der Sequencer nicht geschlossen wird, während der Song abgespielt wird. Diese Schleife in den Zeilen 108–112 unterscheidet sich ein wenig von derjenigen in der Applikation PlayMidi. Anstatt die Schleife durchlaufen zu lassen, solange player.isRunning() den Wert true ausgibt, müssen dieses Mal zwei Bedingungen erfüllt werden: while (player.isRunning() && runner != null) { // Anweisungen in der Schleife }
Der Und-Operator && lässt die Schleife nur dann weiterlaufen, wenn beide Ausdrücke true sind. Würden Sie hier nicht den Wert von runner testen, würde der Thread die MIDI-Datei bis zum Ende des Songs weiterspielen und nicht stoppen, wenn runner auf null gesetzt wird, was das Ende des Threads anzeigt. Das Programm MidiApplet beendet den Thread nicht, wenn der Besucher auf eine fremde Webseite geht. Da MidiPanel() eine stop()-Methode hat, die den Thread stoppt, können Sie die MIDIWiedergabe anhalten, sobald die Seite nicht mehr angesehen wird. Dafür sind zwei Schritte notwendig:
561
JavaSound
1. Erzeugen Sie eine Instanzvariable in MidiApplet für die Benutzerschnittstellenkomponente MidiPanel. 2. Überschreiben Sie die stop()-Methode des Applets, und benutzen Sie sie, um die stop()-Methode des Panels aufzurufen.
18.4 Zusammenfassung Zu den Stärken der Java-Klassenbibliothek gehört, dass komplexe Programmieraufgaben wie die Programmierung einer Benutzerschnittstelle und die Soundwiedergabe in einfach zu erstellende und gut benutzbare Klassen eingebaut sind. Sie können eine MIDI-Datei abspielen, die in Echtzeit verändert werden kann. Dazu brauchen Sie nur ein paar Objekte und Klassenmethoden, während im Hintergrund Aktionen ausgeführt werden, die nicht einfach zu programmieren sind. Heute haben Sie Sounds abgespielt, indem Sie sowohl sehr einfache als auch eher komplexe Techniken angewendet haben. Wenn Sie nur ein Audio-File abspielen wollen, ist es meist völlig ausreichend, mit den Methoden getAudioClip() und newAudioClip() der Klasse Applet zu arbeiten. Wenn Sie an dem Sound komplexere Veränderungen vornehmen wollen, etwa Geschwindigkeitsänderungen oder andere dynamische Veränderungen, können JavaSound-Pakete wie javax.sound.midi genutzt werden.
18.5 Workshop Fragen und Antworten F
In diesem Kapitel wurde die Methode getSequence(InputStream) erwähnt. Was sind überhaupt Eingabestreams, und wie werden sie mit Sounddateien benutzt? A
F
Was ist sonst noch mit JavaSound möglich? A
562
Eingabestreams sind Objekte, die Daten einlesen, während diese von einer anderen Quelle versandt werden. Die Quelle kann alles sein, was Daten produziert: Dateien, serielle Ports, Server oder auch Objekte desselben Programms. An Tag 15 sind wir ausführlich auf Streams eingegangen. JavaSound ist eine Gruppe von Java-Paketen, die in ihrer Komplexität Swing nahe kommt. Viele der Klassen benötigen komplexere Techniken zur Behandlung von Streams und Ausnahmen. Mehr über JavaSound erfahren Sie bei Sun
Workshop
unter http://java.sun.com/products/java-media/sound. Sun bietet eine JavaApplikation namens Java Sound Demo, die einige der eindruckvollsten Features von JavaSound demonstriert: Sound abspielen, aufnehmen, MIDI-Synthese und programmierbare MIDI-Instrumente.
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Mit welcher Applet-Klassenmethode erzeugt man ein AudioClip-Projekt in einer Applikation? (a) newAudioClip() (b) getAudioClip() (c) getSequence() 2. Welche Klasse repräsentiert die MIDI-Ressourcen, die auf einem bestimmten Computer vorhanden sind? (a) Sequencer (b) MIDISystem (c) MIDIEvent 3. Wie viele Mikrosekunden dauert es, um ein 3-Minuten-Ei zu kochen? (a) 180.000 (b) 180.000.000 (c) 180.000.000.000
Antworten 1. a. Es ist nicht sehr logisch, dass diese Methode Teil der Applet-Klasse ist, aber diese Merkwürdigkeit aus Java 1.0 ist auch in Java 2.0 noch enthalten. 2. b. Die Klasse MIDISystem wird benutzt, um Objekte zu erzeugen, die Sequencer, Synthesizer und andere Geräte zu repräsentieren, die für MIDI-Audio zuständig sind. 3. b. Eine Million Mikrosekunden entsprechen einer Sekunde, also entsprechen 180 Millionen Mikrosekunden 180 Sekunden.
563
JavaSound
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Gegeben sei: public class Operation { public static void main(String[] arguments) { int x = 1; int y = 3; if ((x != 1) && (y++ == 3)) y = y + 2; } }
Wie lautet der endgültige Wert von y? a. 3 b. 4 c. 5 d. 6 Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 18, und klicken Sie auf den Link »Certification Practice«.
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Erstellen Sie eine Applikation, die mit newAudioClip() ein Soundfile abspielt.
Verändern Sie das Projekt MidiApplet, sodass man mehr als eine MIDI-Datei auf einer Webseite als Parameter angeben kann. Die Dateien werden dann der Reihe nach abgespielt.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Website zum Buch: http://www.java21pro.com.
564
JavaBeans
9 1
JavaBeans
Wie wir bereits gesehen haben, ist einer der Hauptvorteile objektorientierter Programmierung, dass ein Objekt in verschiedenen Programmen benutzt werden kann. Wenn Sie ein Rechtschreibprüfungsobjekt für eine Textverarbeitung entwickelt haben, dann sollte dieses Objekt auch in einem E-Mail-Programm funktionieren. Sun hat dieses Prinzip mit der Einführung von JavaBeans ausgeweitet. Eine JavaBean, auch Bean genannt, ist ein Software-Objekt, das mit anderen Objekten gemäß genau festgelegten Richtlinien interagiert – der JavaBeans-Spezifikation. Wenn Sie wissen, wie man gemäß dieser Richtlinien mit einer JavaBean arbeitet, können Sie mit allen arbeiten. Viele Programmiertools berücksichtigen Beans explizit. Diese Entwicklungsumgebungen, z. B. auch das kostenlose JavaBeans Development Kit von Sun, ermöglichen es, schnell Java-Programme zu entwickeln, indem man existierende Beans verwendet und zwischen ihnen Beziehungen herstellt. Heute behandeln wir folgende Themen:
Erzeugung wieder benutzbarer Software-Objekte in Java
die Beziehung zwischen JavaBeans und der Java-Klassenbibliothek
die JavaBeans-API
JavaBeans-Entwicklungswerkzeuge
mit JavaBeans arbeiten
ein Applet mit JavaBeans erstellen
19.1 Wieder verwendbare Softwarekomponenten Ein Trend mit zunehmender Bedeutung bei der Software-Entwicklung ist der Einsatz wieder verwendbarer Komponenten – Elemente eines Programms, die mit mehr als einem Softwarepaket benutzt werden können. Eine Softwarekomponente ist ein Stück Software, das in einer diskreten, leicht wieder verwendbaren Struktur isoliert ist.
Wenn Sie die Teile eines Programms so entwickeln, dass sie vollkommen selbstständig sind, sollte es möglich sein, diese Komponenten mit weitaus größerer Entwicklungseffizienz zu Programmen zusammenzubauen. Dieser Gedanke der Wiederverwendung sorgfältig isolierter Software wurde zu einem gewissen Maß dem Montagebandprinzip entliehen. Auf Software angewandt bedeutet das, einmalig zu Beginn kleine, wieder verwendbare
566
Wieder verwendbare Softwarekomponenten
Komponenten zu bauen, um sie dann in höchstmöglichem Maß wieder zu verwenden und damit den gesamten Entwicklungsprozess zu rationalisieren. Das vielleicht größte Problem, dem sich Komponentensoftware stellen musste, ist die Vielzahl verschiedener Mikroprozessoren und Betriebssysteme. Es gab eine Reihe engagierter Versuche für Komponentensoftware, die aber immer auf ein spezifisches Betriebssystem limitiert waren. Die VBX- und OCX-Komponentenarchitekturen von Microsoft verzeichneten große Erfolge in der Welt der Intel-PCs, konnten aber wenig zum Schließen der Lücke zwischen PCs und anderen Betriebssystemtypen beitragen. Tatsächlich zielt die auf OCX-Technologie basierende ActiveX-Technologie von Microsoft darauf ab, eine universelle Komponententechnologie bereitzustellen, die Kompatibilität für einen breiten Bereich von Plattformen bietet. Da aber ActiveX von 32-Bit-Windows-Code abhängig ist, eignet es sich nicht für die Entwicklung auf anderen Plattformen. Einige verfügbare Komponententechnologien leiden zudem darunter, in einer spezifischen Programmiersprache oder für eine spezielle Umgebung entwickelt worden zu sein. So wie die Plattformabhängigkeit Komponenten zur Laufzeit verkrüppelt, führt die Limitierung der Komponentenentwicklung auf eine spezifische Programmiersprache oder Entwicklungsumgebung zur Verkrüpplung der Komponenten im Entwicklungsbereich. Software-Entwickler würden lieber selbst entscheiden, welche Sprache für eine bestimmte Aufgabe am besten geeignet ist. Genauso wollen sie die Entwicklungsumgebung benutzen, die am besten zur gestellten Aufgabe passt, anstatt gezwungen zu sein, aufgrund der Komponententechnologie eine bestimmte Umgebung verwenden zu müssen. Demnach muss sich jede realistische, auf lange Sicht ausgerichtete Komponententechnologie dem Problem der Plattform- und Sprachenabhängigkeit stellen. Java nahm eine wichtige Rolle dabei ein, plattformübergreifendes Entwickeln in die Praxis umzusetzen. Es unterstützt Softwarekomponenten-Entwicklung durch JavaBeans. JavaBeans ist eine architektur- und plattformunabhängige Gruppe von Klassen zur Erstellung und Verwendung von Java-Softwarekomponenten. Es benutzt die portable Java-Plattform, um eine Softwarekomponenten-Lösung zu bieten.
Das Ziel von JavaBeans JavaBeans wurde kompakt entworfen, weil Komponenten häufig in verteilten Umgebungen verwendet werden, in denen ganze Komponenten über eine Internetverbindung mit geringer Bandbreite übertragen werden. Der zweite Teil dieses Designziels bezieht sich auf die Leichtigkeit, mit der diese Komponenten erstellt und verwendet werden. Leicht verwendbare Komponenten kann man sich verhältnismäßig mühelos vorstellen, aber die Erzeugung einer Komponentenarchitektur, die das Erstellen von Komponenten leicht gestaltet, ist eine völlig andere Sache.
567
JavaBeans
Das zweite Hauptziel von JavaBeans ist vollständige Portabilität. Als Folge davon brauchen sich Entwickler keine Gedanken über die Einbeziehung plattformspezifischer Bibliotheken in Ihre Java-Applets zu machen. Die bestehende Java-Architektur bietet bereits eine große Anzahl von Vorteilen, die leicht auf Komponenten anwendbar ist. Eine der wesentlichen (aber selten erwähnten) Eigenschaften von Java ist sein eingebauter Klassenerkennungsmechanismus, der dynamische Interaktionen zwischen Objekten ermöglicht. Daraus entsteht ein System, in dem sich Objekte unabhängig von ihrem Ursprung oder ihrer Entwicklungsgeschichte miteinander integrieren lassen. Dieser Mechanismus zur Klassenauffindung stellt nicht nur eine praktische Eigenschaft von Java dar, sondern ist für jede Komponentenarchitektur eine notwendige Voraussetzung. Darüber hinaus erhält JavaBeans aus der bestehenden Java-Funktionalität auch die Persistenz, d. h. die Fähigkeit eines Objektes, seinen internen Status zu speichern und wieder zu laden. Diese Persistenz erfolgt in JavaBeans automatisch durch die Verwendung des in Java bereits vorhandenen Serialisationsmechanismus. Serialisation ist der Prozess, bei dem Informationen über ein Standardprotokoll gespeichert bzw. geladen werden. Falls erforderlich, können Entwickler alternativ speziell angepasste Persistenz-Lösungen erstellen. Obwohl kein Schlüsselelement der JavaBeans-Architektur, ist die Unterstützung für verteiltes Rechnen bei JavaBeans dennoch integriert. Entwickler von JavaBeans-Komponenten haben die Möglichkeit, das für ihre Bedürfnisse am besten geeignete Verfahren auszuwählen. Mit der Technologie der Remote Method Invocation (RMI) bietet Sun eine verteilte Rechenlösung, die aber den JavaBeans-Entwicklern keinesfalls die Hände bindet. Andere Lösungsmöglichkeiten sind z. B. CORBA (Common Object Request Broker Architecture) und Microsofts DCOM (Distributed Component Object Model). Verteiltes Rechnen wurde säuberlich von JavaBeans abgetrennt, um die Struktur straff zu halten und gleichzeitig Entwicklern, die die entsprechende Unterstützung benötigen, eine breite Auswahl an Optionen zu bieten. Das letzte Designziel von JavaBeans behandelt Probleme der Entwurfszeit und die Art und Weise, wie Entwickler Applikationen unter Verwendung von JavaBeans-Komponenten erstellen. Die Architektur von JavaBeans bietet Unterstützung für die Spezifizierung von Entwurfszeit-Eigenschaften und für eine erleichterte visuelle Bearbeitung von JavaBeans-Komponenten. Das hat zur Folge, dass Entwickler visuelle Werkzeuge zum nahtlosen Zusammenfügen und Modifizieren von JavaBeans-Komponenten einsetzen können, ganz ähnlich der Weise, in der visuelle PC-Werkzeuge mit Komponenten wie VBX- oder OCXControls arbeiten. Komponentenentwickler spezifizieren so die Art und Weise, wie die Komponenten in einer Entwicklungsumgebung zu verwenden und zu manipulieren sind.
568
Wieder verwendbare Softwarekomponenten
Wie JavaBeans in Beziehung zu Java steht Javas Objektorientiertheit gibt Objekten die Möglichkeit, in Verbindung miteinander zu arbeiten. Es gibt jedoch einige Regeln bzw. Standards zu beachten, die die Interaktion von Objekten regeln. Diese Regeln sind für eine robuste Softwarekomponenten-Lösung notwendig und werden durch JavaBeans festgelegt. JavaBeans spezifiziert eine umfangreiche Reihe von Mechanismen für die Interaktion zwischen Objekten, gemeinsam mit allgemeinen, von den meisten Objekten zu unterstützende Aktionen wie Persistenz und Ereignisbehandlung. Es liefert auch den Rahmen, in dem diese Komponentenkommunikation stattfinden kann. Noch wichtiger ist jedoch die Tatsache, dass JavaBeans-Komponenten durch ein Standard-Set genau festgelegter Eigenschaften angepasst werden können. JavaBeans-Komponenten sind nicht auf Benutzerschnittstellenobjekte wie Buttons beschränkt. Sie können genauso einfach nicht visuelle JavaBeans-Komponenten entwickeln, die zusammen mit anderen Komponenten Hintergrundfunktionen ausüben. Auf diese Weise vereinigt JavaBeans die Leistungsstärke visueller Java-Applets mit nicht visuellen Java-Applikationen in einem durchgängigen Komponentengerüst. Eine nicht visuelle Komponente ist jede Komponente, die keine sichtbare Ausgabe hat. Wenn man bei Komponenten an Swing-Objekte wie Buttons und Menüs denkt, mag das ein wenig befremdlich erscheinen. Vergessen Sie nicht, dass eine Komponente einfach ein dicht gepacktes Programm ist und nicht unbedingt visuell sein muss. Ein gutes Beispiel für eine nicht visuelle Komponente ist eine Timer-Komponente, die in bestimmten Abständen Ereignisse auslöst. Timer-Komponenten sind in anderen Komponenten-Entwicklungsumgebungen wie Microsoft Visual Basic sehr populär. Dank visueller Werkzeuge können Sie eine Vielzahl von JavaBeans-Komponenten gemeinsam benutzen, ohne dass Sie Code schreiben müssten. JavaBeans-Komponenten legen ihre eigenen Schnittstellen visuell dar und stellen damit eine Möglichkeit zur Bearbeitung ihrer Eigenschaften ohne Programmierung bereit. Des Weiteren können Sie mit der Verwendung eines visuellen Editors eine JavaBeans-Komponente in eine Anwendung einfügen, ohne dass Sie eine einzige Zeile Code schreiben müssen. Das ist eine vollständig neue Dimension der Flexibilität und Wiederverwendung, die mit Java allein bisher nicht möglich war.
Die JavaBeans-API JavaBeans ist eine Programmierschnittstelle, was bedeutet, dass all seine Eigenschaften als Erweiterungen der Standard-Klassenbibliothek von Java realisiert werden. Die gesamte von JavaBeans zur Verfügung gestellte Funktionalität ist im JavaBeans-API implementiert. Das JavaBeans-API selbst ist eine Reihe von kleineren APIs, die spezifischen Funktionen (Ser-
569
JavaBeans
vices) gewidmet sind. Die nachfolgende Liste zeigt wesentliche Komponentenservices im JavaBeans-API, die für alle Features erforderlich sind, die wir uns heute ansehen:
Merging der grafischen Benutzeroberfläche
Persistenz
Ereignisbehandlung
Introspektion
Application Builder Support
Durch entsprechendes Verständnis dieser Services und wie sie arbeiten, bekommen Sie einen besseren Einblick in die Technologie von JavaBeans. Jeder dieser Services wird durch kleinere APIs implementiert, die im JavaBeans-API enthalten sind. Die APIs für das Benutzerschnittstellen-Merging ermöglichen einer Komponente, ihre Elemente mit einem Container zu vereinigen. Die meisten Container haben Menüs und Werkzeugleisten, die zur Anzeige der von dieser Komponente bereitgestellten Eigenschaften dienen. Die APIs für das Benutzerschnittstellen-Merging ermöglichen der Komponente, dem Menü und der Werkzeugleiste des Container-Dokuments Eigenschaften hinzuzufügen. Diese APIs definieren auch den Mechanismus, der ein Schnittstellen-Layout zwischen den Komponenten und ihren Containern erleichtert. Die Persistenz-APIs spezifizieren den Mechanismus, mit dem Komponenten innerhalb des Kontexts eines Container-Dokuments gespeichert und geladen werden können. Komponenten erben per Voreinstellung den automatischen, von Java bereitgestellten Serialisationsmechanismus. Entwickler haben die Möglichkeit, Persistenz-Lösungen gemäß den speziellen Erfordernissen ihrer Komponenten zu entwerfen. Die APIs zur Ereignisbehandlung spezifizieren eine ereignisgesteuerte Architektur, die die Wechselwirkung der Komponenten miteinander definiert. Java beinhaltet bereits ein leistungsstarkes Ereignisbehandlungsmodell, das als Grundlage für die Komponenten-APIs zur Ereignisbehandlung dient. Diese APIs sind entscheidend, wenn es darum geht, den Komponenten die Freiheit zu geben, in konsistenter Weise miteinander zu interagieren. Die Introspektions-APIs definieren Techniken, die Komponenten dazu veranlassen, ihre interne Struktur zur Entwurfszeit direkt zur Verfügung zu stellen. Diese APIs ermöglichen Entwicklungstools, eine Komponente nach ihrem internen Status zu fragen, einschließlich der Schnittstellen, Methoden und Member-Variablen, aus denen die Komponente besteht. Die APIs sind, basierend auf der Ebene, auf der sie benutzt werden, in zwei getrennte Abschnitte unterteilt. Die Introspektions-APIs der unteren Ebene ermöglichen beispielsweise Entwicklungswerkzeugen direkten Zugriff auf die Komponenteninterna – eine Funktion, die Sie nicht unbedingt in den Händen von Komponentenbenutzern sehen wollen. APIs der höheren Ebene verwenden APIs der unteren Ebene zur Bestimmung der Teile einer Komponente, die zur Änderung durch den Benutzer exportiert werden. Zwar
570
Entwicklungswerkzeuge
müssen Entwicklungswerkzeuge Gebrauch von beiden Arten von APIs machen, sie verwenden jedoch die APIs der höheren Ebene nur dazu, um dem Benutzer Komponenteninformationen zu bieten. Application-Builder-Support-APIs stellen den bei Entwurfszeit für Bearbeitung und Manipulation der Komponenten erforderlichen Overhead bereit. Diese APIs werden größtenteils von visuellen Entwicklungswerkzeugen eingesetzt, um ein visuelles Layout und die Veränderung von Komponenten während der Erstellung einer Anwendung zu ermöglichen. Der Teil einer Komponente, der die visuellen Bearbeitungsmöglichkeiten bereitstellt, wurde speziell so entworfen, dass er physisch von der Komponente selbst getrennt ist und somit dazu beiträgt, autonome Laufzeitkomponenten so kompakt wie möglich zu gestalten. In einer reinen Laufzeitumgebung werden Komponenten nur mit der erforderlichen Laufzeitkomponente übertragen. Entwickler, die lediglich den Entwurfszeitteil der Komponente benutzen wollen, können dies tun. Die JavaBeans-Spezifikationen sind verfügbar auf der Java-Website von Sun. Sie finden die JavaBeans-Komponenten-API-Dokumentation unter:
http://java.sun.com/j2se/1.3/docs/guide/beans/
19.2 Entwicklungswerkzeuge Am leichtesten versteht man JavaBeans, wenn man mit ihnen in einer Programmierumgebung arbeitet, die Bean-Entwicklung unterstützt. Bean-Programmierung verlangt eine Umgebung mit einer relativ ausgefeilten grafischen Benutzerschnittstelle, denn viel Entwicklungsarbeit geht visuell vonstatten. In einer integrierten Entwicklungsumgebung wie Sun ONE Studio können Sie zwei Beans innerhalb einer Schnittstelle in Beziehung setzen, indem Sie die grafische Benutzerschnittstelle der Software verwenden. Die Werkzeuge des Software Development Kits werden fast ausschließlich von der Kommandozeile aus, ohne grafische Benutzerschnittstelle benutzt. Daher benötigen Sie ein anderes Programmiertool, um JavaBeans zu entwickeln. Die meisten kommerziellen JavaEntwicklungstools unterstützen JavaBeans, z. B. Metrowerks CodeWarrior Professional, IBM Visual Age for Java, Borland JBuilder und Oracle JDeveloper. Wenn Sie nach einer integrierten Java-Entwicklungsumgebung mit JavaBeansSupport suchen, sollten Sie genau darauf achten, ob sie Java 2 SDK 1.4 oder nur eine ältere Version der Sprache unterstützt.
571
JavaBeans
Wenn Sie kein Entwicklungstool mit JavaBeans-Unterstützung haben, können Sie das kostenlose JavaBeans Development Kit von Sun benutzen.
Das JavaBeans Development Kit Das JavaBeans Development Kit von Sun, auch BDK genannt, ist ein kostenloses Tool, das man zum Experimentieren benutzen kann, wenn keine andere Programmierumgebung mit Bean-Unterstützung zur Verfügung steht. Wenn das in Ihren Ohren so klingt, als wollten wir das BDK von vorneherein schlecht machen, haben Sie unsere Intention richtig erkannt. Sun selbst äußert sich auf seiner JavaWebseite wie folgt: »Das BDK ist weder dafür gedacht, von Applikationsentwicklern benutzt zu werden, noch soll es eine echte Applikationsentwicklungsumgebung darstellen. Applikationsentwickler sollten stattdessen die verschiedenen Java-Entwicklungsumgebungen mit JavaBeans-Unterstützung in Betracht ziehen.« Das BDK diente bei seiner Veröffentlichung einem ähnlichen Zweck wie das ursprüngliche Java Development Kit: Es sollte Programmierern einen Zugang zu der neuen Technologie geben, solange noch keine Alternative verfügbar war. Da es mittlerweile zahlreiche Programmiertools mit JavaBeans-Unterstützung gibt, hat Sun wenig Interesse, die Funktionalität des BDK zu verbessern oder seine Performance zu erhöhen. Das BDK ist eigentlich nur zum Einstieg in die JavaBeans-Entwicklung brauchbar, und genau dafür wollen wir es heute einsetzen. Das BDK ist für Windows und Solaris erhältlich. Es wurde mithilfe von Java entwickelt, sodass es auch eine plattformunabhängige Version gibt, die Sie auf anderen Plattformen mit Java-Unterstützung verwenden können. Die Download-Adresse lautet: http://sun.java.com/beans/software/bdk_download.html
Sollte dieser Link nicht mehr funktionieren, gehen Sie auf die Java-Hauptseite http://java.sun.com. Das JavaBeans Development Kit finden Sie im Download-Bereich dieser Site. Das BDK ist 2,4 MB groß. Man braucht mit einem 56.6kb-Modem rund 15 Minuten zum Herunterladen, während ein Breitbandanschluss dies in 2 Minuten erledigt. Während Sie also auf das Ende des Downloads warten, können Sie die Installationsanweisungen und Zusatzbemerkungen auf der BDK-Download-Seite lesen. Eventuell müssen Sie die Einstellungen des CLASSPATH Ihres Systems ändern, damit das BDK korrekt funktioniert. Das BDK wird als einzelne ausführbare Datei übermittelt, die man zur Installation der Software starten muss, oder aber als ZIP-Datei. Wenn Sie die ZIP-Datei herunterladen, finden sich alle Dateien des BDK in dem beans-Ordner im Archiv. Kopieren oder verschie-
572
Mit JavaBeans arbeiten
ben Sie diesen Ordner aus dem Archiv in einen Ordner auf Ihr System (z. B. in den Hauptinstallationsordner von Java 2). Während der Installation über die EXE-Datei müssen Sie die Java Virtual Machine auswählen, die das BDK benutzen soll. Verwenden Sie den Java-Interpreter, den Sie benutzt haben, um die Java-2-Programme dieses Buches auszuführen. Das Windows-Installationsprogramm des BDK könnte Kompatibilitätsprobleme mit der aktuellen Version von Java 2 haben. Wenn Sie es nicht installieren können, versuchen Sie stattdessen die plattformunabhängige Version des BDK zu installieren, die als ZIP-Archiv vorliegt. Das BDK umfasst folgende Teile:
die BeanBox – ein JavaBean-Container, mit dem man Beispiel-Beans und von Ihnen erstellte Beans manipulieren kann
mehr als ein Dutzend Beispiel-Beans, z. B. die Juggler-Bean, die eine Jongleur-Animation darstellt, die Molecule-Bean, die das 3D-Modell eines Moleküls anzeigt, und OrangeButton, eine Benutzerschnittstellenkomponente
den kompletten Java-Quelltext von BeanBox
Makefiles – Konfigurationsskripten, mit denen das BDK wieder erstellt werden kann
ein Tutorial von Sun zu JavaBeans und der BeanBox
19.3 Mit JavaBeans arbeiten Wenn Sie erst einmal mit JavaBeans in einer Entwicklungsumgebung wie dem BDK arbeiten, werden Sie schnell feststellen, wie sehr sie sich von Java-Klassen unterscheiden, die nicht als Beans entworfen wurden. JavaBeans unterscheiden sich von anderen Klassen in einem wesentlichen Charakteristikum: Sie können mit einer Entwicklungsumgebung interagieren und laufen in ihr, als würde ein Benutzer sie ausführen. Auf der anderen Seite kann die Entwicklungsumgebung direkt mit der JavaBean interagieren, ihre Methoden aufrufen und Werte für ihre Variablen festlegen. Wenn Sie das BDK installiert haben, können Sie es in den folgenden Abschnitten benutzen, um mit existenten JavaBeans zu arbeiten und eine neue Bean zu erstellen. Wenn Sie kein BDK installiert haben, lernen Sie dennoch mehr darüber, wie JavaBeans in Verbindung mit einer Entwicklungsumgebung genutzt werden.
573
JavaBeans
Bean-Container Java Beans haben etwas mit Swing gemeinsam: beide benutzen Container – Benutzerschnittstellenkomponenten, die andere Komponenten beinhalten. JavaBeans-Entwicklung findet in einem Bean-Container statt. Das BDK bringt die BeanBox mit, einen rudimentären Container, mit dem man Folgendes durchführen kann:
eine Bean speichern
eine gespeicherte Bean laden
eine Bean in ein Fenster setzen, wo sie anschließend platziert werden kann
Beans bewegen und größenmäßig verändern
die Eigenschaften einer Bean bearbeiten
eine Bean konfigurieren
eine Bean assoziieren, die ein Ereignis mit einem Event-Handler erzeugt
die Eigenschaften verschiedener Beans miteinander assoziieren
eine Bean in ein Applet umwandeln
neue Beans aus einem Java-Archiv (jar-Dateien) hinzufügen
Um die BeanBox-Applikation auszuführen, gehen Sie zu dem Ordner, in dem das BDK installiert wurde und öffnen den Unterordner beanbox. Dieser Unterordner enthält zwei Batch-Kommandodateien, mit denen die BeanBox ausgeführt werden kann: run.bat für Windows-Rechner und run.sh für Solaris-Systeme. Diese Batch-Dateien laden die BeanBox-Applikation. Dabei wird der Java-Interpreter eingesetzt, den Sie während der BDK-Installation angegeben haben, was vermutlich der Java2-Interpreter war. Es öffnen sich vier Fenster, wie Abbildung 19.1 zeigt: Das große Fenster ist das BeanBox-Composition-Fenster, das Beans arrangiert und die Assoziationen unter ihnen erzeugt. Die beiden anderen Fenster oben sind das Werkzeugkasten-Fenster (links), das die JavaBeans auflistet, die für die Platzierung ins Composition-Fenster ausgewählt werden können, und ein Eigenschafts-Fenster (rechts), mit dem die Bean konfiguriert wird. Das vierte Fenster, rechts etwas weiter unten, ist das Methodenverfolgungs-Fenster, das Informationen darüber liefert, wie Komponenten in der BeanBox miteinander interagieren. Die meiste Arbeit erfolgt im Composition-Fenster, das man mit dem Hauptfenster eines Grafikprogramms wie Adobe Illustrator vergleichen kann. In diesem Fenster werden alle Beans platziert, neu arrangiert, aufgereiht und für die Bearbeitung ausgewählt.
574
Mit JavaBeans arbeiten
Abbildung 19.1: Die Fenster, die die BeanBox-Applikation darstellen
Eine Bean platzieren Um eine Bean in die BeanBox zu platzieren, muss sie zuerst im Werkzeugkasten-Fenster ausgewählt werden. Wenn Sie dies tun, verwandelt sich der Cursor in ein Fadenkreuz. Mit dem Fadenkreuz können Sie auf einen beliebigen Punkt im Composition-Fenster klicken, um die ausgewählte Bean dorthin zu legen. Wenn Sie eine Bean platzieren, ist es nützlich, sie erst einmal in die Mitte des Composition-Fensters zu legen. Mit den Menükommandos Edit, Cut and Edit sowie Paste können Sie die Bean bei Bedarf bewegen. Sie können eine Bean auch bewegen, indem Sie den Cursor auf die Kante einer Bean stellen, bis der Cursor zu einem Kreuz von Richtungspfeilen wird. Sie können dann die Bean zu ihrem neuen Ort verschieben und müssen nur noch die Maus loslassen. Versuchen Sie das gleich, indem Sie auf »Juggler« im Werkzeugkasten-Fenster klicken und dann auf eine beliebige Stelle in der Mitte des Composition-Fensters klicken. Ein animierter, jonglierender Backenzahn erscheint im Hauptfenster (Abbildung 19.2). Sie erkennen den Jongleur natürlich – das ist kein Zahn, sondern Duke, das offizielle Maskottchen von Java. Und passenderweise jongliert er mit Riesenbohnen (engl. bean: Bohne).
Abbildung 19.2: Duke jongliert mit Riesenbohnen im Hauptfenster der BeanBox.
575
JavaBeans
Die gestreifte Linie um die Juggler-Bean in Abbildung 19.2 zeigt an, dass sie derzeit für die Bearbeitung ausgewählt ist. Sie können das BeanBox-Fenster auswählen, indem Sie auf eine beliebige Stelle außerhalb der Juggler-Bean klicken. Sie können dann wiederum die Juggler-Bean auswählen, indem Sie erneut auf sie klicken. Sie können eine Bean nur dann bearbeiten, kopieren, ausschneiden oder einfügen, wenn sie ausgewählt ist.
19.4 Die Eigenschaften einer Bean anpassen Wenn eine Bean im Composition-Fenster der BeanBox ausgewählt wurde, werden ihre veränderlichen Eigenschaften, sofern es überhaupt welche gibt, im Eigenschaftsfenster angezeigt. Dieses Fenster wird für unser Beispiel in Abbildung 19.3 dargestellt.
Abbildung 19.3: Die veränderlichen Eigenschaften einer Bean im Eigenschaftsfenster
Wie Abbildung 19.3 zeigt, hat die Juggler-Bean drei veränderliche Eigenschaften: debug, animationRate und name. Verändert man die Eigenschaften einer JavaBean, spiegelt sich das in der Bean wieder. Geben Sie der animationRate-Eigenschaft der Juggler-Bean einen höheren, ganzzahligen Wert, gibt es eine längere Pause zwischen den einzelnen Frames der Animation. Wenn Sie die Eigenschaft verringern, beschleunigt sich die Animation. Wenn Sie die animationRate-Eigenschaft verändern, ändert sich die Bean entsprechend, sobald Sie zu einer anderen Eigenschaft springen (wenn Sie also die (Tab)-Taste drücken oder auf den Wert einer anderen Eigenschaft klicken). Geben Sie Extremwerte wie 1 oder 1000 für die Animationsgeschwindigkeit ein, um das Ergebnis an der Juggler-Bean zu sehen. Man richtet die veränderlichen Eigenschaften einer JavaBean durch öffentliche Methoden innerhalb der Bean ein. Jede Eigenschaft, die festgelegt werden kann, hat eine set()Methode, deren vollständiger Name dem Namen der Eigenschaft im Eigenschaftsfenster der BeanBox entspricht. Umgekehrt hat jede Eigenschaft, deren Wert gelesen werden kann, eine korrespondierende get()-Methode. Eine JavaBeans-Entwicklungsumgebung wie die BeanBox findet diese Methoden mittels Reflexion und ermöglicht es Ihnen dann, diese Eigenschaften während der Entwurfszeit oder im laufenden Programm zu ändern. Die animationRate-Eigenschaft der Juggler-Bean hat zwei Methoden, die folgendermaßen aussehen könnten:
576
Die Eigenschaften einer Bean anpassen
public int getAnimationRate(){ return animRate; } public void setAnimationRate(int newRate) { animRate = newRate; }
In diesen beiden Methoden ist animRate eine private Variable, die die Pause zwischen den Frames der Jongleur-Animation festlegt. Indem der Entwickler der Juggler-Bean die Präfixe set und get für diese Methodennamen benutzt hat, legte er fest, dass die animationRate-Eigenschaft innerhalb einer JavaBean-Entwicklungsumgebung wie der BeanBox modifiziert werden kann. Wie alle Bean-Entwicklungstools, die gemäß den Standards von Sun arbeiten, ruft die BeanBox die öffentliche get()-Methode der Bean auf, um festzustellen, welche Eigenschaften ins Eigenschaftsfenster aufgenommen werden müssen. Wenn eine dieser Eigenschaften verändert wird, wird eine set()-Methode mit dem neuen Wert als Argument aufgerufen. Der Entwickler einer Bean kann dieses Verhalten überschreiben, indem er eine BeanInfoKlasse erstellt, die die Methoden, Eigenschaften, Ereignisse und andere Informationen angibt, die innerhalb einer Bean-Entwicklungsumgebung zugänglich sein sollen. Eine Variable privat zu halten und sie mit get() und set() zu lesen und zu setzen, ist beim objektorientierten Programmieren immer ein gutes Prinzip, auch wenn Sie nicht gerade an einer JavaBean arbeiten. Man nennt dies Verkapselung. Damit lässt sich kontrollieren, inwieweit ein Objekt anderen Objekten zugänglich ist. Je stärker ein Objekt gekapselt ist, umso schwieriger wird es für andere Objekt, es missbräuchlich zu verwenden.
Beans interagieren lassen Ein weiterer Zweck der BeanBox besteht darin, Interaktionen zwischen verschiedenen Beans einzurichten. Schauen wir uns an, wie das funktioniert. Setzen Sie zunächst zwei ExplicitButtons an eine beliebige Stelle in das Composition-Fenster der BeanBox. Wenn sie mit der JugglerBean oder untereinander überlappen, dann bewegen Sie sie ein wenig auseinander. Es scheint zwei Bugs bei den Beans aus dem JavaBean Development Kit zu geben, wobei das Problem allerdings anscheinend auf Windows XP begrenzt ist. Wenn Sie die ExplicitButton-Bean nicht im Werkzeugkasten-Fenster sehen können, müssen Sie einige der JARArchivdateien des BDK aktualisieren. Rufen Sie dazu die Website zum Buch auf, http://
577
JavaBeans
www.java21pro.com, suchen Sie die Seite zu Tag 19, laden Sie die Dateien buttons.jar und test.jar herunter, und speichern Sie sie im jars-Ordner Ihrer BDK-Installation (wenn Sie also BDK nach C:\j2sdk1.4.0\beans installiert haben, dann speichern Sie die beiden jarDateien nach C:\j2sdk1.4.0\beans\jars). Schließen Sie danach die BeanBox und starten
Sie sie neu. Um eine Bean zu bewegen, müssen Sie sie zuerst anklicken, damit die gestrichelte Linie um sie herum erscheint. Dann setzen Sie den Cursor auf die Unterkante der Bean, bis der Cursor zu einem Kreuz aus vier Pfeilen wird. Anschließend ziehen Sie die Bean an ihren neuen Ort. Abbildung 19.4 zeigt zwei Buttons an der Unterkante der Juggler-Bean.
Abbildung 19.4: Zwei ExplicitButton-Beans und eine Juggler-Bean im Hauptfenster der BeanBox ExplicitButton-Beans ähneln den JButton-Komponenten, die Sie in grafischen Benutzer-
schnittstellen verwendet haben. Sie haben eine Hintergrundfarbe, eine Vordergrundfarbe und ein Textlabel mit wählbaren Schriften. Nach der Platzierung der Buttons geben Sie dem einen Button das Label »Stop!« und ändern seine Hintergrundfarbe auf Rot. Geben Sie dem anderen das Label »Go!« und ändern seine Hintergrundfarbe auf Grün. Um das Label eines Buttons zu ändern, klicken Sie auf den Button in der BeanBox und bearbeiten dann das Label-Textfeld im Eigenschaftsfenster. Um die Hintergrundfarbe zu ändern, klicken Sie auf das Feld neben dem Label »Background« im Eigenschaftsfenster. Ein Color-Editor-Dialog wird geöffnet und ermöglicht Ihnen, eine Farbe durch Eingabe numerischer Werte für Rot, Grün und Blau oder durch Verwendung eines Listenfeldes zu wählen. Die durchgeführten Veränderungen werden sofort in der Bean widergespiegelt. Der Zweck der beiden Knöpfe sollte klar sein: Der eine hält die Animation an, der andere startet sie. Dazu müssen Sie eine Beziehung zwischen den Buttons und der Juggler-Bean herstellen. Zuerst wählen Sie die Bean aus, die irgendetwas verursachen soll. In unserem Beispiel wäre das einer der beiden ExplicitButtons. Wenn man einen der beiden anklickt, dann soll etwas mit der Juggler-Bean geschehen.
578
Die Eigenschaften einer Bean anpassen
Nachdem Sie die Bean ausgewählt haben, wählen Sie das Menükommando Edit/Events /button push/actionPerformed. Eine rote Linie verbindet den Button und den Cursor, wie Abbildung 19.5 zeigt.
Abbildung 19.5: Eine Ereignis-Assoziation zwischen zwei Beans herstellen
Diese rote Linie soll die ExplicitButton-Bean mit der Juggler-Bean verbinden. Ziehen Sie die Linie zur Juggler-Bean und klicken Sie dann, um die Assoziation zwischen den beiden Beans herzustellen. Ist die Assoziation eingerichtet, sehen Sie ein EventTargetDialog-Fenster, das die Methoden der Ziel-Bean auflistet (Abbildung 19.6). Die ausgewählte Methode wird automatisch aufgerufen, wenn der spezifizierte ExplicitButton ein actionPerformed-Ereignis ausgibt. Dieses Ereignis tritt auf, wenn der Button angeklickt wird oder wenn die Return-Taste betätigt wird, während der Eingabefokus der Schnittstelle auf dem Button liegt.
Abbildung 19.6: Eine aufzurufende Methode im EventTargetDialog auswählen
Die Juggler-Bean hat zwei Methoden, um die Jongleur-Animation anzuhalten bzw. zu starten. Sie heißen stopJuggling() bzw. startJuggling(). Indem der Entwickler des Juggler solches Verhalten in eigene Methoden separiert hat, ermöglicht er, dass diese Methoden bei der Interaktion zwischen verschiedenen Beans eingesetzt werden können. Es gehört zu den größten Aufgaben bei der JavaBeans-Entwicklung, die Methoden einer Bean derartig zu organisieren, um so viele Interaktionen wie nötig anzubieten.
579
JavaBeans
Der »Stop«-Button sollte mit der stopJuggling()-Methode der Juggler-Bean assoziiert werden, der »Go«-Button sollte mit der startJuggling()-Methode assoziiert werden. Mit der Einrichtung dieser Interaktion zwischen drei JavaBeans haben Sie ein einfaches, aber voll funktionierendes Java-Programm geschaffen, das eine Animation anzeigen, starten und stoppen kann.
Ein JavaBeans-Programm erzeugen Wenn Sie eine oder mehrere JavaBeans auf eine gemeinsame Schnittstelle gesetzt, ihre Eigenschaften angepasst und Interaktionen zwischen ihnen eingerichtet haben, haben Sie ein Java-Programm erzeugt. Um ein Projekt in der BeanBox abzuspeichern, benutzen Sie das Kommando File/Save. Damit können Sie die folgenden Informationen in einer Datei speichern:
die Beans, wie sie gerade konfiguriert sind
das Arrangement der Beans
die Größe des Fensters, in dem sich die Beans befinden
die Interaktion zwischen den Beans
Damit wird das Projekt aber nicht als Java-Programm abgespeichert, das außerhalb der BeanBox lauffähig wäre. Um ein Projekt in ausführbarer Form abzuspeichern, benutzen Sie File/MakeApplet. Dieses Kommando erfordert zweierlei: einen Namen für die Hauptklassendatei des Applets und einen Namen für das jar-Archiv, in dem alle für das Applet erforderlichen Dateien gespeichert werden, also die Klassendateien und andere Daten. Nachdem Sie diese Namen angegeben haben, wird ein Applet samt einer Beispiel-HTMLSeite zum Laden erzeugt. Die HTML-Datei wird in denselben Ordner gelegt, in dem sich die jar-Datei des Archivs befindet. Sie können die Seite mit dem Appletviewer oder einem Browser mit Java-2-Plug-In laden. Diese Applets werden mithilfe von jar-Dateien weitergegeben, die das Applet selbst und alle darin befindlichen Beans beinhalten. Listing 19.1 enthält das Applet-Tag, das die BeanBox für das Applet erzeugt hat, das JugglingFool getauft wurde. Listing 19.1: Das von der BeanBox erzeugte Applet-Tag 1: 2: 3: 4: 5:
580
Test page for JugglingFool as an APPLET
Die Eigenschaften einer Bean anpassen
6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
Test for JugglingFool as an APPLET This is an example of the use of the generated JugglingFool applet. Notice the Applet tag requires several archives, one per JAR used in building the Applet Trouble instantiating applet JugglingFool!!
Abbildung 19.7 zeigt die Jongleur-Animation, wie sie im Appletviewer läuft.
Abbildung 19.7: Ein JavaBeans-Applet, das im appletviewer läuft
Die Größe des Applet-Fensters wird durch die Größe des Composition-Fensters der BeanBox festgelegt. Um die Größe des Fensters zu verändern, wählen Sie es an, indem Sie auf eine beliebige Stelle im Fenster klicken, an der sich keine Bean befindet, und dann seine Größe ändern, als wäre es eine Bean.
Mit anderen JavaBeans arbeiten Software-Entwicklung auf der Basis vorgefertigter Komponenten ist eine Form von Rapid Application Development (RAD), was sich als »Schnelle Applikationsentwicklung« überset-
581
JavaBeans
zen ließe. RAD benutzt man oft, um eine lauffähige Version einer Software als Demonstration oder Prototyp zu erzeugen. Ein häufiges Beispiel für RAD ist, dass man mit Microsoft Visual Basic den Prototyp eines Visual C++-Programms erzeugt. Die flinke Entwicklung einer grafischen Benutzerschnittstelle ist eine der Stärken von Visual Basic, das damit besser für Protoypenerzeugung geeignet ist als das komplexere Visual C++. JavaBeans machen RAD unter Java leicht. Ein Programmierer kann im Nu ein lauffähiges Programm aus existierenden JavaBeans-Komponenten zusammenschustern. Sun und andere Entwickler stellen Hunderte von JavaBeans zur Verfügung. Sie werden u. a. an folgenden Adressen fündig:
JARS, der Java Applet Ratings Service hat einen JavaBeans-Ressourcen-Bereich:
http://www.jars.com/jars_resources_javabeans.html
Developer Tools Guide des Magazins JavaWorld:
http://www.javaworld.com/javaworld/tools/jw-tools-index.html
JavaBeans-Homepage von Sun:
http://java.sun.com/beans
Beans sind in jar-Archive gepackt. Wenn Sie eine Bean heruntergeladen haben und wünschen, dass sie im Werkzeugkasten-Fenster erscheint, müssen Sie das jar-Archiv der Bean in den jars-Ordner des BDK speichern. Dieser Ordner kann in dem Ordner gefunden werden, in dem das BDK auf Ihrem System installiert wurde.
19.5 Zusammenfassung In Kombination mit einer integrierten Entwicklungsumgebung ermöglichen JavaBeans Rapid Application Development von Java-Programmen. Heute haben Sie die grundlegenden Prinzipien wieder verwendbarer Komponenten kennen gelernt und erfahren, wie diese Prinzipien in Java verwirklicht sind. Wir haben diese Ideen in die Praxis umgesetzt und gesehen, wie das JavaBeans Development Kit (BDK) von Sun benutzt werden kann, um mit existierenden Beans zu arbeiten, Verbindungen zwischen ihnen herzustellen und komplette Java-Programme zu erzeugen. Um eigene Programme mit JavaBeans zu erzeugen, müssen Sie sich ein besseres Entwicklungstool als das BDK aussuchen. Doch mit dem BDK können Sie immerhin einen Eindruck davon erhalten, inwieweit sich Beans für Ihre Programmieraufgaben eignen.
582
Workshop
Sie sollten die JavaBeans-Ressourcen im Internet nutzen. Viele der im Netz verfügbaren Beans erfüllen Aufgaben, die Sie in Ihren eigenen Programmen behandeln wollen. Mit der Benutzung von Beans können Sie sich viele Arbeiten sparen, bei denen Sie sonst bei null beginnen müssten.
19.6 Workshop Fragen und Antworten F
Wird das JavaBeans Development Kit zu einem echten Bean-Programmiertool ausgebaut werden? A
F
Sun erklärt auf seiner Website, dass die Entwicklung von BeanBox im Jahr 1999 eingestellt wurde und sie nur noch für »Schulungs- und Demonstrationszwecke« eingesetzt werden soll. Weiterhin gibt Sun an, dass das BDK für das Testen von Beans und als Referenz, wie Beans in einer Entwicklungsumgebung verwendet werden sollten, intendiert ist. Demnach erscheint es so, als ob professionelle Programmiertools wie Visual Café, Sun ONE Studio und andere wohl die beste Wahl für JavaBeans-Entwickler bleiben.
Im JugglingFool-Beispiel ist die Eigenschaft animationRate mit kleinem »a« geschrieben, doch die Methoden heißen setAnimtationRate() bzw. getAnimationRate(), jeweils mit großem »A«. Warum ist das so? A
Das liegt an den Benennungskonventionen: Alle Variablen und Methodennamen beginnen mit einem Kleinbuchstaben, und alle Wörter außer dem ersten in einer Variable beginnen mit einem Großbuchstaben.
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Angenommen, Sie entwickeln eine Bean, die eine getWindowHeight()-Methode, die einen Integer ausgibt, und eine setWindowHeight(int)-Methode hat. Welche Eigenschaft erscheint in einer Bean-Entwicklungsumgebung? (a) WindowHeight (b) windowHeight (c) gar nichts, wenn Sie nichts explizit in einer BeanInfo-Datei angeben
583
JavaBeans
2. Wann kann man die Eigenschaften einer Bean ändern? (a) zur Entwurfzeit (b) zur Laufzeit (c) zu beiden Zeiten 3. Wie ändern Sie die Größe eines Applets, das mithilfe des BDK erzeugt wurde? (a) Bearbeiten Sie das HTML, das das BDK nach der Erzeugung des Applets generierte. (b) Bearbeiten Sie eine Eigenschaft der BeanBox. (c) Ändern Sie die Größe der BeanBox, bevor Sie das Applet erzeugen.
Antworten 1. b. Allerdings können Sie auch die BeanInfo-Datei benutzen, um zu verhindern, dass windowHeight als Eigenschaft in der Bean-Entwicklungsumgebung auftaucht. 2. c. Wie Sie beim Juggler-Beispiel gesehen haben, laufen Beans sogar, während sie entworfen werden. 3. c. Antwort a ist jedoch auch richtig, denn Sie können natürlich auch das HTML direkt bearbeiten und die Attribute HEIGHT und WIDTH des APPLET-Tags ändern.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Gegeben sei: public class NameDirectory { String[] names; int nameCount; public NameDirectory() { names = new String[20]; nameCount = 0; }
584
Workshop
public void addName(String newName) { if (nameCount < 20) // answer goes here } }
Die Klasse NameDirectory muss 20 verschiedene Namen speichern können. Was sollte anstelle von // answer goes here stehen, damit die Klasse wie erwünscht funktioniert? a. names[nameCount] = newName; b. names[nameCount] == newName; c. names[nameCount++] = newName; d. names[++nameCount] = newName; Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 19, und klicken Sie auf den Link »Certification Practice«.
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Laden Sie im JARS JavaBean-Ressourcen-Bereich eine Bean herunter und verwenden Sie sie in der BeanBox.
Fügen Sie eine TickTock-Bean zum Juggler-Projekt hinzu. Eine TickTock-Bean lässt etwas in bestimmten Zeitintervallen geschehen. Experimentieren Sie mit der Bean herum. Sie soll die Juggler-Bean alle 30 Sekunden neu starten.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Website zum Buch: http://www.java21pro.com.
585
Daten mit JDBC lesen und schreiben
0 2
Daten mit JDBC lesen und schreiben
Fast jedes Java-Programm geht in irgendeiner Form mit Daten um. Bisher haben sie primitive Typen, Objekte, Arrays, verkettete Listen und andere Datenstrukturen zur Repräsentation von Daten benutzt. Wenn Ihre Programme komplexer werden, werden auch Ihre Anforderungen steigen. Heute lernen Sie, wie Sie mit Daten in ausgefeilterer Art und Weise umgehen: Wir erkunden die Java Database Connectivity (JDBC). Das ist eine Klassenbibliothek, die Java-Programme mit relationalen Datenbanken verbindet, wie sie von Microsoft, Sybase, Oracle, Informix und anderen Herstellern entwickelt werden. Indem man einen Treiber als Brücke zur Datenbankquelle benutzt, kann man direkt von Java aus Daten speichern und auslesen. Danach sehen wir uns an, wie Daten intern in Java repräsentiert werden, und arbeiten mit neuen Datenstrukturen, die Arrays und verkettete Listen ergänzen. Heute geht es um Database Connectivity, und dabei werden im Einzelnen die folgenden Themen behandelt:
Wie man JDBC-Treiber benutzt, um mit verschiedenen relationalen Datenbanken zu arbeiten
Wie man auf eine Datenbank mit Structured Query Language (SQL) zugreift
Wie man eine JDBC-Datenquelle einrichtet
Wie man mit SQL und JDBC die Einträge einer Datenbank liest
Wie man mit SQL und JDBC neue Einträge hinzufügt
20.1 Java Database Connectivity Java Database Connectivity (JDBC) ist eine Gruppe von Klassen, mit denen Client/ServerDatenbankapplikationen unter Java erstellt werden können. Client/Server-Software verbindet einen Benutzer von Informationen mit einem Lieferanten dieser Informationen. Dies ist eine der üblichsten Formen der Programmierung. Sie kommt jedes Mal zum Einsatz, wenn Sie im Internet unterwegs sind: Ein Clientprogramm namens Browser fordert Webseiten, Bilddateien und andere Dokumente mittels einer URL an. Verschiedene Serverprogramme liefern dem Client die gewünschten Informationen, falls sie gefunden werden können. Datenbankprogrammierer müssen mit der Tatsache kämpfen, dass sehr viele verschiedene Datenbankformate in Gebrauch sind, die jeweils eine eigene, proprietäre Methode des Datenzugriffs haben. Um den Gebrauch relationaler Datenbanken zu vereinfachen, wurde eine Standardsprache namens SQL (Structured Query Language) eingeführt. Dank dieser Sprache erübrigt es sich, für jedes Datenbankformat eine eigene Sprache für Datenanfragen zu erlernen.
588
Java Database Connectivity
In der Datenbankprogrammierung heißt die Anforderung von Einträgen in einer Datenbank Anfrage (Query). Wenn Sie SQL benutzen, können Sie komplexe Anfragen an eine Datenbank senden und die gesuchten Einträge in einer beliebigen, von Ihnen angegebenen Reihenfolge erhalten. Als Beispiel stellen wir uns einen Datenbankprogrammierer bei einer Kreditgesellschaft vor, der eine Liste der säumigsten Zahler erstellen soll. Der Programmierer könnte SQL benutzen, um bei einer Datenbank nach allen Einträgen anzufragen, bei denen die letzte Zahlung mehr als 180 Tage zurückliegt und der geschuldete Betrag höher als 0,00 EUR liegt. Mit SQL kann auch die Reihenfolge festgelegt werden, in der die Einträge zurückgegeben werden. Der Programmierer könnte die Einträge also geordnet nach Kundennummer, Name, geschuldetem Betrag oder nach jedem anderen Feld der Kredit-Datenbank zurück erhalten. All dies ist mit SQL möglich. Der Programmierer müsste sich nicht um die proprietären Sprachen kümmern, die die populären Datenbanken benutzen. SQL wird von vielen Datenbankformaten unterstützt, sodass Sie theoretisch dieselben SQL-Kommandos für jedes Datenbanktool mit Unterstützung dieser Sprache einsetzen können sollten. Trotzdem kann es notwendig werden, dass Sie einige Besonderheiten eines speziellen Datenbankformats lernen müssen, selbst wenn Sie mit SQL zugreifen. SQL ist der Industriestandard für den Zugriff auf relationale Datenbanken. JDBC unterstützt SQL, was Entwicklern ermöglicht, zahlreiche Datenbankformate zu nutzen, ohne sich um die Besonderheiten der zugrunde liegenden Datenbank kümmern zu müssen. Darüber hinaus ermöglicht es den Gebrauch von Datenbankanfragen, die für ein Datenbankformat spezifisch sind. Die Art des Datenbankzugriffs der JDBC-Klassenbibliothek mittels SQL ist vergleichbar mit existenten Datenbankentwicklungstechniken, sodass die Kommunikation mit einer SQL-Datenbank über JDBC sich nicht sehr von der über traditionelle Datenbanktools unterscheidet. Java-Programmierer, die sich bereits mit Datenbanken auskennen, können sofort mit JDBC loslegen. Die JDBC-API wurde bereits weitgehend von den großen Firmen berücksichtigt. Einige Firmen, die Entwicklungstools erstellen, haben Unterstützung für JDBC in ihren Entwicklungsprodukten angekündigt. Die JDBC-Bibliothek hat Klassen für die einzelnen Aufgaben, die sich beim Umgang mit Datenbanken stellen:
eine Verbindung zu einer Datenbank herstellen
eine Anweisung mittels SQL erstellen
diese SQL-Anfrage in der Datenbank ausführen
die resultierenden Einträge ansehen
Diese JDBC-Klassen sind alle Teil des java.sql-Pakets von Java 2.
589
Daten mit JDBC lesen und schreiben
Datenbanktreiber Java-Programme, die JDBC-Klassen verwenden, können dem üblichen Programmiermodell folgen, SQL-Anweisungen zu geben und die resultierenden Daten zu verarbeiten. Dabei sind sie unabhängig vom Format der Datenbank und von der Plattform, auf der sie erstellt wurde. Diese Plattform- und Datenbankunabhängigkeit wird in einem Java-Programm durch einen Treiber-Manager ermöglicht. Die Klassen der JDBC-Klassenbibliothek sind weitgehend von Treiber-Managern abhängig, die sich darum kümmern, welche Treiber für den Zugriff auf Datenbankeinträge notwendig sind. Sie erfordern einen eigenen Treiber für jedes Datenbankformat, das in einem Programm benutzt wird. Manchmal benötigt man sogar mehrere Treiber für verschiedene Versionen desselben Formats. JDBC-Datenbanktreiber sind entweder komplett in Java geschrieben oder mithilfe nativer Methoden implementiert, um Java-Applikationen mit existenten Datenbank-Zugriffsbibliotheken zu verbinden. JDBC hat einen Treiber, der JDBC mit einem anderen Datenbank-Connectivity-Standard namens ODBC verbindet.
Die JDBC-ODBC-Brücke ODBC, Microsofts Schnittstelle zu SQL-Datenbanken, wird auf einem Windows-System vom ODBC-Datenquellen-Administrator verwaltet. Unter Windows führt man ihn über die Systemsteuerung aus. Bei den meisten WindowsVersionen wählt man Start/Einstellungen/Systemsteuerung/ODBC Datenquellen, bei Windows XP hingegen Start/Einstellungen/Systemsteuerung/Verwaltung/Datenquellen (ODBC). Der Administrator fügt ODBC-Treiber hinzu, konfiguriert Treiber, damit sie mit speziellen Datenbankdateien funktionieren, und loggt die SQL-Benutzung. Abbildung 20.1 zeigt den ODBC-Datenquellen-Administrator auf einem Windows-System. In Abbildung 20.1 listet das Dialogfeld »Drivers« alle ODBC-Treiber im System auf. Viele Treiber sind spezifisch für das Datenbankformat einer bestimmten Firma, so z. B. der Microsoft-Access-Treiber. Die JDBC-ODBC-Brücke ermöglicht JDBC-Treibern, als ODBC-Treiber benutzt zu werden, indem JDBC-Methodenaufrufe in ODBC-Funktionsaufrufe konvertiert werden.
590
Java Database Connectivity
Abbildung 20.1: Der ODBC-Datenquellen-Administrator auf einem Windows-XP-System
Die Verwendung der JDBC-ODBC-Brücke erfordert Folgendes:
den JDBC-ODBC-Brücken-Treiber von Java 2: sun.jdbc.odbc.JdbcOdbcDriver
einen ODBC-Treiber
eine ODBC-Datenquelle, die mit dem Treiber durch eine Software wie dem ODBCDatenquellen-Administrator assoziiert wurde
ODBC-Datenquellen können innerhalb bestimmter Datenbankprogramme aufgesetzt werden. Wenn z. B. in Lotus Approach eine neue Datenbankdatei erzeugt wird, hat man die Option, sie mit einem ODBC-Treiber zu assoziieren. Eine ODBC-Datenquelle muss einen kurzen, beschreibenden Namen erhalten. Dieser Name wird in Java-Programmen benutzt, wenn eine Verbindung zu der Datenbank hergestellt wird, auf die sich die Quelle bezieht. Wenn unter Windows ein ODBC-Treiber ausgewählt und eine Datenbank erzeugt wurde, erscheinen sie im ODBC-Datenquellen-Administrator. Abbildung 20.2 zeigt beispielsweise eine Datenquelle namens WorldEnergy. Abbildung 20.2 zeigt, dass die Datenquelle WorldEnergy mit einem Microsoft-Access-Treiber assoziiert ist. Microsoft Access hat ODBC-Treiber, mit denen man eine Verbindung mit einer Access-Datenbankdatei herstellen kann. Die meisten Windows-Datenbankprogramme enthalten einen oder mehrere ODBC-Treiber, die diesem Format entsprechen.
591
Daten mit JDBC lesen und schreiben
Abbildung 20.2: Eine Auflistung von Datenquellen im ODBCDatenquellen-Administrator
Eine Verbindung mit einer ODBC-Datenquelle herstellen Unser erstes Projekt heute ist eine Java-Applikation, die mit der JDBC-ODBC-Brücke eine Verbindung zu einer Microsoft-Access-Datei herstellt. Die Access-Datei für dieses Projekt heißt world20.mdb und ist eine Datenbank mit weltweiten Energiestatistiken, die von der U.S. Energy Information Administration publiziert wird. Die Kohletabelle dieser Datenbank hat folgende Felder:
Country
Year
Anthracite Production
Diese Datenbank finden Sie auf der Webseite zum Buch unter http://www.java21pro.com. Um diese Datenbank verwenden zu können, ist ein ODBC-Treiber mit Microsoft-AccessUnterstützung auf Ihrem System erforderlich. Mithilfe des ODBC-Datenquellen-Administrators (oder eines vergleichbaren Programms, wenn Sie kein Windows-System benutzen) müssen Sie eine neue ODBC-Datenquelle erstellen, die mit world20.mdb assoziiert ist. Je nachdem, welche ODBC-Treiber auf Ihrem System vorhanden sind, können weitere Einrichtungsarbeiten nötig sein. Konsultieren Sie dazu die Dokumentation des ODBCTreibers. Eine Microsoft-Access-Datenbank auf Windows mittels ODBC zu benutzen, ist recht einfach. Allerdings kann es bei anderen Datenbankformaten oder Systemen vorkommen, dass Sie erst einen ODBC-Treiber installieren und sich über dessen Verwendung informieren müssen, bevor Sie eine JDBC-ODBC-Applikation erstellen können.
592
Java Database Connectivity
Nachdem Sie world20.mdb heruntergeladen oder eine andere Datenbank gefunden haben, die mit den ODBC-Treibern Ihres Systems kompatibel ist, müssen Sie als letzten vorbereitenden Schritt eine Datenquelle erstellen, die mit der Datenbank assoziiert ist. Im Gegensatz zu andern I/O-Klassen von Java verwendet JDBC keinen Dateinamen, um ein Datenfile zu identifizieren. Stattdessen wird ein Tool wie der ODBC-Datenquellen-Administrator benutzt, um die ODBC-Datenquelle zu benennen und den Dateiordner anzugeben, in dem sie gefunden werden kann. Im ODBC-Datenquellen-Administrator klicken Sie auf die Registerkarte »Benutzer-DSN«, um eine Liste der derzeit verfügbaren Datenquellen zu erhalten. Um eine neue Datenquelle hinzuzufügen, die mit world20.mdb (oder Ihrer eigenen Datenbank) assoziiert ist, klicken Sie auf »Hinzufügen ...«, wählen einen ODBC-Treiber und klicken dann auf »Fertigstellen«. Es öffnet sich ein Setup-Fenster, in dem Sie einen Namen, eine kurze Beschreibung und andere Informationen zu dieser Datenbank angeben können. Klicken Sie auf »Auswählen ...«, um die Datenbankdatei zu suchen und auszuwählen. Abbildung 20.3 zeigt das Setup-Fenster, mit dem world20.mdb als Datenquelle im ODBCDatenquellen-Administrator eingerichtet wird. Nachdem eine Datenbank mit einer ODBC-Datenquelle assoziiert wurde, ist es einfach, mit ihr in einem Java-Programm zu arbeiten, wenn man sich mit SQL auskennt. Die erste Aufgabe in einem JDBC-Programm ist, die Treiber zu laden, mit denen die Verbindung zur Datenquelle hergestellt wird. Ein Treiber wird mit der Class.forName(String)-Methode geladen. Class, Teil des Pakets java.lang, lädt Klassen in den JavaInterpreter. Die forName(String)-Methode lädt die Klasse, die durch den String angegeben wird. Diese Methode kann eine ClassNotFoundException auswerfen.
Abbildung 20.3: Das Fenster zur Treibereinrichtung
Alle Programme, die eine ODBC-Datenquelle benutzen, verwenden sun.jdbc.odbc.JdbcOdbcDriver, der JDBC-ODBC-Brücken-Treiber von Java 2. Um diese Klasse in einen Java-Interpreter zu laden, isat folgende Anweisung erforderlich: Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
593
Daten mit JDBC lesen und schreiben
Wenn der Treiber geladen ist, können Sie mit der DriverManager-Klasse im Paket java.sql eine Verbindung zur Datenquelle herstellen: Die Methode getConnection(String, String, String) von DriverManager kann benutzt werden, um die Verbindung einzurichten. Sie gibt ein Connection-Objekt zurück, das eine aktive Datenverbindung repräsentiert. Die drei Argumente dieser Methode sind:
ein Name, der die Datenquelle und die Art der Datenbank-Connectivity repräsentiert
ein Benutzername
ein Passwort
Die letzten beiden Punkte sind nur dann erforderlich, wenn die Datenbank mit einem Benutzernamen und einem Passwort geschützt ist. Andernfalls können diese Argumente leere Strings sein (""). Dem Namen der Datenquelle geht der Text jdbc:odbc: voraus, wenn die JDBC-ODBCBrücke benutzt wird. So wird die Art der benutzten Datenbank-Connectivity angegeben. Mit der folgenden Anweisung wird eine Verbindung zu einer Datenquelle namens Payroll mit dem Benutzernamen Doc und dem Passwort 1rover1 hergestellt: Connection payday = DriverManager.getConnection( "jdbc:odbc:Payroll", "Doc", "1rover1");
Die getConnection()-Methode wirft wie alle Methoden einer Datenquelle eine SQLException aus, wenn während der Verwendung der Datenquelle etwas schief läuft. SQL hat eigene Fehlermeldungen, die als Teil des SQLException-Objekts weitergeleitet werden.
20.2 Daten aus einer Datenbank mittels SQL auslesen Eine SQL-Anweisung wird unter Java als Statement-Objekt repräsentiert. Statement ist eine Schnittstelle, kann also nicht direkt abgeleitet werden. Es wird von der createStatement()Methode eines Connection-Objekts zurückgegeben: Statement lookSee = payday.CreateStatement();
Das Statement-Objekt, das diese Methode zurückgibt, ist ein Objekt, das die Schnittstelle implementiert. Wenn Sie ein Statement-Objekt haben, können Sie damit eine SQL-Anfrage durchführen, indem Sie die executeQuery(String)-Methode des Objekts aufrufen. Das String-Argument ist eine SQL-Anfrage in der Syntax dieser Sprache.
594
Daten aus einer Datenbank mittels SQL auslesen
Dieses Kapitel kann keine angemessene Darstellung von SQL bieten. SQL ist eine umfassende Sprache zum Auslesen und Speichern von Daten, über die es umfangreiche Bücher gibt. Um komplizierte Dinge mit SQL zu machen, müssen Sie SQL lernen, aber viel von dieser Sprache erfahren Sie bereits, wenn Sie sich ein paar Beispiele näher ansehen. Dies ist ein Beispiel für eine SQL-Anfrage, die man bei der Kohletabelle von world20.mdb benutzen könnte: SELECT Country, Year, 'Anthracite Production' FROM Coal WHERE (Country Is Not Null) ORDER BY Year
Diese SQL-Anfrage holt aus der Datenbank bestimmte Felder aller Einträge, in denen das Country-Feld ungleich null ist. Die zurückgegebenen Einträge werden nach dem CountryFeld sortiert, sodass Afghanistan vor Burkina Faso stünde. Die folgende Java-Anweisung führt diese Anfrage mittels eines Statement-Objekts namens looksee aus: ResultSet set = looksee.executeQuery( "SELECT Country, Year, ‘Anthracite Production’ FROM Coal " + "WHERE (Country Is Not Null) ORDER BY Year");
Wenn die SQL-Anfrage richtig formuliert wurde, gibt die executeQuery()-Methode ein ResultSet-Objekt zurück, in dem sich alle aus der Datenquelle entnommenen Einträge befinden. Wenn Sie einer Datenbank Einträge hinzufügen wollen, muss die executeUpdate()-Methode des Statements aufgerufen werden. Wir kommen darauf noch zurück. Wenn executeQuery() ein ResultSet zurückgibt, ist es auf den ersten erhaltenen Eintrag positioniert. Mit den folgenden Methoden von ResultSet lassen sich Informationen aus dem aktuellen Eintrag ziehen:
getDate(String) – gibt den Date-Wert zurück, der im angegebenen Feld gespeichert ist. getDouble(String) – gibt den double-Wert zurück, der im angegebenen Feld gespei-
chert ist.
getFloat(String) - gibt den float-Wert zurück, der im angegebenen Feld gespeichert ist.
getInt(String) – gibt den int-Wert zurück, der im angegebenen Feld gespeichert ist.
getLong(String) – gibt den long-Wert zurück, der im angegebenen Feld gespeichert ist.
getString(String) – gibt den String zurück, der im angegebenen Feld gespeichert ist.
Das sind nur die einfachsten Methoden der Schnittstelle ResultSet. Welche Methoden Sie verwenden müssen, hängt von der Form ab, die die Felddaten in der Datenbank haben.
595
Daten mit JDBC lesen und schreiben
Allerdings können einige Methoden wie getString() und getInt() flexibler beim Auslesen von Informationen aus einem Eintrag sein. Sie können auch einen Integer statt eines Strings als Argument für diese Methoden benutzen, z. B. getString(5). Der Integer gibt an, welches Feld ausgelesen werden soll (1 für das erste Feld, 2 für das zweite Feld usw.). Wenn es zu einem Datenbankfehler kommt, während Sie Informationen aus einem ResultSet auslesen wollen, wird eine SQLException ausgeworfen. Sie können mehr über den Fehler erfahren, wenn Sie die getSQLState()- und die getErrorCode()-Methoden die-
ser Ausnahme aufrufen. Nachdem Sie die benötigten Informationen aus einem Eintrag gezogen haben, können Sie zum nächsten Eintrag gehen, indem Sie die next()-Methode des ResultSet-Objekts aufrufen. Diese Methode gibt den booleschen Wert false zurück, wenn man versucht, über das Ende eines ResultSet zu gehen. Darüber hinaus kann man sich mit folgenden Methoden durch die Einträge eines ResultSet bewegen:
afterLast() – bewegt sich auf die Stelle unmittelbar nach dem letzten Eintrag des Sets.
beforeFirst() – bewegt sich auf die Stelle unmittelbar vor dem ersten Eintrag des Sets.
first() – bewegt sich zum ersten Eintrag des Sets.
last() – bewegt sich zum letzten Eintrag des Sets.
previous() – bewegt sich zum vorherigen Eintrag des Sets.
Mit der Ausnahme von afterLast() und beforeFirst() geben diese Methoden ein boolesches false zurück, wenn an dieser Position des Sets kein Eintrag steht. Wenn Sie die Verbindung zu einer Datenquelle beenden möchten, schließen Sie sie mit einem argumentenlosen Aufruf der close()-Methode der Verbindung. Listing 20.1 zeigt die Applikation CoalTotals, die die JDBC-ODBC-Brücke und eine SQLAnweisung verwendet, um einige Einträge aus einer Datenbank zu lesen. Vier Felder werden aus jedem Eintrag ausgelesen, den die SQL-Anweisung angibt: FIPS, Country, Year und Anthracite Production. Das Ergebnis-Set ist nach dem Year-Feld geordnet, und die Felder werden im Standardausgabegerät angezeigt. Listing 20.1: Der vollständige Quelltext von CoalTotals.java 1: import java.sql.*; 2: 3: public class CoalTotals { 4: public static void main(String[] arguments) { 5: String data = "jdbc:odbc:WorldEnergy";
596
Daten aus einer Datenbank mittels SQL auslesen
6: try { 7: Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); 8: Connection conn = DriverManager.getConnection( 9: data, "", ""); 10: Statement st = conn.createStatement(); 11: ResultSet rec = st.executeQuery( 12: "SELECT * " + 13: "FROM Coal " + 14: "WHERE " + 15: "(Country=’" + arguments[0] + "‘) " + 16: "ORDER BY Year"); 17: System.out.println("FIPS\tCOUNTRY\t\tYEAR\t" + 18: "ANTHRACITE PRODUCTION"); 19: while(rec.next()) { 20: System.out.println(rec.getString(1) + "\t" 21: + rec.getString(2) + "\t\t" 22: + rec.getString(3) + "\t" 23: + rec.getString(4)); 24: } 25: st.close(); 26: } catch (SQLException s) { 27: System.out.println("SQL Error: " + s.toString() + " " 28: + s.getErrorCode() + " " + s.getSQLState()); 29: } catch (Exception e) { 30: System.out.println("Error: " + e.toString() 31: + e.getMessage()); 32: } 33: }
Das Programm wird mit einem einzelnen Argument ausgeführt, das das Country-Feld in der Datenbankform angibt, aus dem die Einträge zu lesen sind. Wenn man die Applikation mit dem Argument Poland ausführt, sieht der Output der Beispieldatenbank folgendermaßen aus: FIPS PL PL PL PL PL PL PL PL PL
COUNTRY Poland Poland Poland Poland Poland Poland Poland Poland Poland
YEAR 1990 1991 1992 1993 1994 1995 1996 1997 1998
ANTHRACITE PRODUCTION 0.0 0.0 0.0 174.165194805424 242.50849909616 304.237935229728 308.64718066784 319.67029426312 319.67029426312
Starten Sie das Programm mit anderen Ländern, die Anthrazit herstellen, also z. B. Frankreich (France), Swasiland (Swaziland) oder Neuseeland (New Zealand). Bei Ländern mit einem Leerzeichen im Namen dürfen Sie die Anführungszeichen um den Namen in der Kommandozeile nicht vergessen.
597
Daten mit JDBC lesen und schreiben
20.3 Daten in eine Datenbank mittels SQL schreiben In der Applikation CoalTotals haben Sie mit einer SQL-Anweisung Daten aus einer Datenbank gelesen. Diese SQL-Anweisung wurde als String vorbereitet: SELECT * FROM Coal WHERE (Country=’Swaziland’) ORDER BY YEAR
Das ist eine übliche Weise der SQL-Verwendung. Man könnte ein Programm schreiben, das den Benutzer zur Eingabe einer SQL-Anfrage auffordert und das dann das Ergebnis anzeigt (allerdings wäre das keine sehr schlaue Idee – man kann mit SQL-Anfragen Einträge, Tabellen, ja ganze Datenbanken löschen). Das Paket java.sql unterstützt eine andere Weise, eine SQL-Anweisung zu erzeugen: Prepared Statements (vorkompilierte Anweisungen). Ein Prepared Statement wird von der Klasse PreparedStatement repräsentiert und ist eine SQL-Anweisung, die vor der Ausführung kompiliert wird. So kann die Anweisung die Daten schneller zurückgeben und ist damit die bessere Wahl, wenn Sie eine SQL-Anweisung mehrfach im selben Programm ausführen wollen. Prepared Statements haben auf Windows-Systemen noch einen anderen Vorteil: Sie können damit mit dem JDBC-ODBC-Treiber Daten in eine MicrosoftAccess-Datenbank schreiben. Seit Jahren will es mir nicht gelingen, unter Java mit Anweisungen Daten in eine Access-Datenbank zu schreiben, bei Prepared Statements gibt es jedoch keinerlei Probleme. Ich habe keine Ahnung, warum das so ist. Vielleicht schreibt ja irgendwann einmal jemand ein Buch: »Warum hasst Microsoft Access meine unkompilierten SQL-Anweisungen in 21 Tagen«. Um ein Prepared Statement zu erzeugen, rufen Sie die prepareStatement(String)Methode der Verbindung auf, wobei der String die Struktur der SQL-Anweisung angibt. Um die Struktur anzugeben, schreiben Sie eine SQL-Anweisung, in der Parameter durch Fragezeichen ersetzt werden. Bei einem Verbindungsobjekt namens cc könnte das wie folgt aussehen: PreparedStatement ps = cc.prepareStatement( "SELECT * FROM Coal WHERE (Country=’?’) ORDER BY YEAR");
Ein weiteres Beispiel, diesmal mit mehr als einem Fragezeichen: PreparedStatement ps = cc.prepareStatement( "INSERT INTO BOOKDATA VALUES(?, ?, ?, ?, ?, ?, ?)");
Die Fragezeichen in diesen SQL-Anweisungen sind Platzhalter für Daten. Bevor Sie die Anweisung ausführen können, müssen Sie an diese Stellen Daten setzen. Dies geschieht mit den Methoden der PreparedStatement-Klasse.
598
Daten in eine Datenbank mittels SQL schreiben
Um Daten in ein Prepared Statement zu setzen, rufen Sie eine Methode mit der Position des Platzhalters und dem einzufügenden Wert auf. Um den String "Swaziland" in das erste Prepared Statement einzufügen, rufen Sie die Methode setString(int, String) auf: ps.setString(1, "Swaziland");
Das erste Argument gibt die Position des Platzhalters an, gezählt von links nach rechts, d. h., das erste Fragezeichen ist 1, das zweite 2 usw. Das zweite Argument ist der Wert, der an diese Stelle der Anweisung eingefügt werden soll. Es gibt folgende Methoden:
setAsciiStream(int, InputStream, int) – Das erste Argument gibt die Position an, an der der spezifizierte InputStream (der einen Stream von ASCII-Zeichen repräsentiert) eingesetzt wird. Das dritte Argument bestimmt, wie viele Zeichen des InputStream ein-
gefügt werden sollen.
setBinaryStream(int, InputStream, int) – Das erste Argument gibt die Position an, an der der spezifizierte InputStream (der einen Bytestream repräsentiert) eingesetzt wird. Das dritte Argument bestimmt, wie viele Bytes des InputStream eingefügt werden sollen.
setCharacterStream(int, Reader, int) – Das erste Argument gibt die Position an, an der der spezifizierte Reader (der einen Zeichenstream repräsentiert) eingesetzt wird.
Das dritte Argument bestimmt, wie viele Zeichen des Streams eingefügt werden sollen.
setBoolean(int, boolean) – Fügt einen boolean-Wert an der Stelle ein, die der Integer
bestimmt.
setByte(int, byte) – Fügt einen byte-Wert an der angegebenen Stelle ein.
setBytes(int, byte[]) – Füge ein Byte-Array an der angegebenen Stelle ein.
setDate(int, Date) – Fügt ein Date-Objekt (aus dem Paket java.sql) an der
angegebenen Stelle ein.
setDouble(int, double) – Fügt einen double-Wert an der angegebenen Stelle ein.
setFloat(int, float) – Fügt einen float-Wert an der angegebenen Stelle ein.
setInt(int, int) – Fügt einen int-Wert an der angegebenen Stelle ein.
setLong(int, long) – Fügt einen long-Wert an der angegebenen Stelle ein.
setShort(int, short) – Fügt einen short-Wert an der angegebenen Stelle ein.
setString(int, String) – Fügt einen String-Wert an der angegebenen Stelle ein.
Zudem gibt es eine setNull(int, int)-Methode, die die SQL-Version eines Nullwerts (eines leeren Werts) an der Position speichert, die durch das erste Argument angegeben ist.
599
Daten mit JDBC lesen und schreiben
Das zweite Argument für setNull() ist eine Klassenvariable der Types-Klasse in java.sql, die angibt, welche Art von SQL-Wert an diese Stelle gehört. Es gibt Klassenvariablen für alle SQL-Datentypen. Die folgende, unvollständige Liste umfasst die wichtigsten Variablen: BIGINT, BIT, CHAR, DATE, DECIMAL, DOUBLE, FLOAT, INTEGER, SMALLINT, TINYINT und VARCHAR. Der folgende Code setzt einen leeren CHAR-Wert an die fünfte Stelle eines Prepared Statement namens ps: ps.setNull(5, Types.CHAR);
Das nächste Projekt demonstriert Ihnen, wie man Börsenkurse mittels eines Prepared Statements in eine Datenbank schreibt. Als Service für Börsianer bietet die Website Yahoo einen Link »Download Spreadsheet« auf der Aktienkurs-Hauptseite der einzelnen Aktienkürzel. Um diesen Link zu sehen, schauen Sie entweder einen Aktienkurs auf der Yahoo-Website an oder gehen direkt auf: http://quote.yahoo.com/q?s=msft&d=v1
Unter der Tabelle mit den Kursen und den Volumina finden Sie den Link »Download Spreadsheet«. Bei Microsoft sieht dies z. B. folgendermaßen aus: http://quote.yahoo.com/d/quotes.csv?s=MSFT&f=sl1d1t1c1ohgv&e=.csv
Sie können den Link anklicken, um die Datei zu öffnen oder um sie auf Ihrem System zu speichern. Die Datei, die nur eine Zeile umfasst, enthält die Kurse und Volumina der Aktie beim letzten Marktschluss. So sahen die Daten von Microsoft am 14. Oktober 2002 aus: "MSFT",49.21,"10/14/2002","3:58pm",+0.34,48.25,49.29,47.84,36067756
Die Felder dieser Datei sind das Aktienkürzel, der Schlusskurs, Datum, Uhrzeit, Differenz zum Vortagesschlusskurs, Tagestiefststand, Tageshöchststand, Eröffnungskurs und Volumen. Die Applikation QuoteData verwendet alle diese Felder mit Ausnahme des Feldes »Zeit«. Da es sich hier stets um den Zeitpunkt des Markschlusses handelt, ist dieses Feld nicht sonderlich nützlich. Folgendes geschieht im Programm:
Ein Aktienkürzel wird als Kommandozeilenargument übernommen.
Ein QuoteData-Objekt wird mit dem Aktienkürzel als Instanzvariable ticker erzeugt.
Die retrieveQuote()-Methode des Objekts wird aufgerufen, um die Aktiendaten von Yahoo herunterzuladen und sie als String zurückzugeben.
600
Daten in eine Datenbank mittels SQL schreiben
Die storyQuote()-Methode des Objekts wird mit dem String als Argument aufgerufen. Sie speichert die Aktiendaten mittels einer JDBC-ODBC-Verbindung in einer Datenbank.
Für Letzteres ist eine Aktienkursdatenbank notwendig, die mittels JDBC-ODBC erreichbar ist und die diese Daten sammeln wird. Windows-Benutzer können auf der Website zum Buch quotedata.mdb herunterladen, eine Microsoft-Access-2000-Datenbank. Gehen Sie auf http://www.java21pro.com, und öffnen Sie die Seite zu Tag 20. Nachdem Sie die Datenbank heruntergeladen (oder eine eigene erstellt) haben, erstellen Sie mit dem ODBC-Datenquellen-Administrator eine neue Datenquelle, die mit der Datenbank assoziiert ist. Die Applikation geht davon aus, dass der Name der Quelle QuoteData ist. Geben Sie den Text aus Listing 20.2 ein, und speichern Sie die Datei als QuoteData.java. Listing 20.2: Der vollständige Quelltext von QuoteData.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:
import import import import
java.io.*; java.net.*; java.sql.*; java.util.*;
public class QuoteData { private String ticker; public QuoteData(String inTicker) { ticker = inTicker; } private String retrieveQuote() { StringBuffer buf = new StringBuffer(); try { URL page = new URL("http://quote.yahoo.com/d/quotes.csv?s=" + ➥ticker + "&f=sl1d1t1c1ohgv&e=.csv"); String line; URLConnection conn = page.openConnection(); conn.connect(); InputStreamReader in= new InputStreamReader (conn.getInputStream()); BufferedReader data = new BufferedReader(in); while ((line = data.readLine()) != null) { buf.append(line + "\n"); } } catch (MalformedURLException mue) { System.out.println("Bad URL: " + mue.getMessage()); } catch (IOException ioe) {
601
Daten mit JDBC lesen und schreiben
29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74:
602
System.out.println("IO Error:" + ioe.getMessage()); } return buf.toString(); } private void storeQuote(String data) { StringTokenizer tokens = new StringTokenizer(data, ","); String[] fields = new String[9]; for (int i = 0; i < fields.length; i++) { fields[i] = stripQuotes(tokens.nextToken()); } String datasource = "jdbc:odbc:QuoteData"; try { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection conn = DriverManager.getConnection( datasource, "", ""); PreparedStatement pstmt = conn.prepareStatement( "INSERT INTO Stocks VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); pstmt.setString(1, fields[0]); pstmt.setString(2, fields[1]); pstmt.setString(3, fields[2]); pstmt.setString(4, fields[4]); pstmt.setString(5, fields[5]); pstmt.setString(6, fields[6]); pstmt.setString(7, fields[7]); pstmt.setString(8, fields[8]); pstmt.executeUpdate(); conn.close(); } catch (SQLException sqe) { System.out.println("SQL Error: " + sqe.getMessage()); } catch (ClassNotFoundException cnfe) { System.out.println(cnfe.getMessage()); } } private String stripQuotes(String input) { StringBuffer output = new StringBuffer(); for (int i = 0; i < input.length(); i++) { if (input.charAt(i) != ‘\"’) { output.append(input.charAt(i)); } } return output.toString(); } public static void main(String[] arguments) {
Daten in eine Datenbank mittels SQL schreiben
75: 76: 77: 78: 79: 80: 81: 82: 83: }
if (arguments.length < 1) { System.out.println("Usage: java QuoteData tickerSymbol"); System.exit(0); } QuoteData qd = new QuoteData(arguments[0]); String data = qd.retrieveQuote(); qd.storeQuote(data); }
Kompilieren Sie die Applikation QuoteData, stellen Sie eine Verbindung zum Internet her, und führen Sie das Programm aus. Denken Sie daran, ein gültiges Aktienkürzel als Kommandozeilenargument anzugeben. Um den aktuellen Kurs von MSFT (Microsoft) zu laden, müsste man Folgendes eingeben: java QuoteData MSFT
Die Methode retrieveQuote()(Zeile 13–32) lädt die Aktiendaten bei Yahoo herunter und speichert sie als String. Die Techniken, die in dieser Methode zum Einsatz kommen, wurden an Tag 17 besprochen. Die Methode storeQuote() (Zeile 34–62) verwendet SQL-Techniken, die in diesem Kapitel besprochen wurden. Die Methode zerlegt zunächst die Kursdaten in eine Gruppe von String-Tokens, wobei das Komma als Trenner zwischen den einzelnen Tokens dient. Die Tokens werden in einem String-Array mit neun Elementen gespeichert. Das Array enthält dieselben Felder wie die Yahoo-Daten in derselben Reihenfolge: Aktienkürzel, Schlusskurs, Datum, Zeit, Preisdifferenz, Tagestief, Tageshoch, Eröffnungskurs und Volumen. In den Zeilen 40–44 wird eine Datenverbindung zur QuoteData-Datenquelle mithilfe des JDBC-ODBC-Treibers hergestellt. Diese Verbindung wird dann in den Zeilen 45–46 verwendet, um ein Prepared Statement zu erzeugen. Diese Anweisung verwendet INSERT INTO SQL, was das Speichern von Daten in der Datenbank veranlasst. Im vorliegenden Fall ist die Datenbank quotedata.mdb, und die Anweisung INSERT INFO bezieht sich auf die Tabelle Stocks in dieser Datenbank. Das Prepared Statement hat acht Platzhalter. Es sind nur acht (nicht neun) notwendig, weil die Applikation das Feld »Zeit« der Yahoo-Daten nicht verwendet. In den Zeilen 47–54 setzt eine Reihe von setString()-Methoden die Elemente des StringArray in das Prepared Statement, und zwar in derselben Reihenfolge wie die Felder in der Datenbank stehen: Aktienkürzel, Schlusskurs, Datum, Preisdifferenz, Tagestief, Tageshoch, Eröffnungskurs und Volumen.
603
Daten mit JDBC lesen und schreiben
Einige Felder der Yahoo-Daten sind Datumsangaben, Fließkommazahlen und Integer, sodass Sie vielleicht denken, es sei besser, setDate(), setFloat() und setInt() zu verwenden. Doch leider unterstützt Microsoft Access 2000 einige dieser Methoden nicht. Da hilft es auch nichts, dass diese Methoden unter Java existieren. Wenn Sie versuchen, eine nicht unterstützte Methode zu verwenden, erhalten Sie eine SQLException. Es ist einfacher, Strings an Access zu schicken und die Datenbank die Konvertierung ins richtige Format vornehmen zu lassen. Das kann durchaus auch bei anderen Datenbanken der Fall sein. Der Umfang der SQL-Unterstützung variiert je nach Produkt und verwendetem ODBC-Treiber. Nach der Vorbereitung des Prepared Statement und dem Auffüllen der Platzhalter wird in Zeile 55 die executeUpdate()-Methode der Anweisung aufgerufen. Damit werden entweder die Aktiendaten in der Datenbank abgelegt, oder es führt zu einem SQL-Fehler. Wenn Sie absichtlich einen Fehler verursachen wollen, rufen Sie das Programm am selben Tag zweimal mit dem gleichen Aktienkürzel auf – quotedata.mdb erlaubt nicht, dass Aktieneinträge für denselben Tag dupliziert werden. Die private Methode stripQuotes() wird verwendet, um die Anführungszeichen um die Aktiendaten von Yahoo zu entfernen. Diese Methode wird in Zeile 38 aufgerufen und kümmert sich um die drei Felder mit überflüssigen Anführungszeichen: Aktienkürzel, Datum und Zeit.
20.4 JDBC-Treiber Die Erstellung eines Programms, das einen JDBC-Treiber benutzt, ähnelt weitgehend der Erstellung eines Programms, das die JDBC-ODBC-Brücke verwendet. Als Erstes müssen Sie einen JDBC-Treiber installieren. Sun liefert keinen JDBC-Treiber bei Java 2 mit, aber viele Firmen verkaufen diese Treiber oder bündeln sie mit anderen Produkten, wie z. B. Informix, Oracle, Symantec, IBM und Sybase. Eine Liste der derzeit verfügbaren JDBC-Treiber finden Sie auf der JDBC-Seite von Sun unter http:// java.sun.com/products/jdbc/jdbc.drivers.html. Einige Treiber kann man zum Testen herunterladen. Einen davon, den JDataConnect Server von NetDirect, können Sie für das heutige Projekt verwenden. Sie können diesen Treiber testweise unter http://www.j-netdirect.com herunterladen. Seit August 2002 steht bei MySQL der MySQL Connector zur Verfügung, ein kostenloser Open-Source-JDBC-Treiber, den Mark Matthews entwickelt hat. Sie können diesen Typ-IV-Treiber unter http://www.mysql.com/downloads/apijdbc.html herunterladen. Auf dieser Webseite finden Sie auch weiterführende Informationen zu diesem Treiber.
604
JDBC-Treiber
Die Schritte zum Aufsetzen einer Datenquelle für JDBC sind dieselben wie bei der JDBCODBC-Brücke:
Erzeugung der Datenbank
Assoziation der Datenbank mit dem JDBC-Treiber
Erzeugung einer Datenquelle – hier kann es erforderlich sein, ein Datenbankformat, einen Datenbankserver, einen Benutzernamen und ein Passwort zu wählen.
Der JDataConnect Server von NetDirect verwendet den ODBC-Datenquellen-Administrator, um eine neue Datenquelle zu erzeugen, die mit einer Datenbank assoziiert ist. Unter Windows NT, 2000 und XP müssen Sie die Datenquelle mit der SystemDSN-Registerkarte, nicht der Benutzer-DSN-Registerkarte, einrichten. Dies ist erforderlich, weil der JDataConnect Server sich mit einer Datenbank außerhalb Ihres Benutzerkontos verbindet und deshalb allen Benutzern des Computers zur Verfügung stehen muss. Listing 20.3 ist eine Java-Applikation, die den JDataConnect-JDBC-Treiber benutzt, um auf eine Datenbankdatei namens People.mdb zuzugreifen. Diese Datenbank ist ein Microsoft-Access-File mit den Adressen von amerikanischen Präsidenten. Listing 20.3: Der vollständige Quelltext von Presidents.java 1: import java.sql.*; 2: 3: public class Presidents { 4: public static void main(String[] arguments) { 5: String data = "jdbc:JDataConnect://127.0.0.1/Presidents"; 6: try { 7: Class.forName("JData2_0.sql.$Driver"); 8: Connection conn = DriverManager.getConnection( 9: data, "", ""); 10: Statement st = conn.createStatement(); 11: ResultSet rec = st.executeQuery( 12: "SELECT * FROM Contacts ORDER BY NAME"); 13: while(rec.next()) { 14: System.out.println(rec.getString("NAME") + "\n" 15: + rec.getString("ADDRESS1") + "\n" 16: + rec.getString("ADDRESS2") + "\n" 17: + rec.getString("PHONE") + "\n" 18: + rec.getString("E-MAIL") + "\n"); 19: } 20: st.close(); 21: } catch (Exception e) {
605
Daten mit JDBC lesen und schreiben
22: 23: 24: 25: }
System.out.println("Error -- " + e.toString()); } }
Damit dieses Programm funktioniert, muss zuerst der JDataConnect Server gestartet werden (es sei denn, er läuft bereits – unter Windows NT oder XP wird er als Dienst während der JDataConnect-Installation festgelegt und muss deswegen nicht mehr manuell gestartet werden). Die Referenz auf 127.0.0.1 in Zeile 6 bezieht sich auf den Server – 127.0.0.1 ist ein Platzhalter für den Namen Ihres Computers. Der JDataConnect Server kann dazu verwendet werden, um auf Internetserver zuzugreifen. Man kann also 127.0.0.1 durch eine Internetadresse ersetzen, z. B. durch db.naviseek.com:1150, wenn ein JDataConnect Server an diesem Ort und auf diesem Port laufen würde. Zeile 5 erzeugt die Datenbankadresse, die verwendet wird, wenn ein Connection-Objekt erzeugt wird, das die Verbindung zur Datenquelle Presidents herstellt. Diese Adresse umfasst mehr Informationen als die beim JDBC-ODBC-Brücken-Server: jdbc:JDataConnect://127.0.0.1/Presidents
Zeile 7 der Applikation Presidents lädt den JDBC-Treiber, der Teil des JDataConnect Servers ist: JData2_0.sql.$Driver
Die Firma, die den JDBC-Treiber liefert, stellt Ihnen die Konfigurationsinformationen für die Datenquelle und den Treiber zur Verfügung. Die Datenbankadresse kann sich bei verschiedenen JDBC-Treiberimplementierungen stark unterscheiden. Es sollte aber stets eine Referenz zu einem Server, ein Datenbankformat und den Namen der Datenquelle geben. Falls die People.mdb-Datenbank existiert, die Datenbank mit einer ODBC-Datenquelle assoziiert und der JDBC-Treiber korrekt eingerichtet wurde, sollte die Ausgabe der PresidentsApplikation ungefähr wie folgt aussehen (abhängig von den Einträgen in der Datenbank): Gerald Ford Box 927 Rancho Mirage, CA 92270 (734) 741-2218 [email protected] Jimmy Carter Carter Presidential Center 1 Copenhill, Atlanta, GA 30307 (404) 727-7611 [email protected]
606
Zusammenfassung
Ronald Reagan 11000 Wilshire Blvd. Los Angeles, CA 90024 [email protected] George Bush Box 79798 Houston, TX 77279 (409) 260-9552 Bill Clinton 15 Old House Lane Chappaqua, NY 10514 (501) 370-8000 [email protected] George W. Bush White House, 1600 Pennsylvania Ave. Washington, DC 20500 (202) 456-1414 [email protected]
20.5 Zusammenfassung Heute haben Sie gelernt, wie man mit Daten arbeitet, die in verbreiteten Datenbankformaten wie Microsoft Access, MySQL oder xBASE gespeichert sind. Indem Sie entweder Java Database Connectivity (JDBC) oder eine Kombination aus JDBC und ODBC benutzen, können Sie existente Datenspeicherlösungen in Ihre Java-Programme einbauen. Sie können verschiedene relationale Datenbanken in Ihre Java-Programme integrieren, indem Sie JDBC oder ODBC und SQL verwenden, eine Standardsprache, um in Datenbanken zu lesen und zu schreiben und um sie zu verwalten. Morgen sehen wir uns eine weitere Möglichkeit an, um Daten in Java-Programmen zu repräsentieren: die Extensible Markup Language (XML).
607
Daten mit JDBC lesen und schreiben
20.6 Workshop Fragen und Antworten F
Kann der JDBC-ODBC-Brücken-Treiber in einem Applet benutzt werden? A
Die Standardsicherheitseinstellungen für Applets erlauben die Verwendung der JDBC-ODBC-Brücke nicht, da die ODBC-Seite der Brücke nativen Code, nicht Java verwendet. Nativer Code kann den für Java geltenden Sicherheitseinstellungen nicht unterworfen werden, sodass die Sicherheit des Codes nicht überprüft werden kann. JDBC-Treiber, die komplett in Java implementiert sind, können in Applets benutzt werden und haben zudem den Vorteil, dass keinerlei Konfiguration auf dem Client-Computer nötig ist.
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Was repräsentiert das Statement-Objekt in einem Datenbankprogramm? (a) eine Verbindung zu einer Datenbank (b) eine Datenbankanfrage in SQL (c) eine Datenquelle 2. Welche Art von Treiber ist nicht Teil von Java 2 SDK 1.4? (a) ein JDBC-Treiber (b) ein JDBC-ODBC-Treiber (c) keiner von beiden 3. Welche Java-Klasse repräsentiert SQL-Anweisungen, die vor ihrer Ausführung kompiliert werden? (a) Statement (b) PreparedStatement (c) ResultSet
608
Workshop
Antworten 1. b. Diese Klasse, Teil des Pakets java.sql, repräsentiert eine SQL-Anweisung. 2. a. Viele relationale Datenbankprogramme haben JDBC-Treiber, das SDK hat derzeit jedoch keinen eigenen. 3. b. Da ein PreparedStatement kompiliert wird, ist es die bessere Wahl, wenn man dieselbe SQL-Anfrage etliche Male wiederholen will.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Gegeben sei: public class ArrayClass { public static ArrayClass newInstance() { count++; return new ArrayClass(); } public static void main(String arguments[]) { new ArrayClass(); } int count = -1; }
Welche Zeile ist schuld daran, dass das Programm nicht kompiliert werden kann? a. count++; b. return new ArrayClass(); c. public static void main(String arguments[]) { d. int count = -1; Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 20, und klicken Sie auf den Link »Certification Practice«.
609
Daten mit JDBC lesen und schreiben
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Modifizieren Sie die Applikation CoalTotals so, dass Sie Felder aus der Tabelle mit der Öl- statt der Kohle-Gesamtförderung ziehen.
Erzeugen Sie eine Applikation mit einer grafischen Benutzeroberfläche, die Benutzereingaben annimmt und sie in einer relationalen Datenbank mittels ODBC oder JDBC speichert.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Website zum Buch: http://www.java21pro.com.
610
XML-Daten lesen und schreiben
1 2
XML-Daten lesen und schreiben
Eine der Hauptstärken von Java-Programmen besteht darin, dass sie auf verschiedenen Betriebssystemen unmodifiziert lauffähig sind. Diese Portabilität ist in der heutigen Computerlandschaft ein großer Pluspunkt, wo Windows, Linux, Mac OS und ein halbes Dutzend anderer Betriebssysteme sich einer weiten Verbreitung erfreuen und viele Programmierer mit mehreren Systemen arbeiten müssen. Rund 18 Monate nach Erscheinen von Java stand auch ein portabler Standard für Daten zur Verfügung: XML (Extensible Markup Language) steht. XML macht Daten vollständig portierbar. Sie können von verschiedenen Programmen auf verschiedenen Betriebssystemen geschrieben und/oder gelesen werden, ohne dass es zu Kompatibilitätsproblemen kommt. Heute sehen wir uns XML näher an und kommen dabei im Einzelnen auf folgende Themen zu sprechen:
Wie Daten als XML dargestellt werden
Warum XML so nützlich ist, um Daten zu speichern
Wie man XML benutzt, um ein neues Datenformat zu erzeugen
Wie man die Elemente eines neuen Formats dokumentiert
Wie man XML-Daten mithilfe von Java liest
21.1 XML verwenden XML steht für Extensible Markup Language und ist ein Format, um Daten zu speichern und zu organisieren, das von den Programmen, die mit den Daten arbeiten, unabhängig ist. In XML vorliegende Daten sind aus verschiedenen Gründen leichter wieder verwendbar: Erstens sind die Daten auf standardisierte Art und Weise strukturiert, sodass alle Programme darauf lesend und schreibend zugreifen können, solange sie XML unterstützen. Wenn Sie eine XML-Datei erstellen, die die Angestelltendatenbank Ihrer Firma darstellt, stehen Ihnen zahlreiche XML-Parser zur Verfügung, mit denen Sie die Datei lesen und verstehen können. Dies gilt unabhängig davon, welche Informationen Sie über Ihre Angestellten erfassen. Wenn Ihre Datenbank nur Name, ID-Nummer und das derzeitige Gehalt speichert, können die XML-Parser die Datenbank lesen. Wenn 25 weitere Angaben gespeichert werden, wie z. B. Geburtstag, Blutgruppe oder Haarfarbe, können Parser sie genauso gut lesen. Zweitens sind die Daten selbstdokumentierend, sodass man den Zweck einer Datei relativ leicht verstehen kann, indem man sie sich in einem Texteditor ansieht. Jeder, der Ihre XML-Angestelltendatenbank öffnet, sollte Struktur und Inhalt jedes Angestellteneintrags ohne Hilfe von Ihrer Seite verstehen können.
612
XML verwenden
Listing 21.1, das eine XML-Datei enthält, stellt dies dar. Listing 21.1: Der vollständige Quelltext von collection.librml 1: 2: 3: 4: 5: Joseph Heller 6: <Title>Catch-22 7: 09/1996 ➥ 8: Simon and Schuster 9: <Subject>Fiction 10: heller-catch22.html 11: 12: 13: Kurt Vonnegut 14: <Title>Slaughterhouse-Five 15: 12/1991 ➥PublicationDate> 16: Dell 17: <Subject>Fiction 18: 19:
Geben Sie diesen Text mit einer Textverarbeitung oder einem Editor ein, und speichern Sie ihn als Nur-Text mit dem Namen collection.librml. Alternativ können Sie die Datei auch auf der Website zum Buch http://www.java21pro.com auf der Seite zu Tag 21 herunterladen. Wissen Sie, was die Daten darstellen? Die Tags ?xml und !DOCTYPE am Anfang sind vielleicht unverständlich, der Rest ist jedoch zweifellos irgendeine Bücherdatenbank. Das Tag ?xml in der ersten Zeile der Datei hat ein Attribut namens version, das den Wert 1.0 hat. Jede XML-Datei muss mit einem solchen ?xml-Tag beginnen. Daten in XML sind von Tag-Elementen umgeben, die die Daten beschreiben. AnfangsTags beginnen mit dem Zeichen <, gefolgt vom Namen des Tags und einem >-Zeichen. End-Tags beginnen mit den Zeichen gefolgt vom Namen und dem Zeichen >. In Listing 28.1 ist z. B. in Zeile 12 ein Anfangs-Tag und in Zeile 18 ein End-Tag. Alles, was dazwischen steht, wird als Wert dieses Elements angesehen. Tags können in andere Tags verschachtelt werden, was eine Hierarchie von XML-Daten erzeugt, die Beziehungen innerhalb der Daten aufzeigt. In Listing 21.1 ist alles zwischen den Zeilen 13 und 17 verwandt – jedes Tag definiert etwas über dasselbe Buch.
613
XML-Daten lesen und schreiben
XML unterstützt auch Tag-Elemente, die durch ein einfaches Tag, und nicht durch ein Tag-Paar, definiert sind. Diese Tags beginnen mit dem Zeichen <, gefolgt vom Namen des Tags und den Zeichen />. Beispielsweise könnte es in der Buchdatenbank ein Tag geben, das angibt, dass das Buch vergriffen ist. Tag-Elemente können ferner Attribute haben, die aus zusätzlichen Daten bestehen, die die restlichen Daten ergänzen, die zum Tag gehören. Attribute werden mit einem AnfangsTag-Element definiert. Auf die Bezeichnung des Attributs folgt ein Gleichheitszeichen und Text in Anführungszeichen. In Zeile 7 von Listing 21.1 hat das Tag PublicationDate zwei Attribute: edition mit dem Wert "Trade" und isbn mit dem Wert "0684833395". XML fördert die Erstellung von Daten, die man selbst dann verstehen und benutzen kann, wenn dem Benutzer weder eine Dokumentation noch das Programm zur Verfügung steht, mit dem die Daten erstellt wurden. Indem XML wohlgeformte (well-formed, d. h. bestimmten Ansprüchen genügende) Auszeichnungen fordert, vereinfacht es die Aufgabe, Programme zu schreiben, die mit den Daten arbeiten sollen. Ein Hauptgrund für die Entwicklung von XML im Jahr 1996 waren die Inkonsequenzen bei HTML. Das ist eine sehr beliebte Weise, Daten Benutzern zu präsentieren, Browser wurden jedoch stets so entworfen, dass sie eine inkonsequente Verwendung von HTMLTags tolerierten. Webdesigner können zahllose Regeln für richtiges HTML (die vom World Wide Web Consortium definiert werden) brechen, und dennoch werden ihre Seiten von Browsern wie dem Netscape Navigator korrekt angezeigt. Millionen von Menschen stellen Internetseiten online, ohne sich sonderlich um korrektes HTML zu scheren. Sie testen ihre Seiten, um sicherzugehen, dass man sie in Browsern sehen kann, aber es ist ihnen völlig gleichgültig, ob sie gemäß aller HTML-Regeln strukturiert sind. Das World Wide Web Consortium wurde vom Web-Erfinder Tim Berners-Lee gegründet und ist die Gruppe, die HTML entwickelt hat und die Standardversion der Sprache definiert und fortentwickelt. Sollten Sie sich näher dafür interessieren, besuchen Sie die Website des Consortiums http://www.w3.org. Wenn Sie eine Webseite darauf überprüfen lassen wollen, ob sie allen Regeln von Standard-HTML entspricht, besuchen Sie http://validator.w3.org. Es gibt im WWW großen Bedarf nach Programmen, die Daten von Webseiten sammeln und mit den Diensten interagieren, die über das Internet angeboten werden. Man kann dabei beispielsweise an E-Commerce-Shoppingagenten denken, die Preise und Verfügbarkeit von verschiedenen Onlineshops anfordern und den Kunden einfache Preisvergleiche ermöglichen. Die Entwickler solcher Dienste bekamen die Inkonsequenzen schnell zu spüren, mit denen Webcontent unter HTML organisiert ist. Selbst dann, wenn Sie endlich ein Programm geschrieben haben, das sich durch die Auszeichnungs-Tags einer Seite kämpft, um Informationen auszulesen, können Sie wieder von vorne anfangen, wenn der Shop sein Design verändern sollte.
614
XML verwenden
Einen XML-Dialekt entwerfen Man XML zwar als Sprache und vergleicht es mit HTML, doch eigentlich geht es weit darüber hinaus. XML ist eine Auszeichnungssprache, die definiert, wie man eine Auszeichnungssprache definiert. Das ist eine merkwürdige Unterscheidung und klingt wie eine Definition aus einem Philosophiebuch. Dennoch ist es wichtig, dieses Konzept zu verstehen, weil es erklärt, wie man mit XML so unterschiedliche Daten wie Krankenkassenansprüche, Ahnentafeln, Zeitungsartikel oder Moleküle definieren kann. Das »X« in XML steht für extensible (erweiterbar) und bezieht sich darauf, dass man Daten für eigene Zwecke organisieren kann. Daten, die gemäß den Regeln von XML organisiert sind, können alles Mögliche repräsentieren:
Ein Programmierer bei einer Telemarketing-Firma kann XML benutzen, um Daten über die einzelnen Anrufe zu speichern, wobei Zeitpunkt des Anrufs, die Telefonnummer, der Operator und das Ergebnis erfasst werden.
Ein genervter Privatier kann XML benutzen, um die lästigen Telemarketing-Anrufe zu erfassen, wobei Zeitpunkt des Anrufs, Firma und angepriesenes Produkt gespeichert werden.
Eine Programmiererin im staatlichen Auftrag kann XML benutzen, um Beschwerden über Telemarketing-Firmen zu erfassen, wobei Name der Firma und Anzahl der Beschwerden gespeichert werden.
Bei jedem dieser Beispiele wird mit XML eine neue Sprache definiert, die einem speziellen Zweck entspricht. Man könnte zwar von XML-Sprachen sprechen, aber im Allgemeinen benutzt man die Ausdrücke XML-Dialekte oder XML-Dokumenttypen. Wenn ein neuer XML-Dialekt erstellt wird, ist die formale Weise, ihn zu dokumentieren, die Erstellung einer Dokumenttypdefiniton (DTD). Sie legt die Regeln fest, denen die Daten folgen müssen, um in diesem Dialekt als wohlgeformt (well-formed) gelten zu dürfen. Listing 21.2 enthält die DTD für die Bücherdatenbank, die wir bereits gesehen haben. Listing 21.2: Der vollständige Quelltext von librml.dtd 1: 2: 3: 4: 5:
615
XML-Daten lesen und schreiben
6: 7: 8: 9:
PublicationDate edition CDATA "" isbn CDATA ""> Publisher (#PCDATA)> Subject (#PCDATA)> Review (#PCDATA)>
In Listing 21.1 enthielt die XML-Datei folgende Zeile:
Das Tag !DOCTYPE wird benutzt, um die für die Daten gültige DTD anzugeben. Wenn eine DTD zur Verfügung steht, können viele XML-Tools XML lesen, das für diese DTD erstellt wurde, und entscheiden, ob die Daten allen Regeln korrekt folgen. Ist dies nicht der Fall, erscheint eine Fehlermeldung mit Verweis auf die fehlerhafte Zeile. Diesen Prozess nennt man XML-Validierung. Bei der Arbeit mit XML werden Ihnen auch Daten begegnen, die als XML strukturiert sind, aber nicht mithilfe einer DTD definiert wurden. Diese Daten können geparst werden (soweit sie wohlgeformt sind), sodass sie in ein Programm eingelesen und weiterverarbeitet werden können, aber Sie können nicht Ihre Gültigkeit überprüfen, um sicherzustellen, dass sie korrekt nach den Regeln ihres Dialekts organisiert sind. Um eine Vorstellung zu erhalten, welche XML-Dialekte erstellt wurden, sollten Sie sich die XML.org-Datenbank unter http://www.xml.org/xml/registry.jsp ansehen. Diese Site bietet Branchennachrichten, Entwicklerressourcen, einen Kongresskalender, FAQs u. v. m.
21.2 XML mit Java verarbeiten Seit Java 2 Version 1.4 ist die XML-Unterstützung ein Standardbestandteil der Java-Klassenbibliothek. Sun unterstützt XML über das Java Application Programming Interface for XML Processing, eine Reihe von Java-Klassen (derzeit in Version 1.1), um XML-Daten zu lesen, zu schreiben und zu verändern. Um mehr über diese Klassen und verwandte XML-Technologien von Sun zu erfahren, besuchen Sie http://java.sun.com/xml/. Die Java-API für XML-Verarbeitung umfasst neun Pakete:
javax.xml.parsers
javax.xml.transform
javax.xml.transform.dom
javax.xml.transform.sax
javax.xml.transform.stream
616
XML mit Java verarbeiten
org.w3c.dom
org.xml.sax
org.xml.sax.ent
org.xml.sax.helpers
Das Paket javax.xml.parsers ist das Tor zu den anderen Paketen. Man benutzt die Klassen dieses Pakets, um XML-Daten zu parsen und zu validieren, wobei zwei verschiedene Techniken zum Einsatz kommen: die Simple API for XML (SAX) und das Document Object Model (DOM). Bevor wir uns mit noch mehr TLAs (Three-letter acronyms, zu gut Deutsch: dreibuchstabige Abkürzungen) verzetteln, wollen wir uns lieber ansehen, wie man SAX mit dem Paket javax.xml.parsers einsetzt und wie XML-Parsing in der Praxis aussieht.
Eine XML-Datei lesen Wie Sie an Tag 15 gelernt haben, müssen Sie eine Reihe von Stream- oder Reader-Objekten festlegen, die zusammenarbeiten, um eine Datei unter Java von der Festplatte zu lesen. Um z. B. einen gepufferten Bytestream aus einer Datei zu lesen, erzeugt man mithilfe eines File-Objekts ein FileInputStream-Objekt, mit dem wiederum ein BufferedInputStream erzeugt wird. Um mit SAX und dem Paket javax.xml.parsers eine XML-Datei zu lesen, braucht man dieselbe Art von Beziehung zwischen Klassen. Als Erstes erzeugen Sie ein SAXParserFactory-Objekt, indem Sie die Klassenmethode SAXParserFactory.newInstance() aufrufen, wie in dieser Anweisung: SAXParserFactory factory = SAXParserFactory.newInstance();
Der Zweck einer SAXParserFactory ist es, einen SAX-Parser gemäß Ihrer Angaben zu erzeugen. Eine Spezifikation ist, ob der SAX-Parser das XML mit einer DTD validieren soll. Um Validierung zu unterstützen, rufen Sie die Methode setValidating(Boolean) der ParserFactory mit dem Argument true auf: factory.setValidating(true);
Nachdem Sie die Factory so eingestellt haben, dass sie den gewünschten Parser produziert, rufen Sie die newSaxParser()-Methode der Factory auf, um ein SAXParser-Objekt zu erzeugen: SAXParser sax = factory.newSAXParser();
Diese Methode erzeugt eine ParserConfigurationException, wenn die Factory keinen Parser erzeugen kann, der Ihren Spezifikationen entspricht. Daher müssen Sie in der Methode, in der newSaxParser() aufgerufen wird, einen try-catch-Block oder eine throws-Anweisung einrichten.
617
XML-Daten lesen und schreiben
Der SAX-Parser kann XML-Daten aus Dateien, Eingabestreams und anderen Quellen lesen. Um aus einer Datei zu lesen, wird die Methode parse(File, DefaultHandler) des Parsers aufgerufen. Diese Methode wirft zwei Arten von Ausnahmen aus: IOException, wenn während des Lesens der Datei ein Fehler auftritt, und SAXException, wenn der SAXParser während des Parsens der Daten Schwierigkeiten hat. SAXException ist eine Klasse im Paket org.xml.sax. Dies ist eines der drei Pakete, das die
XML-Industriegruppe XML.Org erstellt hat und das Teil der Java-2-Klassenbibliothek ist. Diese Ausnahme ist die Superklasse aller SAX-Ausnahmen. Sie können die Methode getMessage() des Ausnahmeobjekts in einem catch-Block aufrufen, um sich mehr Informationen über das Problem anzusehen, das zur Ausnahme führte. Das zweite Argument der Methode parse() ist ein Objekt der Klasse DefaultHandler, Teil des Pakets org.xml.sax.helpers. Die Klasse DefaultHandler ist eine Klasse, die nichts weiter tut, als vier Schnittstellen des Pakets org.xml.sax zu implementieren: ContentHandler, DTDHandler, EntityResolver und ErrorHandler. Diese vier Schnittstellen werden von Klassen implementiert, die benachrichtigt werden wollen, wenn besondere Ereignisse beim Lesen von XML-Daten durch die parse()-Methode auftreten. Um alle diese Schnittstellen zu implementieren, umfasst die Klasse DefaultHandler die folgenden Methoden:
startDocument() – Der Parser hat den Anfang der XML-Daten erreicht.
endDocument() – Der Parser hat das Ende der XML-Daten erreicht.
startElement(String, String, String, Attributes) – Der Parser hat ein Start-Tag-
Element gelesen.
characters(char[], int, int) – Der Parser hat Zeichendaten gelesen, die sich zwi-
schen einem Anfangs- und einem End-Tag befinden.
endElement(String, String, String) – Der Parser hat ein End-Tag-Element gelesen.
Jede dieser Methoden wirft SAXException-Ausnahmen aus. Um mit den geparsten XML-Daten etwas anfangen zu können, erzeugen Sie eine Subklasse von DefaultHandler, die die Methoden überschreibt, um die Sie sich kümmern wollen.
XML-Tags zählen Listing 21.3 zeigt Ihnen CountTag, eine Java-Applikation, die mitzählt, wie oft ein AnfangsTag-Element in einer XML-Datei vorkommt. Sie geben den Dateinamen und das Tag als Kommandozeilenargumente an, sodass Sie dieses Programm mit jeder XML-Datei verwenden können, die Sie inspizieren möchten.
618
XML mit Java verarbeiten
Listing 21.3: Der vollständige Quelltext von CountTag.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44:
import import import import
javax.xml.parsers.*; org.xml.sax.*; org.xml.sax.helpers.*; java.io.*;
public class CountTag extends DefaultHandler { public static void main(String[] arguments) { if (arguments.length > 1) { CountTag ct = new CountTag(arguments[0], arguments[1]); } else { System.out.println("Usage: java CountTag filename tagName"); } } CountTag(String xmlFile, String tagName) { File input = new File(xmlFile); SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(false); try { SAXParser sax = factory.newSAXParser(); CountTagHandler cth = new CountTagHandler(tagName); sax.parse(input, cth); System.out.println("The " + cth.tag + " tag appears " + cth.count + " times."); } catch (ParserConfigurationException pce) { System.out.println("Could not create that parser."); System.out.println(pce.getMessage()); } catch (SAXException se) { System.out.println("Problem with the SAX parser."); System.out.println(se.getMessage()); } catch (IOException ioe) { System.out.println("Error reading file."); System.out.println(ioe.getMessage()); } } } class CountTagHandler extends DefaultHandler { String tag; int count = 0; CountTagHandler(String tagName) { super();
619
XML-Daten lesen und schreiben
45: 46: 47: 48: 49: 50: 51: 52: 53: 54: }
tag = tagName; } public void startElement(String uri, String localName, String qName, Attributes attributes) { if (qName.equals(tag)) count++; }
Zwei Klassen werden in Listing 21.3 definiert: CountTag, das einen SAX-Parser erzeugt und ihn veranlasst, ein File-Objekt zu parsen, sowie CountTagHandler, das Tags zählt. Diese Applikation besitzt eine Hilfsklasse namens CountTagHandler, die eine Subklasse von DefaultHandler ist. Um mitzuzählen, wie oft ein Anfangs-Tag in einer XML-Datei vorkommt, wird die Methode startElement(String, String, String, Attributes) überschrieben. Wenn ein SAX-Parser startElement() aufruft, bieten die Argumente der Methode Informationen über das Tag:
Das erste Argument ist der URI (Uniform Resource Indicator) des Tags.
Das zweite Argument ist der lokale Name des Tags.
Das dritte Argument ist der qualifizierte Name des Tags.
Das vierte Argument ist ein Attributes-Objekt, das Informationen über die Attribute beinhaltet, die mit dem Tag assoziiert sind.
URI und qualifizierter Name eines Tags beziehen sich auf XML-Namensräume, die es ermöglichen, XML-Tags und -Attribute auf eine Weise zu identifizieren, die im ganzen Internet eindeutig ist, sodass nicht zwei verschiedene Tags über denselben URI und denselben qualifizierten Namen verfügbar sein können. Der einzige Name, nach dem die Klasse CountTagHandler sucht, ist der qualifizierte Name des Tags. In einer XML-Datei, die keine Namensräume benutzt, ist der qualifizierte Name der Text, der zwischen den Zeichen < und > des Tags steht. Wenn sich die Klasse CountTag.class im selben Ordner wie collection.librml befindet, können Sie sie mit dem folgenden Kommando ausführen: java CountTag collection.librml Book
Die Ausgabe dieser Applikation sollte wie folgt aussehen: The Book tag appears 2 times.
Die Applikation CountTag benutzt einen nicht validierenden Parser, sodass Sie sie mit jeder XML-Datei auf Ihrem System ausprobieren können. Wenn Sie zum Testen lieber eine
620
XML mit Java verarbeiten
etwas längere Datei benutzen möchten, finden Sie auf der Website zum Buch unter http:/ /www.java21pro.com auf der Seite zu Tag 21 die Datei history.opml.
Outliner Processor Markup Language (OPML) ist ein XML-Dialekt, der von UserLand Software erstellt wurde und der Informationen repräsentiert, die als Outline gespeichert wurden. Unter http://www.opml.org können Sie mehr über diesen Dialekt herausfinden.
XML-Daten lesen Wenn Sie mit einer Subklasse von DefaultHandler arbeiten, können Sie die Anfangs-Tags mitzählen, indem Sie die Methode startElement() überschreiben. Wenn ein Parser einen Anfangs-Tag findet, wissen Sie nichts über die Daten, die dem Tag folgen. Wenn Sie Daten aus einer XML-Datei lesen wollen, müssen Sie noch einige Methoden überschreiben, die von DefaultHandler ererbt sind. Daten von einem XML-Tag auszulesen, ist ein Prozess in drei Schritten: 1. Überschreiben Sie die Methode startElement(), um zu erfahren, wann ein neues Anfangs-Tag geparst wird. 2. Überschreiben Sie die Methode characters(), um zu erfahren, was das Tag beinhaltet. 3. Überschreiben Sie die Methode endElement(), um zu , wann das End-Tag erreicht wird. Ein Parser ruft die Methode characters(char[], int, int) auf, wenn ein Tag Zeichendaten (mit anderen Worten: Text) enthält. Das erste Argument ist ein Zeichen-Array, in dem die Daten gespeichert sind. Sie verwenden jedoch nicht das ganze Zeichen-Array. Die Daten innerhalb des Tags befinden sich in einem Teilbereich des Arrays. Das zweite Argument von characters() gibt das erste Element des Arrays an, von dem ab Daten zu lesen sind, und das dritte Argument gibt die Zahl der zu lesenden Zeichen an. Die folgende characters()-Methode benutzt Zeichendaten, um ein String-Objekt zu erzeugen, und zeigt es dann an: public void characters(char[] text, int first, int length) { String data = new String(text, first, length); System.out.println(data); }
Ein Parser ruft die Methode endElement(String, String, String) auf, wenn ein End-Tag erreicht ist. Die drei Argumente der Methode sind dieselben wie die ersten drei Argumente der startElement()-Methode – der URI, der qualifizierte Name und der lokale Name des Tags.
621
XML-Daten lesen und schreiben
Der SAX-Parser sieht und > nicht als Teil des Namens des Tags an. Wenn der Parser ein End-Tag namens liest, ruft er die endElement()-Methode mit Source als drittem Argument auf. Die letzten beiden Methoden, die Sie in einer DefaultHandler-Subklasse überschreiben könnten, sind startDocument() und endDocument(), die keine Argumente erwarten.
XML-Daten validieren Das letzte Projekt des heutigen Tages ist ReadLibrary, eine Java-Applikation, die die XMLDatei liest, die mit dem bereits vorgestellten Dialekt erstellt wurde – das Buchdatenbankformat, das in der Datei collection.librml benutzt und in der Datei librml.dtd definiert wurde. Da für diesen Dialekt eine DTD zur Verfügung steht, sollte der SAX-Parser, den Sie mit einem SAXParserFactory-Objekt erzeugen, validieren. Dies geschieht, indem man die Methode setValidating(Boolean) der Factory mit dem Argument true aufruft. Das Projekt ReadLibrary ist ähnlich wie das letzte Projekt aufgebaut – es gibt eine Applikationshauptklasse namens ReadLibrary.class, eine Hilfsklasse namens LibraryHandler.class und eine Hilfsklasse namens Book.class. Die Klasse ReadLibrary lädt eine Datei, die durch ein Kommandozeilenargument angegeben wird, erzeugt einen SAX-Parser und weist ihn an, die Datei zu parsen. Die Klasse LibraryHandler, eine Subklasse von DefaultHandler, enthält die Methoden, mit denen man verfolgt, was der Parser treibt, und mit denen man an verschiedenen Stellen des XML-Parsing-Prozesses eingreifen kann. Wenn Sie XML-Daten mit SAX lesen, muss die Methode characters() das letzte AnfangsTag kennen, das vom Parser gelesen wurde. Ansonsten gibt es keine Möglichkeit herauszufinden, welches Tag die Daten enthält. Um dies im Auge behalten zu können, besitzt die Klasse LibraryHandler eine Instanzvariable namens currentActivity, die die derzeitige Parsing-Aktivität als Integerwert speichert. Der Integerwert, den currentActivity erhält, ist eine von sieben Klassenvariablen, die in den folgenden Anweisungen definiert werden: static static static static static static static
622
int int int int int int int
READING_TITLE = 1; READING_AUTHOR = 2; READING_PUBLISHER = 3; READING_PUBLICATION_DATE = 4; READING_SUBJECT = 5; READING_REVIEW = 6; READING_NOTHING = 0;
XML mit Java verarbeiten
Wenn man Klassenvariablen für Integerwerte benutzt, erleichtert man Programmierern das Verständnis und minimiert das Risiko, dass falsche Werte in einer Anweisung benutzt werden. Die Klasse LibraryHandler hat ferner eine Variable namens libraryBook, die eine Instanz der Klasse Book ist. Im Folgenden sehen Sie die Anweisungen, aus denen sich diese Klasse zusammensetzt: class Book String String String String String String String String }
{ title; author; publisher; publicationDate; edition; isbn; subject; review;
Die Klasse Book speichert die verschiedenen Elemente der einzelnen Bibliotheksbücher, wie sie aus der XML-Datei gelesen werden. Innerhalb der Methode startElement() wird der lokale Name eines Tags in der Variable localName gespeichert. Die folgende Anweisung wird in der Methode benutzt: if (localName.equals("Title")) currentActivity = READING_TITLE;
Diese Anweisung setzt die Variable currentActivity auf den Wert der Klassenvariable READING_FILE, wenn der SAX-Parser auf den Tag <Title> stößt. Wenn Zeichendaten in der Methode characters() empfangen wurden, wird die Variable currentActivity benutzt, um herauszufinden, welches Tag die Daten beinhaltet. Die folgenden Anweisungen kommen in der Methode vor: String value = new String(ch, start, length); if (currentActivity == READING_TITLE) libraryBook.title = value;
Die erste Anweisung erzeugt einen String namens value, der die Zeichendaten innerhalb des Tags beinhaltet. Wenn der Parser gerade das Titel-Tag liest, wird value der title-Variable des Objekts libraryBook zugewiesen. Als Letztes müssen Sie sich in der Klasse LibraryHandler noch darum kümmern, dass die Informationen über die einzelnen Bücher anzeigt werden, wenn jeweils die XML-Daten geparst wurden. Dies erfolgt in der Methode endElement(), die den lokalen Namen jedes End-Tags in einer Variable namens localName speichert. Wenn localName gleich "Book" ist, hat der Parser das -Tag in der XML-Datei erreicht. Dieses Tag bedeutet, dass keine weiteren Informationen über das aktuelle Buch vorliegen.
623
XML-Daten lesen und schreiben
Die folgende Anweisung erscheint in der Methode: if (qName.equals("Book")) System.out.println("\ nTitle: " + libraryBook.title);
Listing 21.4 enthält den vollständigen Quelltext der Applikation ReadLibrary. Listing 21.4: Der vollständige Quelltext von ReadLibrary.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38:
624
import import import import
javax.xml.parsers.*; org.xml.sax.*; org.xml.sax.helpers.*; java.io.*;
public class ReadLibrary extends DefaultHandler { public static void main(String[] arguments) { if (arguments.length > 0) { ReadLibrary read = new ReadLibrary(arguments[0]); } else { System.out.println("Usage: java ReadLibrary filename"); } } ReadLibrary(String libFile) { File input = new File(libFile); SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(true); try { SAXParser sax = factory.newSAXParser(); sax.parse(input, new LibraryHandler() ); } catch (ParserConfigurationException pce) { System.out.println("Could not create that parser."); System.out.println(pce.getMessage()); } catch (SAXException se) { System.out.println("Problem with the SAX parser."); System.out.println(se.getMessage()); } catch (IOException ioe) { System.out.println("Error reading file."); System.out.println(ioe.getMessage()); } } } class LibraryHandler extends DefaultHandler { static int READING_TITLE = 1; static int READING_AUTHOR = 2;
XML mit Java verarbeiten
39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84:
static int READING_PUBLISHER = 3; static int READING_PUBLICATION_DATE = 4; static int READING_SUBJECT = 5; static int READING_REVIEW = 6; static int READING_NOTHING = 0; int currentActivity = READING_NOTHING; Book libraryBook = new Book(); LibraryHandler() { super(); } public void startElement(String uri, String localName, String qName, Attributes attributes) { if (qName.equals("Title")) currentActivity = READING_TITLE; else if (qName.equals("Author")) currentActivity = READING_AUTHOR; else if (qName.equals("Publisher")) currentActivity = READING_PUBLISHER; else if (qName.equals("PublicationDate")) currentActivity = READING_PUBLICATION_DATE; else if (qName.equals("Subject")) currentActivity = READING_SUBJECT; else if (qName.equals("Review")) currentActivity = READING_REVIEW; if (currentActivity == READING_PUBLICATION_DATE) { libraryBook.isbn = attributes.getValue("isbn"); libraryBook.edition = attributes.getValue("edition"); } } public void characters(char[] ch, int start, int length) { String value = new String(ch, start, length); if (currentActivity == READING_TITLE) libraryBook.title = value; if (currentActivity == READING_AUTHOR) libraryBook.author = value; if (currentActivity == READING_PUBLISHER) libraryBook.publisher = value; if (currentActivity == READING_PUBLICATION_DATE) libraryBook.publicationDate = value; if (currentActivity == READING_SUBJECT) libraryBook.subject = value;
625
XML-Daten lesen und schreiben
85: if (currentActivity == READING_REVIEW) 86: libraryBook.review = value; 87: } 88: 89: public void endElement(String uri, String localName, String qName) { 90: if (qName.equals("Book")) { 91: System.out.println("\nTitle: " + libraryBook.title); 92: System.out.println("Author: " + libraryBook.author); 93: System.out.println("Publisher: " + libraryBook.publisher); 94: System.out.println("Publication Date: " 95: + libraryBook.publicationDate); 96: System.out.println("Edition: " + libraryBook.edition); 97: System.out.println("ISBN: " + libraryBook.isbn); 98: System.out.println("Review: " + libraryBook.review); 99: libraryBook = new Book(); 100: } 101: } 102: } 103: 104: class Book { 105: String title; 106: String author; 107: String publisher; 108: String publicationDate; 109: String edition; 110: String isbn; 111: String subject; 112: String review; 113: }
Die Applikation ReadLibrary liest eine XML-Datei, die den Bibliotheksbuch-Dialekt benutzt, den Ihnen die Listings 21.1 und 21.2 demonstriert haben. Um die Datei collection.librml mit der Applikation zu lesen, geben Sie Folgendes in der Kommandozeile ein: java ReadLibrary collection.librml
Das Programm gibt dann Folgendes aus: Title: Catch-22 Author: Joseph Heller Publisher: Simon and Schuster Publication Date: 09/1996 Edition: Trade ISBN: 0684833395 Review: heller-catch22.html Title: Slaughterhouse-Five Author: Kurt Vonnegut
626
Zusammenfassung
Publisher: Dell Publication Date: 12/1991 Edition: Paperback ISBN: 0440180295 Review: null
21.3 Zusammenfassung In mehrfacher Hinsicht ist XML das Daten-Äquivalent zu Java. Es befreit Daten von der Software, mit der sie erstellt wurden, und dem Betriebssystem, auf dem die Software lief, analog dazu, wie Java Software von einem bestimmten Betriebssystem loslöst. Heute haben Sie die Grundlagen von XML kennen gelernt und erfahren, wie man die Java-API für XML-Verarbeitung verwendet, um Daten aus einer XML-Datei auszulesen. Um XML-Daten unter Java zu schreiben, benötigt man diese API gar nicht. Sie können XML-Dateien einfach dadurch erzeugen, dass Sie Strings in eine Datei, einen Ausgabestream oder ein anderes Medium schreiben. Einer der größten Vorteile bei der Repräsentation von Daten mittels XML ist Ihre Sicherheit, stets an die Daten heranzukommen, z. B. wenn Sie sie irgendwann aus irgendeinem Grund in eine relationale Datenbank, eine MySQL-Datenbank oder an einen anderen Ort verschieben wollen. Sie können XML auch in andere Formen übertragen (z. B. in HTML), indem Sie verschiedene Technologien benutzen, sowohl unter Java als auch mittels Tools, die in anderen Sprachen entwickelt wurden.
21.4 Workshop Fragen und Antworten F
Warum heißt die Extensible Markup Language eigentlich XML und nicht EML? A
Keiner der Schöpfer der Sprache scheint seine Gründe niedergelegt zu haben, warum man XML wählte. Die Mehrheit in der XML-Gemeinde geht davon aus, dass man sich deswegen für XML entschied, weil es »besser klingt« als EML. Bevor sie loskichern, sollten Sie daran denken, dass Sun Microsystems denselben Kriterien folgte, als es Java als Namen für seine Programmsprache wählte und sich dabei gegen technische Abkürzungen wie DNA oder WRL entschied.
627
XML-Daten lesen und schreiben
Möglicherweise wollten die Schöpfer von XML auch eine Verwechslung mit einer relativ unbekannten Programmiersprache namens EML (Extended Machine Language) vermeiden. F
XML mithilfe der Java-API für XML-Verarbeitung zu verwenden, ist ganz schön kompliziert. Gibt es keine einfachere Methode, um unter Java Daten aus einer XML-Datei zu lesen? A
Mehrere Klassenbibliotheken wurden mit dem Ziel entworfen, die XML-Verarbeitung unter Java zu erleichtern. Eine von ihnen ist JDOM, eine Open-Source-Bibliothek für das Lesen, Schreiben und Verändern von XML, wobei Java-Objekte und keine XML-Parsing-Methoden zum Einsatz kommen. Sie finden zusätzliche Informationen auf der Projekt-Website http://www.jdom.org. Eine andere Open-Source-Alternative wäre XOM, ein Projekt, das noch ganz am Anfang steht und das wegen gewisser Unzulänglichkeiten von JDOM in Angriff genommen wurde. Sie können sich auf der XOM-Website unter http://www.cafeconleche.org/XOM/ informieren.
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Angenommen, in einem Dokument sind alle Anfangs-Tag-Elemente, End-Tag-Elemente und andere Auszeichnungen konsequent angewendet; mit welchem Adjektiv könnte man das Dokument am besten beschreiben? (a) validierend (b) persistent (c) wohlgeformt 2. Mit welcher Klasse im Paket javax.xml.parsers erzeugt man SAX-Parser? (a) SAXParserFactory (b) SAXParser (c) DefaultHandler 3. Welcher der drei folgenden Vorschläge wäre ein akzeptables End-Tag-Element in XML? (a) (b) (c)
628
Workshop
Antworten 1. c. Damit Daten als XML angesehen werden können, müssen sie wohlgeformt sein. 2. a. Die Klasse SAXParserFactory wird benutzt, um einen SAX-Parser gemäß Ihrer Spezifikationen zu erzeugen. 3. a. Jedes End-Tag-Element muss ein Name sein, der von den Zeichen und > umgeben ist. Antwort b, , wäre nur als Tag-Element, das keine Daten umschließt, akzeptabel.
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Gegeben sei: public class BadClass { String field[] = new String[10]; int i = 0; public BadClass() { for ( ; i < 10; i++) field[i] = packField(i); } private String packField(int i) { StringBuffer spaces = new StringBuffer(); for (int j = 0; j < i; j++) { spaces.append(‘ ‘); } return spaces; } }
Welche Zeile ist schuld daran, dass das Programm nicht kompiliert werden kann? a. String field[] = new String[10]; b. for ( ; i < 10; i++) c. StringBuffer spaces = new StringBuffer(); d. return spaces;
629
XML-Daten lesen und schreiben
Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 21, und klicken Sie auf den Link »Certification Practice«.
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Erzeugen Sie zwei Applikationen: Die eine liest Einträge aus einer Datenbank und erzeugt eine XML-Datei, die dieselben Informationen beinhaltet. Die andere Applikationen liest die XML-Daten und zeigt sie an.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Website zum Buch: http://www.java21pro.com.
630
W O C H E
Bonuswoche
W O C H E
Tag 22 Servlets Tag 23 JavaServer Pages
633
Tag 24 Java-1.0-Applets erstellen Tag 25 Accessibility
671
651 697
Tag 26 Java Web Start Tag 27 Web Services mit XML-RPC erstellen
717
Tag 28 Reguläre Ausdrücke
757
737
W O C H E
Servlets
2 2
Servlets
Java hat sich über Applets, die auf Webseiten laufen, und Applikationen, die auf Clients laufen, hinausentwickelt. Servlets sind Applikationen, die auf einem Server laufen, der mit dem WWW verbunden ist. Sie werden für Java-Entwickler zunehmend wichtiger. Servlets nutzen Java im Web, ohne dass die engen Sicherheitsvorkehrungen getroffen werden müssten, die für Applets gelten. Sie laufen auf einem Server, nicht auf dem Computer des Besuchers, und können daher alle Features der Sprache nutzen. Man verwendet sie zur Erstellung von Webapplikationen – Programme, die Informationen sammeln und sie als Seiten im WWW präsentieren. Heute beschäftigen wir uns mit folgenden Themen:
Wie sich Servlets von Applikationen und Applets unterscheiden
Wie ein Webserver Servlets ausführt
Wie man die Servlet-Klassenbibliothek von Sun benutzt
Wie man Servlets als Teil von Apache und anderen Webservern ausführt
Wie man Daten aus einem Webseitenformular ausliest
Wie man Cookies speichert und ausliest
Wie man mit Servlets Web-Content dynamisch generiert
22.1 Servlets im WWW Servlets wurden zwar so entworfen, dass man sie mit verschiedenen Internetdiensten verwenden kann, doch konzentrieren sich Sun und andere Servlet-Entwickler auf den ServletEinsatz im WWW. Java-Servlets werden von einem WWW-Server ausgeführt, der über einen Interpreter mit Unterstützung für die Java-Servlet-Spezifikation verfügt. Diesen Interpreter nennt man auch Servlet Engine, und er ist so optimiert, dass er Servlets möglichst ressourcenschonend auf dem Webserver ausführt. Servlets dienen demselben Zweck wie Programme, die das Common Gateway Interface (CGI) nutzen. Dabei handelt es sich um ein Protokoll für die Erstellung von Programmen, die Informationen über einen Webserver versenden und empfangen. Im WWW gibt es CGI-Programmierung eigentlich schon immer. Die meisten CGI-Programme, die man auch CGI-Scripts nennt, sind in Sprachen wie Perl, Python oder C enthalten.
634
Servlets im WWW
Sie haben bei Ihren Surfsessions mit Sicherheit schon Hunderte von CGI-Programmen genutzt. Man verwendet CGI für die folgenden Einsatzgebiete:Daten aus einem Formular auf einer Webseite auslesen
Informationen aus Feldern einer URL auslesen
Programme auf dem Computer ausführen, auf dem der Webserver läuft
Konfigurationsinformationen über die einzelnen Besucher einer Webseite speichern und auslesen (dieses Feature ist allgemein als Cookies bekannt)
Daten an den Besucher zurückschicken, und zwar in Form eines HTML-Dokuments, einer GIF-Datei oder in einem anderen üblichen Format
Servlets können alle diese Aufgaben erfüllen und beherrschen zudem ein Verhalten, das mit den meisten CGI-Scriptsprachen nur mit größter Schwierigkeit zu implementieren wäre. Servlets bieten vollständige Unterstützung für Sessions, mit denen man genau verfolgen kann, wie sich ein Besucher über die verschiedenen Teile der Website bewegt und im Laufe der Zeit Fenster öffnet bzw. schließt. Servlets können auch über ein Standard-Interface direkt mit dem Webserver kommunizieren. Sofern der Server Java-Servlets unterstützt, kann er Informationen mit diesen Programmen austauschen. Java-Servlets haben dieselben Portabilitätsvorteile wie die Sprache selbst. Die offizielle Sun-Implementierung von Servlets geschah zwar in Zusammenarbeit mit den Schöpfern von Apache, doch auch andere Server-Entwickler haben Tools zur Unterstützung von JavaServlets herausgebracht, so z. B. IBM WebSphere Application Server, BEA WebLogic und den Microsoft Internet Information Server. Servlets sind zudem speicherschonend. Wenn zehn Besucher dasselbe CGI-Script gleichzeitig nutzen, hat ein Webserver zehnmal dasselbe Script im Speicher. Wenn zehn Besucher ein Java-Servlet verwenden, wird dieses Servlet dagegen nur einmal in den Speicher geladen; dieses Servlet richtet einfach für jeden Besucher einen neuen Thread ein.
Unterstützung für Servlets Sun Microsystems unterstützt Java-Servlets über Tomcat, das von Sun Microsystems und der Apache Software Foundation entwickelt wurde. Tomcat umfasst zwei Java-Klassenbibliotheken, javax.servlet und javax.servlet.http, sowie Software, die dem Apache Servlet-Funktionalität gibt. Ferner gibt es einen selbstständigen Servlet-Interpreter, mit dem Sie Servlets testen können, bevor Sie sie ins Internet stellen. Auf den folgenden zwei Websites können Sie mehr über Tomcat erfahren:
Die Java-Servlet-Site von Sun: http://java.sun.com/products/servlet/
Die Tomcat-Site von Apache: http://jakarta.apache.org/tomcat
635
Servlets
Damit Sie Servlets verwenden können, brauchen Sie einen Webserver, der diese Programme unterstützt. Wenn Sie einen Apache betreiben und sich so weit auskennen, dass sie dessen Funktionalität erweitern können, dann installieren Sie einfach Tomcat-Unterstützung. Unter Umständen verfügt Ihr Webserver oder Web Application Server bereits über Servlet-Unterstützung. Wenn Ihnen kein Server zur Verfügung steht, Sie aber dennoch gerne mit der Servlet-Entwicklung anfangen möchten, sollten Sie sich an einen kommerziellen Webhoster wenden, der Java-Servlet-Unterstützung anbietet. Diese Firmen haben bereits Tomcat installiert und so eingerichtet, dass es mit ihren Servern zusammenarbeitet, sodass Sie sich nur noch um die Erstellung von Servlets mithilfe der Klassen von javax.servlet und javax.servlet.http kümmern müssen. Das Open Directory Project listet mehr als ein Dutzend Firmen auf, die integriertes Servlet-Hosting anbieten. Gehen Sie auf http://www.dmoz.org und suchen Sie nach dem Begriff »servlet hosting«. In den letzten Jahren habe ich für das Hosten der Servlets und der JavaServer Pages auf der offiziellen Website zu diesem Buch die Firma Motivational Marketing Associates benutzt. MMA bietet Java-Servlet-Hosting auf einem ApacheWebserver unter Linux. Sie können sich das Angebot dieser Firma im Einzelnen unter http://www.mmaweb.com ansehen. Servlets werden von den unterschiedlichsten Webservern unterstützt, die alle jeweils ihre eigenen Installationsprozesse, Sicherheitseinstellungen und Administrationsprozeduren haben. Wir sehen uns heute an, was man mit einem Server macht, nachdem er aufgesetzt ist: Java-Servlets erstellen und kompilieren. Um Servlets auf Ihrem Computer kompilieren zu können, müssen die beiden Java-ServletPakete javax.servlet und javax.servlet.http installiert sein. Diese Pakete sind nicht Teil der Standard-Java-Klassenbibliothek, die als Teil von Java 2 SDK 1.4 mitgeliefert wird. Zum Zeitpunkt der Drucklegung dieses Buchs war die aktuelle Version dieser Pakete Java Servlet 2.3, das von Tomcat Version 4.0 unterstützt wurde. Um Java Servlet 2.3 herunterzuladen, gehen Sie auf die Java-Servlet-Site http:// java.sun.com/products/servlet/ und klicken Sie auf den Download-Link. Möglicherweise können Sie auch den direkten Link benutzen, http://java.sun.com/products/servlet/download.html, doch leider verändert Sun ziemlich regelmäßig den Aufbau seiner Java-Site. Derzeit sind diese Pakete als ZIP-Dateien hinterlegt, die Sie erst mit WinZIP oder einem ähnlichen Programm dekomprimieren müssen. Beim Entpackungsvorgang müssen Sie unbedingt die Option anwählen, mit der die im Archiv existierenden Ordnernamen übernommen werden.
636
Servlets entwickeln
Sie können auf dieser Webseite verschiedene Softwarepakete herunterladen, u.a. verschiedene Versionen, Dokumentationen und technische Spezifikationen. Genaue Angaben zum Download finden Sie im Abschnitt »2.3 Final Release« neben der Überschrift »Download Class Files 2.3«. Version 2.3 beinhaltet alle Klassendateien in zwei Paketen, die Javadoc-Dokumentation für die Klassen und eine jar-Datei, die ebenfalls alle Klassen enthält. Dieses Java-Archiv heißt servlet.jar, und es wird in einen lib-Unterordner installiert. Tragen Sie diese Datei in den CLASSPATH Ihres Computers ein.
22.2 Servlets entwickeln Servlets werden wie jede andere Java-Applikation erzeugt und kompiliert. Nach der Installation der beiden Servlet-jar-Dateien und der Anpassung des CLASSPATH können Sie sie mit dem Java-Compiler des SDK 1.4 oder mit jedem anderen Tool kompilieren, das Java 2 Version 1.4 unterstützt. Um ein Servlet zu erzeugen, leiten Sie die Klasse HttpServlet ab, die Teil des Pakets javax.servlet ist. Diese Klasse besitzt Methoden, die den Lebenszyklus eines Servlets repräsentieren, und solche, die Informationen von dem Webserver einholen, auf dem das Servlet läuft. Die Methode init(ServletConfig) wird automatisch aufgerufen, wenn ein Webserver das erste Mal ein Servlet online stellt, um eine Besucheranfrage zu beantworten. Wie bereits erwähnt, kann ein einziges Servlet mehrere Anfragen von verschiedenen Besuchern bearbeiten. Die init()-Methode wird lediglich einmal aufgerufen, und zwar dann, wenn das Servlet online geht. Ist das Servlet bereits online, wenn eine weitere Anfrage eingeht, wird die init()-Methode nicht noch einmal aufgerufen. Die Methode init() hat ein Argument – ServletConfig, eine Schnittstelle im Paket javax.servlet, die Methoden besitzt, um mehr über die Umgebung herauszufinden, in der das Servlet läuft. Die Methode destroy() wird aufgerufen, wenn der Webserver das Servlet vom Netz nimmt. Wie bei der init()-Methode geschieht dies nur einmal, und zwar dann, wenn alle Besucher ihre Informationen vom Servlet erhalten haben. Wenn dies nicht innerhalb eines festgelegten Zeitraums geschieht, wird destroy() automatisch aufgerufen. Damit wird verhindert, dass sich ein Servlet aufhängt, während es darauf wartet, Informationen mit einem Besucher auszutauschen. Eines der Haupteinsatzgebiete eines Servlets ist, Informationen von einem Besucher einzuholen und irgendeine Antwort darauf zu liefern. Man sammelt mithilfe eines Formulars
637
Servlets
Informationen von Besuchern. Ein Formular ist eine Gruppe von Textfeldern, Radiobuttons, Textbereichen, Buttons und anderen Eingabefeldern auf einer WWW-Seite. Abbildung 22.1 zeigt ein Webformular auf einer Seite, die im Microsoft Internet Explorer 6 dargestellt wird.
Abbildung 22.1: Informationen mit einem Webformular erfassen
Das Formular aus Abbildung 22.1 enthält zwei Felder: einen Textbereich und einen anklickbaren Button mit dem Label »translate«. Der HTML-Code für diese Seite würde wie folgt lauten: ROT-13 Translator ROT-13 Translator Text to translate:
Das Formular wird von den HTML-Tags und umschlossen. Jedes Feld im Formular wird von eigenen Tags repräsentiert: und für den Textbereich und für den »translate«-Button. Der Textbereich erhält den Namen text.
638
Servlets entwickeln
Web-Servlets setzen grundlegende HTML-Kenntnisse voraus, denn die einzige Benutzerschnittstelle für ein Servlet ist eine WWW-Seite, die in einem Browser dargestellt wird. Ein empfehlenswertes HTML-Buch ist »HTML 4 in 21 Tagen« von Laura Lemay. Die einzelnen Felder eines Formulars speichern Informationen, die an einen Webserver übermittelt und dann an ein Servlet geschickt werden können. Browser kommunizieren mit Servern über das Hypertext Transfer Protocol (HTTP). Formulardaten können mit zwei verschiedenen HTTP-Anfragen an einen Server geschickt werden: GET und POST. Wenn eine Webseite GET oder POST an einen Server schickt, muss der Name des Programms, das sich um die Anfrage kümmern soll, als Webadresse – auch Uniform Resource Locator (URL) genannt – angegeben werden. Das GET-Kommando hängt sämtliche Daten an das Ende einer URL, also z. B.: http://www.java21pro.com/servlets/beep?number=5551220&repeat=no
Das POST-Kommando schickt die Formulardaten als Header, der getrennt von der URL übermittelt wird. Dies wird im Allgemeinen vorgezogen und ist sogar unerlässlich, wenn im Formular vertrauliche Informationen abgefragt werden. Zudem haben einige Webserver und Browser Probleme mit URLs, die länger als 255 Zeichen sind, sodass die Menge der Informationen, die per GET verschickt werden können, begrenzt ist. Servlets reagieren auf beide Kommandos mit Methoden, die aus der Klasse HttpServlet ererbt sind: doGet(HttpServletRequest, HttpServletResponse) und doPost(HttpServletRequest, HttpServletResponse). Diese Methoden werfen zwei Arten von Ausnahmen aus: ServletException, das Teil des Pakets javax.servlet ist, und IOException, eine Ausnahme des Standard-java.io-Pakets, bei der es um Ein- und Ausgabestreams geht. Innerhalb von Java funktionieren beide Methoden identisch, daher ist es bei Servlet-Programmieren üblich, mit der einen Methode die andere aufzurufen: public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); }
Die Methoden doGet() und doPost() haben zwei Argumente: ein HttpServletRequestObjekt und ein HttpServletResponse-Objekt. Diese Objekte gehören zu Klassen im Paket javax.servlet.http. Ein Servlet erhält Informationen darüber, wie es ausgeführt wurde, indem es Methoden der Klasse HttpServletRequest aufruft. Wenn z. B. ein WWW-Formular an ein Servlet übermittelt wird, wird jedes Feld des Formulars von der Klasse HttpServletRequest als String gespeichert.
639
Servlets
Sie können diese Felder in einem Servlet auslesen, indem Sie die getParameter(String)Methode mit dem Namen des Feldes als Argument aufrufen. Diese Methode gibt null zurück, wenn kein Feld mit diesem Namen existiert. Ein Servlet kommuniziert mit dem Besucher, indem es ein HTML-Dokument, eine Bilddatei oder eine andere Art von Information zurückschickt, mit der ein Browser etwas anfangen kann. Es sendet diese Informationen, indem es Methoden der Klassen HttpServletResponse aufruft. Wenn Sie eine Antwort vorbereiten, müssen Sie als Erstes festlegen, welche Art von Information das Servlet an den Browser schicken soll. Rufen Sie die Methode setContentType(String) mit dem Informationstyp als Argument auf. Am häufigsten nehmen Antworten die Form von HTML an, was man mit dem Aufruf von setContentType("text/html") festlegen würde. Man kann Antworten auch als Text ("text/ plain"), Grafikdateien ("image/gif", "image/jpeg") oder applikationsspezifische Formate wie "application/msword" zurücksenden. Um Daten an einen Browser zu übermitteln, erzeugen Sie einen Servlet-Ausgabestream, der mit dem Browser assoziiert ist, und rufen dann die println(String)-Methode dieses Streams auf. Servlet-Ausgabestreams werden durch die Klasse ServletOutputStream repräsentiert, die Teil des Pakets javax.servlet ist. Sie können einen dieser Streams erhalten, indem Sie die Methode getOutputStream() der Klasse HttpServletResponse aufrufen. Das folgende Beispiel erzeugt einen Servlet-Ausgabestream aus einem HttpServletResponse-Objekt namens response und schickt eine kurze Webseite an diesen Stream: ServletOutputStream out = response.getOutputStream(); out.println(""); out.println(""); out.println("Hello World! "); out.println(""); out.println("");
Listing 22.1 demonstriert ein Java-Servlet, das Daten aus dem Formular aus Abbildung 22.1 bezieht. Listing 22.1: Der vollständige Quelltext von Rot13.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
640
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class Rot13 extends HttpServlet { public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
Servlets entwickeln
11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: }
String text = req.getParameter("text"); String translation = translate(text); res.setContentType("text/html"); ServletOutputStream out = res.getOutputStream(); out.println(""); out.println(""); out.println("ROT-13 Translator "); out.println("ROT-13 Translator "); out.println("Text to translate:"); out.println("
"); out.println(""); out.println(translation); out.println(" "); out.println(" "); out.println("
"); out.println(""); out.println(""); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doPost(req, res); } String translate(String input) { StringBuffer output = new StringBuffer(); if (input != null) { for (int i = 0; i < input.length(); i++) { char inChar = input.charAt(i); if ((inChar >= 'A') & (inChar <= 'Z')) { inChar += 13; if (inChar > 'Z') inChar -= 26; } if ((inChar >= 'a') & (inChar <= 'z')) { inChar += 13; if (inChar > 'z') inChar -= 26; } output.append(inChar); } } return output.toString(); }
641
Servlets
Speichern Sie dieses Servlet, und kompilieren Sie es mit dem Java-Compiler. Das Rot13-Servlet empfängt Text aus einem WWW-Formular, übersetzt ihn mithilfe von ROT-13 und zeigt das Ergebnis dann in einem neuen WWW-Formular an. ROT-13 ist eine einfache Technologie zur Textverschlüsselung, bei der Buchstaben untereinander vertauscht werden. Jeder Buchstabe wird durch den Buchstaben ersetzt, der sich genau 13 Stellen weiter hinten im Alphabet befindet. A wird zu N, N wird zu A, B wird zu O, O wird zu B, C wird zu P, P wird zu C usw. Da die ROT-13-Verschlüsselung sehr einfach zu decodieren ist, benutzt man sie nicht zur Geheimhaltung wichtiger Informationen. Stattdessen begegnet man ihr hin und wieder in Internet-Diskussionsforen wie Usenet-Newsgroups. Wenn z. B. jemand in einer Kino-Newsgroup einen »Spoiler« postet (also einen Beitrag, in dem erzählt wird, wie ein Film ausgeht), könnte er ihn ROT-13-codiert posten, damit niemand den Text aus Versehen liest. Wollen Sie ein großes Geheimnis aus dem Film »Soylent Green« von 1973 erfahren? Dann decodieren Sie folgenden ROT-13-Text: Onyq jreqra fvr haf jvr Ivru mhrpugra! Qh zhffg nyyr jneara haq rf vuara fntra! Fblyrag terra jveq nhf Zrafpura trznpug! Qh zhffg rf vuara fntra! Fblyrag terra vfg Zrafpu! Damit man das ROT-13-Servlet benutzen kann, müssen Sie seine Klassendateien in einem Ordner auf Ihrem Webserver platzieren, der für Servlets bestimmt wurde. Bei einem Apache mit Tomcat finden sich Servlets häufig in einem WEB-INF/classes-Unterordner des Haupt-WWW-Ordners. Wenn also /htdocs das Stammverzeichnis Ihrer Website ist, würden Servlets in den Ordner /htdocs/WEB-INF/classes gehören. Man führt ein Servlet aus, indem man seine URL, also z. B. http://www.cadenhead.org/ servlet/Rot13, in die Adresszeile des Browsers tippt. Ersetzen Sie den ersten Teil der URL mit dem Namen oder der IP-Adresse Ihres Webservers. Sie können ein funktionierendes ROT-13-Servlet auf der Webseite zum Buch ausprobieren – besuchen Sie http://www.java21pro.com und öffnen Sie die Seite zu Tag 22.
22.3 Cookies Viele Websites lassen sich so anpassen, dass sie Informationen über Besucher und die Features sammeln, die sie auf der Site sehen wollen. Diese Anpassung wird durch ein Browserfeature namens Cookies ermöglicht. Dabei handelt es sich um winzige Dateien, in denen Informationen gespeichert werden, die sich eine Website über einen Besucher merken möchte, z. B. seinen Benutzernamen, die Zahl seiner Besuche usw. Diese Dateien
642
Cookies
werden auf dem Computer des Besuchers gespeichert. Eine Website kann nur die Cookies auf dem Rechner eines Besuchers lesen, die sie selbst erzeugt hat. Aus Datenschutzgründen können die meisten Browser so konfiguriert werden, dass sie sämtliche Cookies ablehnen oder zuerst um Erlaubnis fragen, bevor eine Site ein Cookie erzeugen darf. Die meisten Browser sind so eingestellt, dass sie alle Cookies annehmen. Mit Servlets können Sie leicht Cookies erzeugen und auslesen, während der Besucher das Programm ablaufen lässt. Cookies werden durch die Klasse Cookie im Paket javax.servlet.http unterstützt. Um ein Cookie zu erzeugen, rufen Sie den Konstruktor Cookie(String, String) auf. Das erste Argument ist der Name, den Sie dem Cookie geben wollen, und das zweite der Wert des Cookies. Ein möglicher Einsatzzweck für ein Cookie wäre mitzuzählen, wie oft jemand das Servlet geladen hat. Die folgende Anweisung erzeugt ein Cookie namens visits und gibt ihm den Startwert 1: Cookie visitCookie = new Cookie("visits", "1");
Wenn Sie ein Cookie erzeugen, müssen Sie festlegen, wie lange es auf dem Computer des Besuchers gültig bleiben soll. Cookies können eine Stunde, einen Tag, ein Jahr oder über eine beliebige Zeitspanne gültig bleiben. Sobald ein Cookie ungültig geworden ist, löscht der Browser es automatisch. Rufen Sie die Methode setMaxAge(int) auf, um festlegen, wie viel Zeit in Sekunden das Cookie gültig bleiben soll. Wenn Sie einen negativen Wert angeben, bleibt das Cookie nur so lange gültig, wie der Besucher seinen Browser geöffnet hält. Wenn Sie 0 als Wert angeben, wird das Cookie nicht auf dem Computer des Besuchers gespeichert. Der Sinn eines Cookies mit einer maximalen Verweildauer von 0 ist der, den Browser anzuweisen, ein Cookie zu löschen, falls bereits eines existiert.
Cookies werden zusammen mit den Daten, die der Browser anzeigt, an den Benutzercomputer übermittelt. Um ein Cookie abzuschicken, rufen Sie die Methode addCookie(Cookie) eines HttpServletResponse-Objekts auf. Sie können auch mehrere Cookies abschicken. Wenn Cookies auf einem Benutzercomputer gespeichert werden, werden sie mit der URL der Webseite oder des Programms assoziiert, das das Cookie erzeugt hat. Sie können mehrere verschiedene Cookies mit derselben URL assoziieren. Wenn ein Browser eine URL anfragt, überprüft er, ob er Cookies hat, die mit dieser URL assoziiert sind. Ist dies der Fall, schickt er sie mit der Anfrage mit.
643
Servlets
In einem Servlet rufen Sie die Methode getCookies() eines HttpServletRequest-Objekts auf, um ein Array von Cookie-Objekten zu erhalten. Sie können die Methoden getName() und getValue() der einzelnen Cookies aufrufen, um Informationen über die Cookies zu erhalten und mit den Daten weiterzuarbeiten. Listing 22.2 demonstriert SetColor, ein Servlet, bei dem der Besucher die Hintergrundfarbe der Seite mit dem Servlet festlegen kann. Die Farbe wird in einem Cookie namens color gespeichert, und das Servlet fragt jedes Mal, wenn es geladen wird, beim Browser nach diesem Cookie. Listing 22.2: Der vollständige Quelltext von SetColor.java 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34:
644
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class SetColor extends HttpServlet { public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String pageColor; String colorParameter = req.getParameter("color"); if (colorParameter != null) { Cookie colorCookie = new Cookie("color", colorParameter); colorCookie.setMaxAge(31536000); res.addCookie(colorCookie); pageColor = colorParameter; } else { pageColor = retrieveColor(req.getCookies()); } ServletOutputStream out = res.getOutputStream(); res.setContentType("text/html"); out.println(""); out.println(""); out.println("The U.S. Constitution "); out.println("The U.S. Constitution "); displayFile("constitution.html", out); out.println("Choose a new color "); out.println(""); out.println(" "); out.println(" "); out.println("
"); out.println("");
Cookies
35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: }
out.println(""); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doPost(req, res); } String retrieveColor(Cookie[] cookies) { String inColor = "#FFFFFF"; for (int i = 0; i < cookies.length; i++) { String cookieName = cookies[i].getName(); if (cookieName.equals("color")) { inColor = cookies[i].getValue(); } } return inColor; } void displayFile(String pageName, ServletOutputStream out) { try { ServletContext servletContext = getServletContext(); String filename = servletContext.getRealPath(pageName); FileReader file = new FileReader(filename); BufferedReader buff = new BufferedReader(file); boolean eof = false; while (!eof) { String line = buff.readLine(); if (line == null) eof = true; else out.println(line); } buff.close(); } catch (IOException e) { log("Error -- " + e.toString()); } }
Das Servlet SetColor zeigt den Inhalt einer HTML-Datei zusammen mit dem Rest der Seite an. Dieses Beispiel verwendet constitution.html, die Verfassung der USA in HTML. Sie können diese Datei auf der Website zum Buch http://www.java21pro.com bei Tag 22 herunterladen. Sie können natürlich genauso gut jede andere HTML-Datei verwenden, solange Sie die Zeilen 25-27 des Programms entsprechend anpassen.
645
Servlets
Nachdem Sie das Servlet kompiliert und es auf Ihren Server in das Servlet-Verzeichnis gelegt haben, können Sie es starten, indem Sie die URL des Servlets in einen Browser eingeben, also z. B. http://www.cadenhead.org/servlet/SetColor. Abbildung 22.2 zeigt das untere Ende der Seite, die das Servlet anzeigt.
Abbildung 22.2: Eine vom Servlet SetColor erzeugte Webseite
Um die Hintergrundfarbe der Seite zu ändern, geben Sie einen neuen Wert im Textfeld »Choose a new color« ein und klicken auf den »Change Color«-Button. Farben werden als #-Zeichen, gefolgt von drei zweistelligen Hexadezimalzahlen (in Abbildung 22.2 sind es die Zahlen AA, FF und AA), angegeben. Diese Zahlen repräsentieren den Rot-, Grün- und Blauanteil der Farbe, wobei die Werte von einem Minimum 00 bis zu einem Maximum FF gehen können. Wenn Sie sich mit Hexadezimalzahlen nicht auskennen, können Sie folgende Werte beim Austesten des Servlets einsetzen:
#FF0000: Rot
#00FF00: Grün
#0000FF: Blau
#FFAAAA: Hellrot
#AAFFAA: Hellgrün
#AAAAFF: Hellblau
#FFCC66: Karamellfarben
646
Zusammenfassung
22.4 Zusammenfassung Hauptzweck der Klassen in den Paketen javax.servlet und javax.servlet.http ist der Austausch von Informationen mit einem WWW-Server. Java-Servlets stellen eine Alternative zum Common Gateway Interface dar, der üblichen Art und Weise, um mit Programmiersprachen Informationen im WWW einzuholen und Daten auszugeben. Da Servlets (abgesehen von einer grafischen Benutzerschnittstelle) alle Features der Sprache Java nutzen können, kann man damit hochkomplexe Webapplikationen erstellen. Servlets sind heute bei Onlineshops im Einsatz, wo sie Bestellungen von Kunden aufnehmen, Produktdatenbanken auslesen und Rechnungsdaten erfassen. Mit Servlets werden auch Diskussionsforen, Content-Management-Systeme und viele andere Arten dynamisch generierter Websites betrieben. Morgen sehen wir uns die andere Hälfte von Tomcat an: JavaServer Pages.
22.5 Workshop Fragen und Antworten F
Kann man ein Java-Applet mit einem Servlet kommunizieren lassen? A
Wenn Sie wollen, dass das Applet weiterläuft, nachdem es den Server kontaktiert hat, dann muss das Servlet auf demselben Rechner sein wie die Webseite mit dem Applet. Aus Sicherheitsgründen dürfen Applets nur Netzwerkverbindungen zu dem Rechner herstellen, auf dem sie gehostet werden. Wenn ein Applet ein Servlet laden soll, können Sie die getAppletContext()Methode des Applets aufrufen, um ein AppletContext-Objekt zu erhalten. Rufen Sie dann die showDocument(URL)-Methode des Objekts auf, wobei die URL des Servlets als Argument eingesetzt wird.
647
Servlets
Quiz Überprüfen Sie die heutige Lektion, indem Sie die folgenden Fragen beantworten.
Fragen 1. Wenn ein Servlet gleichzeitig von fünf verschiedenen Surfern benutzt wird, wie oft wird dann die init()-Methode des Servlets aufgerufen? (a) 5 (b) 1 (c) 0–1 2. Welche Technologie ist nicht Teil von Tomcat? (a) Java-Servlets (b) JavaServer Pages (c) Java-API für die XML-Verarbeitung 3. Wenn Daten von einem Formular an eine Webseite übermittelt werden und diese dann in der Adresszeile des Browsers als Teil der URL auftauchen, welche Art von Anfrage wurde dann benutzt? (a) GET (b) POST (c) HEAD
Antworten 1. c. Die init()-Methode wird aufgerufen, wenn der WWW-Server das Applet zum ersten Mal lädt. Dies kann geschehen sein, bevor irgendeiner der fünf Besucher das Servlet anfordert. Daher kann init() einmal oder keinmal aufgerufen worden sein. 2. c. Die Java-API für die XML-Verarbeitung 3. a. Das GET-Kommando codiert Formularfelder in die URL und schickt dann diese URL an den Browser.
648
Workshop
Prüfungstraining Die folgende Frage könnte Ihnen so oder ähnlich in einer Java-Programmierprüfung gestellt werden. Beantworten Sie sie, ohne sich die heutige Lektion anzusehen oder den Code mit dem Java-Compiler zu testen. Gegeben sei: public class CharCase { public static void main(String[] arguments) { float x = 9; float y = 5; char c = '1'; switch (c) { case 1: x = x + 2; case 2: x = x + 3; default: x = x + 1; } System.out.println("Value of x: " + x); } }
Welcher Wert von x wird ausgegeben? a. 9.0 b. 10.0 c. 11.0 d. Nichts, denn das Programm lässt sich so nicht kompilieren. Die Antwort finden Sie auf der Website zum Buch unter http://www.java21pro.com. Besuchen Sie die Seite zu Tag 22, und klicken Sie auf den Link »Certification Practice«.
649
Servlets
Übungen Um Ihr Wissen über die heute behandelten Themen zu vertiefen, können Sie sich an folgenden Übungen versuchen:
Erstellen Sie eine modifizierte Version des Servlets SetColor, in der man auch die Textfarbe der Seite ändern kann.
Erzeugen Sie ein Servlet, das die Daten, die in ein Formular eingegeben werden, als Datei speichert.
Soweit einschlägig, finden Sie die Lösungen zu den Übungen auf der Website zum Buch: http://www.java21pro.com.
650
JavaServer Pages
3 2
JavaServer Pages
Mit Servlets kann man auf einfache Weise HTML-Text dynamisch generieren und Seiten erzeugen, die sich gemäß Besuchereingaben und -daten ändern. Dagegen sind Servlets nicht ideal zur Erzeugung von HTML-Text, der sich nie ändert, denn es ist eine ziemlich mühselige Angelegenheit, HTML mithilfe von Java-Anweisungen auszugeben. Servlets erfordern zudem immer die Dienste eines Java-Programmierers, wenn sich der HTML-Code ändern soll. Das Servlet muss bearbeitet, erneut kompiliert und wieder ins Internet hochgeladen werden. Wenige Firmen werden diese Aufgabe einem laien überlassen wollen. Die Lösung dieses Problems heißt JavaServer Pages. Diese erzeugen Dokumente, die statisches HTML mit der Ausgabe von Servlets und Elementen von Java wie Ausdrücken und Java-Anweisungen mischen. Heute sehen wir uns die folgenden Themen näher an:
Wie Webserver JavaServer Pages unterstützen
Wie man eine JavaServer Page erzeugt
Wie man Servlet-Variablen auf einer Seite benutzt
Wie man Java-Ausdrücke auf einer Seite benutzt
Wie man Java-Anweisungen auf einer Seite benutzt
Wie man Java-Variablen auf einer Seite deklariert
Wie man ein Servlet auf einer Seite aufruft
23.1 JavaServer Pages Wie Sie bereits gestern erfahren haben, ist JavaServer Pages Teil der Tomcat-Spezifikation von Sun Microsystems und der Apache Software Foundation. JavaServer Pages sind keine Konkurrenz zu Servlets, sondern eine Ergänzung. Sie erleichtern es, zwei Arten von WWW-Content zu trennen:
statischer Content – die Bereiche einer WWW-Seite, die sich nicht ändern, wie z. B. die Beschreibung der einzelnen Produkte eines Onlineshops.
dynamischer Content – die Bereiche einer WWW-Seite, die von einem Servlet generiert werden, z. B. die Preis- und Verfügbarkeitsdaten für die einzelnen Produkte, die sich je nach Verkaufszahlen der Produkte ändern können.
Wenn Sie bei einem derartigen Projekt nur Servlets verwenden, wird es ziemlich umständlich, kleinere Änderungen wie die Korrektur von Tippfehlern, die Umformulierung eines
652
JavaServer Pages
Absatzes oder eine Veränderung der Seitendarstellung durch Anpassung der HTML-Tags durchzuführen. Jede Veränderung erzwingt eine Bearbeitung, Kompilierung, ein Testen und Hochladen des Servlets. Mit JavaServer Pages können Sie den statischen Content einer Webseite in ein HTMLDokument legen und Servlets aus diesem Dokument heraus aufrufen. Sie können auch andere Teile der Sprache Java auf einer Seite nutzen, z. B. Ausdrücke, if-then-Blöcke und Variablen. Ein WWW-Server mit Tomcat-Unterstützung erkennt, wie diese Seiten zu lesen sind und wie er den enthaltenen Java-Code auszuführen hat. Er erzeugt daraus ein HTML-Dokument so, als hätten Sie für die ganze Aufgabe ein Servlet geschrieben. Und technisch gesehen, erledigt JavaServer Pages wirklich alles mit Servlets. Sie erzeugen eine JavaServer Page ganz so, wie Sie ein HTML-Dokument erstellen würden – mit einem Texteditor oder einem Webdesignerprogramm wie Microsoft FrontPage 2002 oder Macromedia Dreamweaver. Wenn Sie die Seite abspeichern, benutzen Sie die Dateierweiterung .jsp, um anzugeben, dass es sich um eine JavaServer Page und nicht um ein HTML-Dokument handelt. Danach kann die Seite auf einem WWW-Server wie ein HTML-Dokument publiziert werden (allerdings muss der Server Tomcat unterstützen). Wenn ein Besucher zum ersten Mal die JavaServer Page anfordert, kompiliert der Webserver ein neues Servlet, das die Seite repräsentiert. Dieses Servlet kombiniert alles, dwas in die Seite gesteckt wurde:
mit HTML ausgezeichneten Text
Aufrufe von Java-Servlets
Java-Ausdrücke und -Anweisungen
spezielle JavaServer-Pages-Variablen
Eine JavaServer Page schreiben Eine JavaServer Page besteht aus drei Arten von Elementen, die jeweils ihr eigenes Auszeichnungs-Tag haben, das HTML ähnelt:
Scriptlets – Java-Anweisungen, die nach dem Laden der Seite ausgeführt werden. Diese Anweisungen sind jeweils von den Tags <% und %> umgeben.
Ausdrücke – Java-Ausdrücke, die bewertet werden und Ausgaben generieren, die auf der Seite angezeigt werden. Sie sind von den Tags <%= und %> umgeben.
Deklarationen – Anweisungen, um Instanzvariablen zu erzeugen und andere Aufgaben zu erledigen, die für die Präsentation der Seite erforderlich sind. Sie sind von den Tags <%! und %> umgeben.
653
JavaServer Pages
Ausdrücke Listing 23.1 zeigt Ihnen eine JavaServer Page mit einem Ausdruck, einen Aufruf des Konstruktors java.util.Date(). Dieser Konstruktor erzeugt einen String mit der aktuellen Zeit und dem gegenwärtigen Datum. Geben Sie diese Datei mit einem beliebigen Texteditor ein, der Dateien als Nur-Text speichern kann. Auch der Editor, mit dem Sie die JavaQuelltext-Dateien erzeugt haben, ist für diesen Zweck geeignet. Listing 23.1: Der komplette Text von time.jsp 1: 2: 3: Clock 4: 5: 6: 7: <%= new java.util.Date() %> 8: 9: 10:
Nachdem Sie diese Datei abgespeichert haben, laden Sie sie auf Ihren Webserver in das Verzeichnis mit den anderen Webseiten hoch. Im Gegensatz zu Java-Servlets, die in einem Ordner sein müssen, der für Servlets ausgewiesen ist, können JavaServer Pages in einem beliebigen Verzeichnis liegen, das vom Internet aus zugänglich ist. Wenn Sie die URL der Seite zum ersten Mal laden, kompiliert der Webserver die JavaServer Page zu einem Servlet. Deswegen lädt die Seite nur langsam, aber dies wiederholt sich nicht, denn der Server behält das Servlet für alle weiteren eventuellen Anfragen.
Abbildung 23.1: Ein Ausdruck in einer JavaServer Page
654
JavaServer Pages
Die Ausgabe von time.jsp können Sie in Abbildung 23.1 sehen. Wenn eine JavaServer Page einen Ausdruck beinhaltet, wird er bewertet, um einen Wert zu erzeugen, und auf der Seite angezeigt. Wenn der Ausdruck bei jeder Ausführung einen anderen Wert generiert, wie dies bei time.jsp der Fall ist, dann spiegelt sich dies immer dann in der Seite wieder, wenn sie im Browser geladen wird. Es gibt einige Servlet-Variablen, auf die Sie sich in Ausdrücken und anderen Elementen einer JavaServer Page beziehen können:
application – der Servlet-Kontext, über den mit dem Webserver kommuniziert wird
config – das Servlet-Konfigurationsobjekt, mit dem man feststellt, wie das Servlet initialisiert wurde
out – der Servlet-Ausgabestream
request – die HTTP-Servlet-Anfrage
response – die HTTP-Servlet-Antwort
session – die aktuelle HTTP-Session
Jede dieser Variablen bezieht sich auf ein Objekt, mit dem sie gestern gearbeitet haben. Sie können dieselben Methoden, die in einem Servlet zur Verfügung stehen, auch aus einer JavaServer Page heraus aufrufen. Listing 23.2 zeigt den Quelltext der nächsten Seite, die wir erstellen, environment.jsp, die demonstriert, wie die Variable request auf einer Seite benutzt werden kann. Diese Variable repräsentiert ein Objekt der HttpServletRequest-Klasse, und Sie können die getHeader(String)-Methode des Objekts aufrufen, um HTTP-Header anzufordern, die die Anfrage detaillierter beschreiben. Listing 23.2: Der komplette Text von environment.jsp 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
Environment Variables