Persistenz

Ein weiterer Mechanismus, der in der JavaBeans-Spezifikation festgelegt ist, ist das Persistenzkonzept. Unter Persistenz versteht man, dass eine Komponente ihren Inhalt durch Serialisierung persistent (dauerhaft) speichern kann. Zum Inhalt dieser Komponente gehören in diesem Zusammenhang die Eigenschaften und die übrigen durch die JavaBeans-Komponente repräsentierten Daten, wie bspw. Bilddaten oder Audiodateien. Durch die Serialisierung von Komponenten wird eine einheitliche Schnittstelle festgelegt, wodurch JavaBeans-Komponenten ihren Inhalt bspw. auf der Festplatte abspeichern können. Die Daten werden hierbei durch spezielle API-Klassen gesammelt und an Streams weitergegeben. Die Serialisierungsspezifikation von JavaBeans gewährleistet daher, dass der Inhalt einer JavaBeans-Komponente nach einem einheitlichen Verfahren abgespeichert wird und somit zu einem späteren Zeitpunkt wieder geladen werden kann.

Durch das Persistenzkonzept wird es möglich, dass der Zustand einer Komponente jederzeit wieder hergestellt werden kann

Zusätzlich existiert in Java der Externalisierungsmechanismus, mit dem der Entwickler das Format der Datenspeicherung einer Komponenten manipulieren kann. Hiermit wird erreicht, dass die Daten einer JavaBeans-Komponente in einem bestimmten Dateiformat gespeichert werden, das von anderen JavaBeans-Komponenten oder Programmen gelesen werden kann.

kap914 

Abb. 9.14: Speicherung und Wiederherstellung von persistenten Komponenten

Um den Serialisierungsprozess fehlerfrei durchführen zu können, müssen folgende Regeln beachtet werden:

  • Nur Objekte, die die Interfaces java.io.Serializable- oder java.io.Externalizable implementieren, können serialisiert werden.
  • Alle Objekte eines Beans, das die Serializable-Schnittstelle implementiert, müssen serialisierbar sein, da ansonsten der Serialisierungsmechanismus eine Ausnahme vom Typ NotSerializableException erzeugt.
  • Der Serialisierungsprozess durchläuft die Beans-Hierarchie von oben nach unten.
  • Für die Klassen, die wieder hergestellt werden müssen, gelten die üblichen Sicherheitsbestimmungen, wie bspw. Code-Verifikation und Security-Management.
  • Die statischen Variablen einer JavaBeans-Komponente unterliegen dem Serialisierungsprozess nicht.
  • Soll ein bestimmter Datentyp nicht serialisiert werden, so muss er mit dem Schlüsselwort transient explizit angegeben werden. Im folgenden Beispiel wird die Eigenschaft OperatingSystem nicht serialisiert, da für sie das Schlüsselwort transient deklariert wird:

syntax 

String transient OperatingSystem= " "

Zur Serialisierung werden eine Reihe von Klassen verwendet. Die wichtigsten dieser Klassen sind hierbei ObjectInputStream zum Lesen von Daten aus einem InputStream-Objekt und ObjectOutputStream zum Schreiben der Daten in ein OutputStream-Objekt. Diese Klassen sind deshalb als Streams realisiert, um komplexe Objekte, und nicht lediglich einfache Datentypen wie Byte oder int, übermitteln zu können. Der Mechanismus der Persistenz von Objekten ist leicht implementierbar und erweiterbar, da hierzu lediglich die Schnittstelle Serializable zu implementieren ist. Hieraus ergibt sich keine Änderung des Programms, da keine Methoden implementiert werden müssen.

Die Klassen ObjectInputStream und ObjectOutputStream

