![]() |
|
URL dieses Artikels:
zu Ausgabe:
3.2005
Spieglein, Spieglein ...
Mapping von XML-Daten mittels JavaBeans-Technologie
von Stefan Roth
Sie haben sich schon öfters gefragt, wie Sie Ihre Daten aus der objektorientierten Welt auf einfache Weise in XML spiegeln können und umgekehrt? Sie sind es leid, umfangreiche Marshalling- und Unmarshallingroutinen zu schreiben, welche bei jedem hinzukommenden Property wieder mühsam angepasst werden müssen? Dann könnte der hier skizzierte Ansatz interessant für Sie sein: In diesem Verfahren wird ein automatisierter Abgleich der Daten über die von den JavaBeans bekannten PropertyChange-Events und BeanInfo-Klassen erreicht.
Einleitung und Voraussetzungen Egal, ob eine bestehende Objektstruktur in eine XML-Datei überführt werden soll oder erst ein neues Objektmodell erzeugt wird, welches möglichst auf Knopfdruck in XML serialisiert werden soll: immer kommt eines der bekannteren Low-Level-Verfahren wie SAX, DOM oder ein Vertreter der etwas komfortableren Low-Level-Verfahren wie JDOM oder DOM4J [1] zum Einsatz. Interessante Varianten zeigen die so genannten High-Level-Verfahren wie JAXB, Castor oder Zeuss. Das Verfahren ähnelt hierbei einem IDL-Compiler in CORBA: Aus einer festen Struktur (in CORBA das IDL-File, bei XML Schema-Beschreibungen wie DTDs oder XML-Schema) werden neue Java-Klassen generiert [2]. Beim Parsen des XML-Files werden entsprechend Objekte der neu generierten Klassen erzeugt. Unabhängig vom eingesetzten Verfahren stehen am Ende des Unmarshalling mehr oder weniger aussagekräftige Objekte von generierten Klassen oder Klassen des benutzten Frameworks. Um ein Mapping in die eigenen Business-Objekte kommt man nicht herum. Dieses kann mehr oder weniger fehleranfällig sein; bei einer Änderung beispielsweise eines einfachen Datums im Business-Objektmodell muss grundsätzlich der XML-Baum komplett neu generiert werden. Doch es geht auch anders: Warum nicht auch das bekannte Observer/Observable-Pattern auf die Ebene des XML-Mappings übertragen? Eine JavaBean kann interessierte Objekte (=Listener) grundsätzlich über eine Zustandsänderung informieren, indem sie ein PropertyChange-Event erzeugt. Listener-Objekte können dann auf die Zustandsänderung in für sie geeigneter Weise reagieren. Der gleiche Ansatz wird hier verfolgt: das Business-Objektmodell hält parallel XML-Strukturen, welche als Listener ausgelegt sind. Im Folgenden wird zunächst der Mechanismus des Datenabgleichs zwischen den Strukturen erklärt. Anschließend wird erläutert, wie für bestehende Business-Objekte entsprechende XML-Elemente erzeugt werden. Zum Schluss wird das Unmarshalling beleuchtet, wo also aus einer bestehenden XML-Datei ein Objektmodell kreiert wird, welches die eigenen Business-Klassen instantiiert. Im hier vorgestellten Ansatz kommt das Low-Level-API JDOM [3] zum Einsatz. Es ist grundsätzlich austauschbar gegen andere APIs. Wem also beispielsweise DOM4J [4] besser gefällt, der sollte ohne größere Schwierigkeiten den Code auf dieses API trimmen können. Die Business-Objekte aggregieren dabei jeweils ein JDOM-Objekt. So wird quasi parallel zum Business-Objektbaum ein JDOM-Objektbaum gehalten. Abb. 1: Verknüpfung von JDOM-abgeleiteten Klassen mit Business-Objekt-Klassen In Abbildung 1 ist der Zusammenhang noch einmal dargestellt: Die Klassen des Business-Objektmodells aggregieren spezielle von JDOM abgeleitete Klassen. Sie erweitern die Klassen Attribute und Element lediglich um das Interface PropertyChangeListener. Auf diese Weise wird das Objekt dieser spezialisierten Klasse über den PropertyChangeEvent-Mechanismus mit dem Business-Objekt in Verbindung gebracht. Datenpersistenz über PropertyChange-Events Auf Änderungen im Business-Objektmodell wird nun aufseiten der Listener unterschiedlich reagiert: Wird ein Property verändert, welches einem primitiven Datentyp entspricht, wird entsprechend ein XML-Attribut gesetzt: public synchronized void propertyChange(PropertyChangeEvent evt) {
Abb. 2: Auf Änderungen im Business-Objektmodell wird in Form eines Listeners reagiert Dieser Zusammenhang ist auch noch einmal in Abbildung 2 verdeutlicht. Hier ist ein Beispielmodell skizziert, in welchem ein Temperaturwert im Objektmodell geschrieben wird, was über das PopertyChange-Event eine Änderung im gleich strukturierten XML-Baum induziert. Wird hingegen ein Property eines komplexeren Typs gesetzt, muss ein komplettes XML-Element, welches der komplexeren Klasse entspricht, ausgetauscht werden. Damit darauf zugegriffen werden kann, müssen sämtliche Klassen des Business-Objektmodells, die für die XML-Schnittstelle vorgesehen sind, auch ein entsprechendes Interface definieren, über welches der Zugriff auf das JDOM-Pendant gewährleistet wird. Die (vereinfacht dargestellte) Listener-Methode für Elemente wird in Listing 1 gezeigt. Listing 1 public synchronized void propertyChange(PropertyChangeEvent evt) {
Wurde ein primitiver Datentyp geändert, wird in Form eines XML-Attributs reagiert. Bezieht sich die Änderung hingegen auf das Setzen eines komplexeren Objektes, so wird über das Interface XMLNode ein entsprechendes Sub-Element ersetzt, insofern es Bestandteil des Listener-Knotens war. Aber wieso geschieht dies nur im Falle von bereits vorhandenen Kind-Elementen? Wir betrachten hier die Änderungen im Falle vom Setzen von Properties. Zum initialen Setzen der JDOM-Pendants kommen wir gleich. Wir gehen also davon aus, dass wir lediglich auf Änderungen von bestehenden Elementen reagieren. Wie werden nun diese PropertyChange-Events ausgelöst? Vorausgesetzt ist, dass die Business-Objekte (hoffentlich) mit Properties arbeiten (wenn schon nicht im Sinne von Java 5, so zumindest der Java-Code-Konvention folgend; sodass sie als Properties gekennzeichnet in BeanInfo-Klassen auftauchen). Dieses Verfahren wäre ziemlich müßig, wenn jetzt bei jedem Ändern eines Properties die Benachrichtigung der Listener wieder manuell ausprogrammiert werden müsste. Unterstützung hierbei leisten moderne IDEs wie beispielsweise NetBeans. Sie ermöglichen es, per Mausklick Properties zu erzeugen mit dem dazu benötigten Code. Weiterhin ist die Möglichkeit gegeben, so genannte bound properties zu generieren. Hierbei wird das PropertyChangeEvent automatisch erzeugt und die Listener entsprechend benachrichtigt. Eine weitere angenehme Eigenschaft ist: Es erfolgt ein Eintrag in einer BeanInfo-Klasse, die nachher im Falle des Unmarshalling zum Tragen kommt und die Generierung der Business-Objekte aus einer XML-Quelle heraus vereinfacht und beschleunigt. Initiales Erzeugen der Listener zur Konstruktionszeit Irgendwann müssen die XML-Pendants erzeugt und in die Business-Objekte eingehängt werden. Dies geschieht zur Konstruktionszeit entweder direkt oder über eine Hilfsklasse XMLHelperClass, die Hilfsmethoden zur Verfügung stellt, um die Properties des Business-Objekts in einen XML-Knoten zu überführen. Dabei wurde die Konvention getroffen, dass primitive Datentypen einfach als XML-Attribute an den XML-Knoten angehängt werden, wohingegen komplexere Properties (die alle das Interface XMLNode implementieren) als Kind-Elemente eingehängt werden. Innerhalb der Methode mapXMLNodeToXMLElement werden diese Kind-Elemente wiederum über einen rekursiven Aufruf bestimmt. Das Ergebnis dieses rekursiven Aufrufes ist ein XML-Element, das als Kind-Element in das aktuelle XML-Element eingehängt werden kann. Name, Typ und Wert eines Properties werden über einen PropertyDescriptor bestimmt (Listing 2). ![]() Abb. 3: Erzeugen eines JDOM-Elements aus den Properties der Business-Objekt-Klasse Listing 2 public static HashMap getPropertiesList (Object obj) Listing 2 beinhaltet nur die relevanten Stellen des Code. Das vollständige Listing ist auf der Heft-CD enthalten. Über den PropertyDescriptor ist die Unterscheidung zwischen komplexem oder primitivem Typ des Properties möglich. Dadurch kann entschieden werden, ob ein weiteres Element in den XML-Baum eingehängt wird (im Falle des komplexen Properties) oder lediglich ein Attribut. Man muss einen XML-Baum nicht so aufbauen, aber auf diese Weise wird auch im umgekehrten Fall, nämlich dem späteren Unmarshalling, ein Erzeugen von Objekten des Business-Objektmodells begünstigt. Beim Traversieren des XML-Baums wird so zur Laufzeit entschieden, ob ein primitiver Datentyp aus einem XML-Attribut erzeugt werden muss oder ob es sich um einen komplexeren Datentyp handelt. Die Funktionsweise von mapXMLNodeToXMLElement ist zeigt Abbildung 3. Abbruchbedingung für die Rekursion ist entweder das Erreichen eines primitiven Datentyps (Mapping als Attribut) oder das Erreichen eigener customized Klassen. Die Methode mapXMLNodeToXMLElement kann beliebig an die eigenen Vorstellungen angepasst werden. Im Beispielcode wurde eine Klasse Parameter verwendet, die einen bestimmten Satz Werte hält und das XML-Element eigenständig ohne Zuhilfenahme der rekursiven Helper-Methoden verwaltet. Dadurch ist auch die Erzeugung von customized Knoten möglich. Im Beispiel der Parameter-Klasse ist das Erzeugen eines Knotens nach dem Namen der Variablen realisiert, sodass kein Knoten <Parameter> eingehängt wird, sondern ein Knoten mit dem Namen des Parameters, also <temperatureEntry>. Weiterhin können in den Klassen Exception-Listen für Attribute und Elemente definiert werden (Methoden des Interface XMLNode), die nicht in XML gemappt werden sollen (beispielsweise wie der transient-Modifikator bei der Serialisierung). Erzeugen von Objekten aus einer XML-Datenquelle Wie erzeugt man nun aus einer bestehenden XML-Struktur Instanzen der eigenen Klassen, die möglichst alle Werte aus der XML-Quelle übernehmen? Dieser Prozess ist mehrstufig. Zunächst einmal wird ein JDOM-Baum aus der XML-Datenquelle erzeugt. Dies ist noch relativ unspektakulär, das JDOM-API bringt bereits alles mit, um einen solchen Baum zu generieren. Anschließend muss der JDOM-Baum noch in einen customized JDOM-Baum konvertiert werden, damit der PropertyChange-Mechanismus funktioniert. Hierbei hilft wieder eine statische Hilfsmethode convertJDOMToCustomJDOM, die lediglich einen Copy-Konstruktor der Customized JDOM-Element-Klasse für das Wurzelelement aufruft. Hierfür ist eine Methode copy definiert, welche zunächst alle Attribute in customized Attribute überführt und anschließend für alle Kind-Elemente wiederum über einen rekursiven Aufruf den Copy-Konstruktor benutzt. So wird der gesamte Baum traversiert und ein entsprechender Baum aus customized JDOM-Knoten angelegt. Als letzter Schritt wird das Wurzelelement des gerade konvertierten Baums in das Wurzelelement der zu erzeugenden Objektstruktur eingehängt. Hierfür ist eine Methode setXMLNode über das Interface definiert (Listing 3): Listing 3 private static CoolingCircuit unmarshalXML (String strFilename) {
Was passiert nun in der Methode setXMLNode? Wieder jede Menge Rekursion: Für sämtliche Kind-Knoten des Baums muss direkt ein primitiver Datentyp erzeugt werden. Dies geschieht beispielsweise für ein XML-Attribut Name: hierfür wird ein String erzeugt. Für alle XML-Elemente des Baums hingegen müssen komplexe Objekte kreiert werden. Die Methode mapXMLElement der Hilfsklasse übernimmt diesen Dienst: jedes XML-Element hat einen Namen. Aus diesem Namen wird versucht, über einen PropertyDescriptor ein noch leeres Objekt zu erzeugen: object newObject = pDescriptor.getPropertyType().newInstance(); Außerdem kann wie angegeben die Setter-Methode des Properties durch pDescriptor ermittelt werden. Jedes XML-Element des JDOM-Baums steht für ein Business-Objekt, jedes Attribut für einen primitiveren Datentyp. Jeder Knoten der zu erzeugenden Business-Objekte implementiert aber das Interface XMLNode. Also kann newObject nach XMLNode gecastet werden. Das frisch erzeugte Objekt wird über den (indirekt) rekursiven Aufruf von setXMLNode mit Inhalt gefüllt. Anschließend wird die Setter-Methode des Properties mit dem neuen Objekt aufgerufen. Auf diese Weise entsteht für jedes Objekt des JDOM-Baums ein entsprechendes neues Business-Objekt. Durch die Setter-Methoden entsteht die Verknüpfung der neuen Objekte untereinander. Abschließende Betrachtungen Die hier vorgestellte Vorgehensweise zum Mappen von XML in die eigenen Business-Objekte kann am besten anhand des Beispiels im Sourcecode der Heft-CD nachvollzogen werden. Mittels einiger zwar etwas komplexerer Hilfsmethoden ist sowohl der Übergang von den eigenen Objektstrukturen zu XML als auch der umgekehrte Weg möglich. Über den PropertyChange-Mechanismus bleibt der XML-Baum immer auf dem neuesten Stand und kann jederzeit geschrieben werden, ohne ein komplettes Marshalling des Objektmodells zu erfordern. Dies kann insbesondere in hochdynamischen Umgebungen Sinn machen oder im Falle sehr großer Objektmodelle, in denen ein komplettes Marshalling eine zeitkritische Anwendung negativ beeinflussen könnte. Auf die gleiche Weise kann aus einer XML-Datei ein neues Objektmodell erzeugt werden, unter Verwendung der von Ihnen definierten Klassen. Diese müssen lediglich das Interface XMLNode implementieren, wofür die Klassen des Beispiels eine Orientierungshilfe darstellen. Über Ausnahmelisten kann auf bestimmte Objekttypen reagiert werden, für den Fall dass die Namensgleichheit zwischen XML-Knoten und eigener Klasse mal nicht gegeben oder gewollt ist. In einem weiteren Schritt wäre sogar das spezielle Markieren der Properties über Meta-Daten wie den in J2SE 5.0 eingeführten oder aus .NET bekannten Attributen denkbar, um so zu beschreiben, was in XML serialisiert werden soll und was nicht. Das Kühlkreislauf-Beispiel Auf der beiliegenden Heft-CD ist ein einfaches Beispiel implementiert, in dem einige Klassen eines Kühlkreislaufs definiert werden (Package xmlmapping.example). In einer Hauptklasse Main wird ein Objekt der Klasse CoolingCircuit erzeugt und die einzelnen Properties gesetzt. Anschließend wird das Modell in XML herausgeschrieben. Eine Änderung der Parameter wird in ein weiteres XML-File hinausgeschrieben. Zu guter Letzt wird ein bestehendes XML-File eingelesen, wiederum verändert und wieder hinausgeschrieben. Voraussetzungen und Bemerkungen Um das Beispiel auf der Heft-CD laufen lassen zu können, sind zwei Bibliotheken erforderlich: zum einen JDOM [3] in der aktuellen Version 1.0, zum anderen Xerces. Letzteres ist bereits im JDOM-Paket enthalten. Über Sinn und Zweck des Beispiel-Objektmodells kann gestritten werden. Es dient lediglich zur Veranschaulichung der in xmlmapping.mapping definierten Hilfsklassen, um das hier erläuterte Verfahren in die Praxis umzusetzen. Bitte beachten Sie auch, dass es sich um ein Verfahren handelt, welches aus einem größeren Projekt herausgelöst wurde und erst modifiziert werden musste, um es isoliert betrachten zu können. Auch handelt es sich um keine stabile und ausreichend getestete Library für die in irgendeiner Form Verantwortung übernommen werden kann oder Support geleistet wird. Hier wird lediglich eine Idee veranschaulicht. Stefan Roth studierte Informationstechnik im Maschinenwesen an der TU Berlin und promovierte an der TU-Berlin in Kooperation mit Siemens in Erlangen in den Jahren 2000-2003, wo das Verfahren auch eingesetzt wurde. Heute arbeitet er für das Online-Buchungsportal hotel.de in Nürnberg. Links & Literatur
|
||
|