Objekte können mit der Klasse ObjectOutputStream in ein OutputStream-Objekt geschrieben werden. Diese Objekte können dann wie gewöhnliche Streams bearbeitet werden. Objekte können folglich in eine Datei geschrieben oder über ein Netzwerk übertragen werden. Üblicherweise werden serialisierte Objekte mit der Methode writeObject() in eine Datei mit dem Suffix .ser gespeichert, eine Abkürzung für das Wort Serialisierung. Die Objekte müssen von einem ObjectInputStream-Objekt in der Reihenfolge gelesen werden, in der sie von der Methode writeObject() Methode geschrieben wurden. Der voreingestellte Serialisierungsmechanismus für ein Objekt speichert dann den Klassennamen, die Klassensignatur und alle nichttransient und nichtstatisch deklarierten Felder sowie Referenzen zu anderen Objekten. Mehrere Referenzen zu einem Objekt werden mit einem Mechanismus zum Referenz-Sharing geschrieben, so dass Objektgraphen wieder reproduziert werden können. Als Beispiel werden Instanzen der Klassen java.awt.Button und java.io.FileOutputStream erzeugt. Die zweite Instanz wird als buch.ser bezeichnet und dazu benutzt, um eine Instanz der Klasse java.io.ObjectOutputStream zu erzeugen. Im Anschluss wird die Methode writeObject() aufgerufen, um das Button-Objekt abzuspeichern. Die Methode flush() wird zum Schluss dazu eingesetzt, um den gesamten Stream in die gewünschte Datei zu schreiben:

syntax 

// Beispiel SchreibeApplet.java
// Speichern eines Buttons in der Datei buch.ser
import java.awt.*;
import java.applet.*;
import java.io.*;
public class SchreibeApplet extends Applet {

    java.awt.Button button1;
    public void init(){

      setLayout(null);
      setSize(426,162);
      button1 = new java.awt.Button();
      button1.setLabel("OpenJava");
      button1.setBounds(24,48,156,40);
      button1.setBackground(new Color(12632256));
      add(button1);

      try{

        FileOutputStream fos = new FileOutputStream("buch.ser");

        ObjectOutputStream oos = new ObjectOutputStream(fos);

        oos.writeObject(button1);
        oos.flush();

      } catch (Exception e) {

        System.out.println("Exception: " + e);

      }

    }

}

Für das oben dargestellte Beispiel soll nun eine Lesemethode geschrieben werden, damit die in der Datei buch.ser enthaltenen Daten wieder hergestellt werden können. Hierzu wird eine Instanz der Klasse java.io.FileInputStream erzeugt und dazu benutzt, um eine Instanz der Klasse java.io.ObjectInputStream zu erzeugen. Die Methode readObject() wird anschließend aufgerufen, um das Button-Objekt einzulesen. Diese Methode liefert ein Objekt zurück, das in den Button-Typ umgewandelt wird (Casting), um das Button-Objekt wieder herstellen zu können.

syntax 

// Beispiel restoreApplet.java
// Wiederherstellung der oben gespeicherten Daten
import java.awt.*;
import java.applet.*;
import java.io.*;
public class restoreApplet extends Applet {

    public void init() {

      setLayout(null);
      setSize(426,266);
      try {

        FileInputStream fis = new FileInputStream("buch.ser");

        ObjectInputStream oos = new ObjectInputStream(fis);

        Button b = (Button)oos.readObject();
        add(b);

      } catch (Exception e) {

        System.out.println("Exception: " + e);

      }

    }

}

Die Serialisierung von Klassen bedeutet im Normalfall keinen großen Aufwand. Eine zu serialisierende Klasse muss lediglich eines der zwei folgenden Interfaces implementieren: Entweder das Interface java.io.Serializable oder das Interface java.io.Externalizable. Es sei hier darauf aufmerksam gemacht, dass beim Testen der oben genannten Beispiele Security-Exceptions auftreten können. Man sollte daher den Appletviewer mit den entsprechenden Sicherheitseinstellungen benutzen.

Interface Serializable

Das Interface Serializable beinhaltet keine Methoden, die implementiert werden müssen. Aufgabe des Interfaces ist es, die Daten der Klasse automatisch zu speichern bzw. wieder herzustellen. Das Interface beinhaltet lediglich ein Feld, SerialVersionUID (hierzu siehe Kapitel 9.7).

Interface Externalizable

Soll exakt festgelegt werden, welche Objekte abzuspeichern sind, so reicht das Interface Serializable nicht aus. Ein Bean muss dann das Interface Externalizable implementieren. Diese Schnittstelle erlaubt es dem Programmierer, festzulegen, welche Daten gespeichert werden. Hierzu müssen die folgenden zwei Methoden implementiert werden:

  • WriteExternal(ObjectOutput) und
  • ReadExternal(ObjectInput)

Diese zwei Methoden erlauben ein beliebiges Abspeichern der Formate sowie der Daten der JavaBeans-Komponente. Beide erzeugen dann eine IOException, wenn ein Fehler beim Lesen oder Schreiben der Daten auftritt. Die Methode WriteExternal() wird daher verwendet, wenn Daten einer JavaBeans-Komponente gespeichert werden müssen. Im Gegensatz dazu dient die Methode ReadExternal() zum Lesen der serialisierten Daten.

Versionshaltung

Bei der Speicherung eines Objekts in Java mit Hilfe des Interfaces Serialization werden die Dateninhalte, der Objekttyp und eine Versionsidentifikation in der .ser Datei gespeichert. Die Versionsidentifikation liegt in Form einer 64 bit langen Zeichenkette vor, die als Prüfsumme (Hash-Code) erzeugt wird. Sie enthält Informationen über die Klassenstruktur und deren Inhalt, über den Klassennamen, nichtstatische oder transiente Datenfelder und Methoden. Diese Information wird als Stream Unique IDentifier (SUID) bezeichnet. Jede Änderung der Struktur eines Beans resultiert folglich in einer neuen SUID, wodurch Kompatibilitätsprobleme entstehen können. Um die SUID eines Beans abzufragen, wird die folgende Syntax verwendet:

syntax 

serialver -show

Nach der Eingabe dieser Anweisung erscheint eine grafische Oberfläche, in der der Klassenname eingegeben werden muss. Als Resultat wird anschließend die SUID einer Klasse ausgegeben. In Abb. 9-15 ist die Oberfläche dargestellt, die beim Aufruf von serialver -show und bei der Angabe des gewünschten Klassennamens, der in diesem Fall die oben genannte Klasse SchreibeApplet ist, erscheint.

kap915 

Abb. 9.15: Introspektion der Information „Serial Version"

Änderungen einer Klasse können hierbei zu großen Problemen führen. So kann es vorkommen, dass eine Klasse eine Datei lesen will, die aber in einer früheren Version geschrieben wurde. Dies kann zu einem Kompatibilitätsproblem führen. Im JDK sind einige Mechanismen aufgeführt, die diesbezügliche Änderungen zulassen. JavaBeans-Programmierer dürfen dementsprechend lediglich neu definierte Instanzvariablen und Interfaces einfügen, nicht aber existierenden Code-Fragmente ändern. Wenn bspw. die Variable preis, die im Beispiel MyBean1.java verwendet wurde, geändert und dementsprechend die Lese- und Schreibmethoden von int auf float geändert werden sollen und verhindert werden soll, dass der Datentyp umgesetzt wird, dann muss eine neue Variable des gewünschten Datentyps deklariert werden, anstatt die Variable umzuschreiben. Die Klasse enthält dann die folgenden Variablen mit den entsprechenden Methoden:

code 

import java.awt.*;
import java.beans.*;
public class myBean1 {

    //...

    private int preis = 2;
    private float preis2 = 2f;
    public int getPreis() {

      return preis;

    }

    public float getPreis() {

      return preis2;

    }

}

Diese Vorgehensweise garantiert, dass die neue Version des Beans mit serialisierten Dateien arbeiten kann, die auf einer alten Version basieren. Zusammengefasst weist der Serialisierungsmechanismus die folgenden Eigenschaften auf:

  • Der Serialisierungsmechanismus unterstützt Objekte durch das Lesen bzw. das Schreiben von Streams, die von älteren Komponentenversionen von JavaBeans- geschrieben wurden bzw. gelesen werden sollen.
  • Der Serialisierungsmechanismus von Java unterstützt die Kommunikation zwischen verschiedenen Versionen eines Objektes, die in verschiedenen Java-VMs ausgeführt werden.
  • Durch die Versionshaltung ist es möglich, Objekte zu identifizieren und diese, falls erforderlich, zu laden.

Beans und Java-Archive

Java-Archive wurden in JDK 1.1 eingeführt. Auf Binärebene entsprechen sie ZIP-Archiven. Das JAR-Format wurde vor allem deshalb eingeführt, um dem Anwender die Verpackung von JavaBeans zu erleichtern. Eine JavaBeans-Komponente besteht in der Regel aus mehreren Klassen mit ihren zugehörigen Ressource-Dateien, wie bspw. Bildern, Audiodateien und serialisierten Daten. Das Dateiformat JAR wird folglich dazu benutzt, um alle zu einer Komponente gehörenden Dateien zu einer einzigen Datei zusammenzufassen. Es ist sogar möglich, mehrere Komponenten in einer Datei zusammenzufassen und dadurch eine vollständige Komponentenanwendung in eine Datei zu packen. Hierdurch wird insbesondere eine effiziente Netzübertragung realisiert.

Die BeanBox (siehe Kapitel 9.8) bietet im File-Menü die Option an, ein ausgetestetes Bean sofort als JAR-Archiv abzuspeichern. In dieser Datei werden neben den Klassen bspw. auch Meta-Informationen gespeichert, die vom BDK erstellt worden sind. Im Verzeichnis der JAR-Datei wird zusätzlich eine HTML-Datei gespeichert, die den direkten Aufruf des Beans mit Hilfe des Appletviewers oder eines Browsers unterstützt.

Um aus einer Java-Klasse ein Bean zu erzeugen, muss eine Archivdatei erstellt werden. Diese Datei wird wie auch bei anderen Java-Anwendungen mit dem Befehl jar erzeugt. Auf den Befehl folgen der Name des Archivs, der Name einer Manifest-Datei und die Dateien, die zum Bean gehören.

syntax 

jar {ctx} {vfm0M} [JAR-Datei] [manifest-Datei] dateien

JAR benutzt zwei Kategorien von Optionen. Die Elemente der ersten Kategorie (c, t, und x) sind nicht kombinierbar, d. h. nur ein Element dieser Kategorie darf benutzt werden. Die Elemente der zweiten Kategorie (v, f, m, 0 und M) sind sowohl mit den Elementen der ersten Kategorie als auch untereinander kombinierbar. Die Beschreibung dieser Optionen findet sich in Tab. 9-4.

Option

Bedeutung

c

Erzeugen einer neuen Archivdatei.

t

Anzeige des Inhalts einer JAR-Datei.

x

Extraktion der angegebenen Dateien aus dem Archiv. Wenn keine Datei explizit angegeben ist, werden alle Dateien extrahiert.

v

Generiert ausführliche Ausgabeinformationen.

f

Spezifiziert den Dateinamen des Archivs.

m

Fügt Informationen aus der angegebenen Manifest-Datei an ein bestehendes Archiv an.

0

Null Kompression.

M

Es wird keine Manifest-Datei für das Archiv erzeugt.

Tab. 9.4: JAR-Kommandooptionen

Ein Manifest ist eine Textdatei, die zusätzliche Informationen zu den Klassen der Archivdatei enthält. So kann auf diese Weise bspw. festgelegt werden, welche Klassen im Archiv JavaBeans sind. Dies ist notwendig, da für ein Bean auch Hilfsklassen notwendig sein können, die selbst keine JavaBeans sind.

JavaBeans verfügen über zwei Ausführungsmodi. Zur Designzeit befindet sich eine JavaBeans-Komponente in einer Designoberfläche eines Entwicklungswerkzeuges. Hier kann die Klasse grafisch manipuliert werden. Für eine derartige Manipulation sind Informationen und Mechanismen notwendig, die zur Ausführungszeit der Anwendung nicht länger benötigt werden.

Zum Laden der Daten über das Internet muss beachtet werden, dass die Anzahl der zu übertragenden Daten-Bytes minimiert werden sollte. Es ist daher sinnvoll, bei der Erstellung der JAR-Datei anzugeben, welche Klassen im Archiv nur zur Designzeit benötigt werden, damit eine Übertragung während der Laufzeit verhindert wird.

kap916 

Abb. 9.16: Größenunterschied eines Beans zur Design- und zur Laufzeit

Ein Beispiel einer Manifest-Datei sieht wie folgt aus:

code 

Manifest-Version: 1.0
Name: myBean1.class
Java-Bean: True

Manifest-Dateien

Jedes JAR-Archiv kann eine Manifest-Datei beinhalten. Diese Datei beschreibt den Inhalt des Archivs. Eine JAR-Datei darf hierbei maximal eine Manifest-Datei beinhalten, die den folgenden Namen besitzen muss:

code 

META-INF/MANIFEST.MF

Ein JAR-Archiv kann auch eine Signaturdatei beinhalten. Diese besitzt die Form:

code 

META-INF/xxx.SF

xxx steht hierbei für einen beliebigen Namen, der aus maximal acht Zeichen bestehen darf. Alle Namen, also META-INF, MANIFEST.MF wie auch der Dateityp .SF, müssen in Großbuchstaben geschrieben werden. Die Manifest-Datei besteht aus einer Liste von Dateien, die innerhalb des JAR-Archivs vorhanden sind. Diese Liste beinhaltet zwingend alle Dateien, die signiert werden müssen; alle anderen Dateien müssen nicht unbedingt angegeben werden. Der erste Teil der Manifest-Datei besteht aus einer Zeile, die die folgende standardmäßige Versionsnummer enthält:

code 

Manifest-Version: 1.0

Einer Leerzeile folgt dann ein Eintrag in der Manifest-Datei nach dem Muster:

code 

Eintragsname: Eintragswert

Die wichtigsten Namen, die in Bezug auf JavaBeans definiert wurden, sind Java-Bean, Depends-On und Design-Time-Only. Diese Einträge werden im Folgenden betrachtet.

Eintrag Java-Bean

Der Name Java-Bean wird dazu benutzt, um JavaBeans-Komponenten zu identifizieren. Das folgende Beispiel definiert ein Bean mit dem Namen Bean1.class und eine Klasse, die einen Event-Adapter repräsentiert, die aber kein Bean ist:

code 

Name: Bean1.class
Java-Bean: True

Name: ClickButtonAdapter
Java-Bean: False

Eintrag Depends-On

Durch diesen Eintrag wird die Abhängigkeit eines Beans von anderen Dateien festgelegt. Das folgende Beispiel zeigt, dass das Bean2 vom Vorkommen zweier Bilddateien (OpenJava.gif und Pferd.gif) und von einer weiteren Klasse (Hallo.java) abhängig ist:

code 

Name: Bean2.class
Java-Bean: True
Depends-On: OpenJava.gif Pferd.gif
Depends-On: Hallo.class
Depends-On:

Dem Beispiel ist weiterhin zu entnehmen, dass Einträge gleichen Formats durch ein Leerzeichen getrennt werden können. Ein Name ohne Wertzuweisung hat keinen Einfluss auf die Manifest-Datei, wie die letzte Zeile des Beispiels zeigt.

Eintrag Design-Time-Only

Dieser Eintrag einer Manifest-Datei spezifiziert, ob die angegebenen Dateien zur Laufzeit benötigt werden, oder ob sie nur in der Designphase gebraucht werden. Der Wert dieses Eintrags ist entweder True, falls die angegebene Datei nur zu Designzwecken benötigt wird, oder False, was bedeutet, dass der Eintrag auch für den Ablauf des Beans wichtig ist:

code 

Name: Bean1.class
Java-Bean: True

Name: Bean1BeanInfo
Design-Time-Only: True

Zusätzlich zu den oben erwähnten Einträgen in der Manifest-Datei ist der Name Digest-Algorithm wichtig. Dieser Eintrag gibt den Namen des Algorithmus an, anhand dessen eine Prüfsumme über die Datei berechnet werden kann. Hiermit wird festgestellt, ob die Datei bei der Übertragung beschädigt wurde oder nicht. Java unterstützt die Algorithmen SHA und MD5. Das folgende Beispiel zeigt die Verwendung dieses Eintrags.

code 

Name: Bean1.class
Java-Bean: True
Digest-Algorithm: SHA MD5
SHA-Digest: xxx(Base64)
MD5-Digest: yyy(Base64)

Diese Eingaben werden bei der Erzeugung des JAR-Archivs automatisch in die Manifest-Datei eingefügt. Dies erfolgt, indem eine Manifest-Datei geschrieben wird, die dem folgenden Beispiel ähnelt:

code 

Manifest-Version: 1
Name: SchreibeApplet.class
Java-Bean: False

Diese Manifest-Datei wird manifest.mf genannt. Anschließend wird eine Archivdatei mit folgender Eingabe in der Kommandozeile erzeugt:

code 

jar cfm persistent.jar manifest.mf *.class

Diese Zeile veranlasst, dass eine JAR-Datei mit dem Namen persistent.jar erzeugt wird, die auf der Datei manifest.mf aufbaut und die alle Klassen im Verzeichnis beinhaltet. Im Folgenden wird die von JAR erzeugte Manifest-Datei betrachtet. Hierzu wird folgende Kommandozeile verwendet:

code 

jar xf persistent.jar META_INF

Als Ergebnis wird ein Verzeichnis namens META_INF erzeugt, das wiederum eine Manifest-Datei beinhaltet. Diese Manifest-Datei erweitert die ursprüngliche Datei, indem sie Informationen über alle im Verzeichnis vorhandenen Klassen enthält. Dazu kommt der Eintrag des Digest-Algorithmus der jeweiligen Klasse mit dem folgenden Verschlüsselungscode:

code 

Manifest-Version: 1
Name: SchreibeApplet.class
Java-Bean: False
Digest-Algorithms: SHA MD5
SHA-Digest: QLnU2feKTJUJW1aDeZSYkA2RTWE=
MD5-Digest: RT8BhaIFp6vT5rUwLBhOgA==
Name: RestoreApplet.class
Digest-Algorithms: SHA MD5
SHA-Digest: tNLvFDzuqFDZcl/OMHMkPQLMIqk=
MD5-Digest: RWuQg95r8nWqCoNoq4tMNw==

Zusammenfassung

In diesem Unterkapitel wurde der Persistenzmechanismus von JavaBeans erläutert. Es wurde insbesondere darauf aufmerksam gemacht, dass Eigenschaften, Methoden und Events, die nicht serialisiert werden dürfen, mit dem Schlüsselwort transient versehen werden müssen. Die folgenden Klassen sind grundsätzlich nicht serialisierbar:

  • Klassen des Packages java.io.
  • Viele Klassen des Packages java.net dürfen nicht serialisiert werden, insbesondere Instanzen der folgende Klassen: Socket, ServerSocket, MulticastSocket und DatagramSocket.
  • Die Klasse Runtime ist ebenfalls nicht serialisierbar, da sie eine Menge von systemabhängigen Funktionen kapselt.

Weiterhin wurde auf die Generierung einer JAR-Datei eingegangen. Dabei wurde explizit erwähnt, wie durch geeignete Einträge (bspw. Design-Time-Only) in der Manifest-Datei die Größe der JAR-Datei zur Laufzeit erheblich verkleinert werden kann, was eine geringere Ladezeit bei der Übertragung eines Beans über das Netz bedeutet.


SPNavRight SPNavRight SPNavRight
BuiltByNOF