Techniken zur Performance-Steigerung

(Auszug aus "Java und XSLT" von Eric M. Burke)

Eine häufige Kritik an XSLT betrifft seine Performance. Der zusätzlich Aufwand der Transformation von XML in ein anderes Format ist der Preis für die klare Trennung zwischen Daten und Programmierlogik und die Möglichkeit, Transformationen für verschiedene Clients anzupassen. In diesem Abschnitt beschäftigen wir uns mit Strategien zur Performance-Steigerung, ohne dabei die Vorteile von XSLT aufzugeben.

Die eigentliche XSLT-Transformation ist nicht immer die Hauptursache für Performance-Probleme. XML-Parser haben zwar einen wesentlichen Einfluß auf die Performance, aber auch Faktoren wie die Zeit für Datenbankzugriffe, für die Bearbeitung von Anwendungslogik und Netzwerkübertragungen spielen eine Rolle.

Zuviel Gewicht auf Performance und Optimierungstechniken zu legen, führt oft zu Code, der schwer oder gar nicht zu verstehen und zu pflegen ist. Aus rein technischer Sicht ist die schnellste Technologie die beste. Aus wirtschaftlicher Sicht spielt die Vermarktung und die Wartung eine oft viel größere Rolle als die Laufzeit-Performance. Eine Anwendung, die die Performance-Anforderungen erfüllt und gleichzeitig einfach zu pflegen ist, macht wirtschaftlich gesehen mehr Sinn, als eine auf Effizienz optimierte aber kryptische Anwendung, die zwar schnell läuft, aber nicht geändert werden kann, weil ihr Autor die Firma verlassen hat und niemand sonst den Code versteht.

Belastungstests mit JMeter

Eine Möglichkeit, den Durchsatz einer Anwendung zu bestimmen, ist die Simulation eines wirklichen Anwendungsfalls. JMeter von Apache realisiert dies für Webanwendungen und mißt die Antwortzeiten, während die Zahl von Nutzern ansteigt. JMeter ist in Java geschrieben und stellt die Antwortzeiten wie in folgender Abbildung graphisch dar.

Ausgabe von JMeter

Abbildung: Ausgabe von JMeter

Es handelt sich hier um die Ausgabe von JMeter Version 1.6 Alpha, so daß Änderungen bei späteren Versionen zu erwarten sind. Das GUI-Interface wurde für die Version 1.6 komplett überarbeitet, wobei einige Details noch nicht vollendet waren. In der Graphik repräsentieren die Punkte die tatsächlichen Antwortzeiten, die obere Linie zeigt die mittlere Antwortzeit und die untere Linie die Standardabweichung vom Mittelwert.

Die wichtigste Messung, im Graph auf der rechten Seite zu sehen, ist die mittlere Antwortzeit. Die Zeiten werden in Millisekunden angegeben, die mittlere Antwortzeit der Webanwendung beträgt also 151 Millisekunden.

Bei der Benutzung eines Werkzeugs wie JMeter sollte der Belastungstest für einige Minuten laufen, bis sich die mittlere Antwortzeit stabilisiert. In der Regel beanspruchen die ersten Tests mehr Zeit, da Java Klassen laden, Threads starten und Speicher anfordern muß. Die Performance ändert sich auch mit der Zeit, wenn ein Garbage-Collector läuft oder eine VM wie HotSpot Code-Optimierungen durchführt. Nur wenn die Anwendung Speicherlöcher aufweist oder die Anforderungen einfach nicht erfüllen kann, wird sich die Antwortzeit nicht einpegeln.

Die folgende Tabelle zeigt das Verhalten der Seite »Monatsansicht« vom Diskussionsforum beim Test mit JMeter. Die Tabelle zeigt die Szenarios im schlechtesten und im besten Fall. Andere Kombinationen von Servlet-Container, XSLT-Prozessor und Datenbank werden nicht dargestellt. Die Anzahl von Threads in der vierten Spalte steht für die Anzahl von simulierten Nutzern. Die letzte Spalte zeigt die mittlere Antwortzeit.

Tabelle: Antwortzeiten der Seite »Monatsansicht«

Servlet-Container XSLT-Prozessor Datenbank Threads Mittlere Antwortzeit (ms)
Tomcat 3.2.1 Xalan 2.0 Access 2000 1 130
'' '' '' 5 320
'' '' '' 10 760
'' '' '' 20 1600
Tomcat 4.0 SAXON 6.2.2 MySQL 1 18
'' '' '' 5 150
'' '' '' 10 320
'' '' '' 20 610

Die Tabelle zeigt natürlich kein vollständiges Bild. Sie verdeutlicht aber, daß man in vielen Fällen die Performance drastisch steigern kann, indem man andere Werkzeuge und Bibliotheken benutzt. Für die gezeigten Messungen wurde JMeter so eingestellt, daß bis zu 20 simulierte Nutzer gleichzeig, d.h. ohne Zeitverzögerung, eine Anfrage stellen. JMeter kann aber auch für zufällige Verzögerungen zwischen den Anfragen konfiguriert werden, was den Realweltbedingungen viel näher kommt.

Das Ziel dieses Tests war kein Benchmark für das Diskussionsforum, sondern vielmehr ein Vergleich der relativen Performance bei Benutzung verschiedener Werkzeuge. (Da XSLT-Prozessoren und Servlet-Container schnelle Entwicklungszyklen durchlaufen, können hier nicht die aktuellen Namen genannt werden.) Über einen Zeitraum von mehreren Stunden wurde das Diskussionsforum mit verschiedenen Kombinationen von Servlet-Containern, XSLT-Prozessoren und Datenbanken getestet. Tomcat 4.0 und SAXON 6.2.2 waren dabei etwas schneller als Tomcat 3.2.1 und Xalan 2.0, die deutlichste Leistungssteigerung wurde jedoch beim Wechsel von Microsoft Access auf MySQL verzeichnet.

Detailliertere Messungen

Im vorigen Beispiel wurde die Performance einer Anwendung mit Apaches JMeter bestimmt. Auf diese Weise kann gemessen werden, wie sich eine Anwendung unter Belastung verhält, und es können Vergleiche zwischen Konfigurationen verschiedener Software- und Hardwarekomponenten angestellt werden. Wenn eine Anwendung jedoch die Anforderungen unter Belastung nicht erfüllt, müssen zusätzliche Methoden eingesetzt werden, um die Flaschenhälse im Java-Code zu finden. Werkzeuge wie JMeter ermitteln die Performance aus der Sicht des Endnutzers, nicht aber für jede Methode einer Java-Anwendung.

Kommerzielle Lösungen

Es existieren verschiedene kommerzielle Profiling-Werkzeuge, z.B. JProbe von Sitraka, oder VMGears OptimizeIt. Unter anderem können diese Werkzeuge ermitteln, wie oft jede Methode einer Anwendung aufgerufen wird, wieviel Zeit jede Methode in Anspruch nimmt und wieviel Speicher belegt wird. Damit sind diese Werkzeuge in der Lage, die kritischen Stellen im Java-Code genau zu lokalisieren. Im allgemeinen beanspruchen nur wenige Methoden einen Großteil der Ressourcen. (Dies wird allgemein als die 80/20-Regel bezeichnet.) Mit einer Optimierung dieser Methoden erzielt man oft die größten Leistungssteigerungen, bei verhältnismäßig geringem Aufwand.

JVMPI

Das Java 2 SDK von Sun bietet einige Kommandozeilenoptionen, die das Java Virtual Machine Profiling Interface (JVMPI) aktivieren. Es schreibt detaillierte Profiling-Informationen in eine Protokolldatei, während die Anwendung läuft. Das folgende Beispiel zeigt die Hilfeseite, die bei Eingabe des Kommandos java -Xrunhprof:help dargestellt wird.

Beispiel: Kommandozeilenoptionen von JVMPI

C:\>java -Xrunhprof:help
Hprof usage: -Xrunhprof[:help]|[<option>=<value>, ...]

Option Name and Value   Description          Default
---------------------   -----------          -------
heap=dump|sites|all      heap profiling         all
cpu=samples|times|old    CPU usage              off
monitor=y|n              monitor contention       n
format=a|b            ascii or binary output      a
file=<file>      write data to file      java.hprof(.txt for ascii)
net=<host>:<port>   send data over a socket write to file
depth=<size>          stack trace depth           4
cutoff=<value>       output cutoff point     0.0001
lineno=y|n          line number in traces?        y
thread=y|n             thread in traces?          n
doe=y|n                  dump on exit?            y

Example: java -Xrunhprof:cpu=samples,file=log.txt,depth=3 FooClass

Obwohl es sich nur um ein experimentelles Feature der JVM handelt, ist es doch nützlich, wenn keine anderen Werkzeuge zur Verfügung stehen. Um Prozessor-Flaschenhälse aufzuspüren, sollte die Option cpu auf samples gesetzt werden, die eine statistische Schätzung auf Grundlage einer periodischen Leistungsmessung durchführt. Da diese Option zu drastischen Leistungseinbußen führen kann, ist sie standardmäßig ausgeschaltet.

Das folgende Beispiel zeigt einen kleinen Teil der Ausgabe des JVMPI für einen einzigen Durchlauf einer Java-Anwendung. Dieser Abschnitt ordnet die Methoden entsprechend ihres Zeitverbrauchs ein. Eine Methode deren Abarbeitung nur 20 ms beansprucht, die aber millionenmal aufgerufen wird, wird wahrscheinlich höher eingestuft als eine Methode, die eine Sekunde benötigt und dabei nur einmal aufgerufen wird.

Beispiel: Ein Teil einer JVMPI-Ausgabe

rank self accum count trace method
1 13.70% 13.70%      20 31 java.lang.ClassLoader.defineClass0
2 7.53% 21.23%      11 19 java.util.zip.ZipFile.getEntry
3 5.48% 26.71%       8 35 java.io.Win32FileSystem.getBooleanAttributes
4 4.11% 30.82%       6 26 java.util.zip.ZipFile.read
5 3.42% 34.25%       5 92 java.util.zip.Inflater.inflateBytes
6 3.42% 37.67%       5 6 java.lang.ClassLoader.findBootstrapClass
7 2.74% 40.41%       4 22 java.util.zip.ZipFile.getEntry
8 2.74% 43.15%       4 143 org.apache.xalan.templates
                    StylesheetRootnewTransformer
9 2.74% 45.89%       4 14 java.util.zip.ZipFile.open
10 1.37% 47.26%       2 4 java.net.URLClassLoader.defineClass

Die Datei kann mehrere Megabyte groß werden, abhängig von der Größe der Anwendung und der Zeitdauer, in der der Profiler läuft. Es stellt natürlich keine leichte Aufgabe dar, in einer Datei dieser Größe die Flaschenhälse zu finden, die von der Anwendung und nicht von den Klassenbibliotheken von Java erzeugt werden.

Ein großer Teil der JVMPI-Ausgabedatei besteht aus Stack-Traces. Jede Trace ist durch eine Nummer gekennzeichnet, und die Kommandozeilenoption depth bestimmt, wie viele Zeilen pro Stack-Trace angezeigt werden. Die fünfte Spalte der Tabelle im obigen Beispiel enthält die Trace-Nummer, die es ermöglicht, den eigentlichen Stack-Trace in der Datei zu finden:

TRACE 31:
   java.lang.ClassLoader.defineClass0(ClassLoader.java:Native method)
   java.lang.ClassLoader.defineClass(ClassLoader.java:486)
   java.security.SecureClassLoader.defineClass(SecureClassLoader.java:111)
   java.net.URLClassLoader.defineClass(URLClassLoader.java:248)
   java.net.URLClassLoader.access$100(URLClassLoader.java:56)
   java.net.URLClassLoader$1.run(URLClassLoader.java:195)

Wenn Sie die Tiefe des Stack-Trace vergrößern, wird es wahrscheinlicher, daß Ihr eigener Code irgendwo im Report auftaucht. Dadurch wird der Report natürlich größer. Die Traces zeigen, welche Methoden an einer Stelle mit hohem Rechenzeitbedarf beteiligt sind.

Firmen, die es sich leisten können, sollten über die Anschaffung eines Werkzeugs wie JProbe oder OptimizeIt nachdenken. Obwohl das Interface von JVMPI nahezu dieselbe Funktionalität bietet, ist die Ausgabe von JVMPI rein textbasiert und erfordert eine Menge Handarbeit, um die Probleme schließlich zu finden. Die kommerziellen Profiler erfordern natürlich auch eine Analyse, die Ergebnisse werden aber in graphischer Form dargestellt und sind somit leichter zu überblicken.

Unit-Tests

Die Effektivität mancher einfacher Meßprinzipien sollte nicht unterschätzt werden. Oft ist es am einfachsten, einen 15-Zeiler in Java zu schreiben, der eine bestimmte Funktionalität erfüllt. Denken Sie z.B. an ein kurzes Java-Programm wie unter XSLT-Verarbeitung mit Java: Ein einfaches Beispiel, das nur eine XSLT-Transformation durchführt. Verwenden Sie die Methode System.currentTimeInMillis( ), um die Zeit vor und nach einer Transformation zu bestimmen. Experimentieren Sie mit verschiedenen XSLT-Stylesheets, um den Ansatz mit der besten Performance zu finden.

Auf dieselbe Weise können eigenständige Tests für Datenbankzugriffe, wichtige Elemente Ihrer Anwendungslogik und Code zur Generierung von XML-Daten geschrieben werden. Da JUnit die Zeit für jeden Unit-Test ermittelt, ist es sinnvoll, diese Performance-Messungen mit aussagekräftigen Unit-Tests zu kombinieren.

Unabhängig von der Implementierung der einzelnen Testroutinen sollten die ersten Durchläufe immer ignoriert werden. Der Grund dafür ist, daß die Ergebnisse stark von der Zeit für das Laden der Java-Klassen und von Initialisierungen verfälscht werden. Ein guter Ansatz ist es, den ersten Unit-Test durchzuführen, ohne die Systemzeit zu messen. Dann sollte der Test mehrere tausendmal und die Zeitmessung dabei einmal über alle Läufe durchgeführt werden. Die mittlere Antwortzeit, die durch Division der Gesamtzeit durch die Anzahl der Durchläufe bestimmt wird, gibt ein viel genaueres Bild, als einige isolierte Messungen.

Dabei muß auch eine eventuelle Zwischenspeicherung berücksichtigt werden. In einer realen Anwendung ändern sich die Daten mit jeder Anfrage, so daß Transformationsergebnisse in der Regel nicht zwischengespeichert werden können. Unit-Tests, die wiederholt dieselbe Datei transformieren, stellen keine gute Näherung für das reale Verhalten dar, da der Prozessor die Transformationsergebnisse zwischenspeichern kann und so eine zu hohe Performance gemessen wird.

Der effektive Einsatz von XSLT-Prozessoren

Performance-Messungen sind der erste Schritt, um Java- und XSLT-Anwendungen schneller zu machen. Sind die Engpässe erst einmal gefunden, können sie auch beseitigt werden.

Das Zwischenspeichern von Stylesheets

Wie schon des öfteren in diesem Buch erwähnt wurde, stellt das Zwischenspeichern von XSLT-Stylesheets eine wichtige Technik zur Performance-Steigerung dar. JAXP beinhaltet für diesen Zweck das Interface Templates und die Realisierung eines Stylesheet-Caches wurde schon unter Stylesheet-Kompilierung vorgestellt. Die folgende Tabelle illustriert die Performance-Steigerung, die mit dem Templates-Interface bei der wiederholten Transformation einer kleinen XML-Datei erzielt wird. Bei diesem Test wurde dieselbe Transformation in verschiedenen Realisierungen je hundertmal durchgeführt.

Tabelle: Durch Zwischenspeichern erreichte Performance-Steigerung

Prozessor Ohne Templates Mit Templates Templates und zwischengespeichertes XML
Xalan 2.0 71.8ms 45.9ms 39.2ms
SAXON 6.2.2 52.7ms 37.3ms 34.2ms

In der Spalte »Ohne Templates« finden Sie die schlechteste Performance, da das Stylesheet für jede Transformation neu aus einer Datei eingelesen werden muß. In der nächsten Spalte wurde eine Instanz von Templates erzeugt und für jede Transformation wiederverwendet. Die Performance ist hierbei entscheidend angestiegen.

In der letzten Spalte der Tabelle wurden die XML-Daten eingelesen und als DOM-Dokument zwischengespeichert. Auf diese Weise muß die XML-Datei nicht bei jeder Anfrage neu vom Dateisystem eingelesen werden, sondern derselbe DOM-Baum kann für alle Transformationen verwendet werden, was zu einem weiteren Performance-Zuwachs führt.

Obwohl die Ergebnisse suggerieren, daß SAXON schneller als Xalan ist, muß das nicht generell gelten. Die Performance kann stark von der Größe der Eingabedateien und von den verwendeten Features von XSLT abhängen. Es ist ratsam, die Performance Ihrer speziellen Anwendung mit verschiedenen Werkzeugen zu testen, bevor Sie sich für eines entscheiden.

Das Zwischenspeichern von Ergebnissen

Wenn die XML-Daten dynamisch sind und sich bei jeder Anfrage ändern, kann man bestenfalls die XSLT-Stylesheets zwischenspeichern. Werden dagegen dieselben Daten wiederholt abgerufen, wie z.B. bei der Homepage einer Firma, macht es durchaus Sinn, nicht das XSLT-Stylesheet zwischenzuspeichern, sondern das Ergebnis der Transformation. Auf diese Weise wird die Transformation nur durchgeführt, wenn sich XML- oder XSLT-Daten tatsächlich ändern.

Das folgende Beispiel zeigt eine Hilfsklasse, die die Ergebnisse von XSLT-Transformationen zwischenspeichert. In dieser Implementierung müssen XML-Daten und XSLT-Stylesheet aus statischen Dateien kommen. Wenn sich der Zeitstempel einer Datei ändert, wird die Transformation neu ausgeführt. Ansonsten wird die gespeicherte Kopie des Transformationsergebnisses zurückgegeben.

Beispiel: ResultCache.java

package com.oreilly.javaxslt.util;

import java.io.*;
import java.util.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;

/**
 * Eine Hilfsklasse, die die Ergebnisse von XSLT-Transformationen zwischenspeichert.
 *
 * @author Eric M. Burke
 */
public class ResultCache {
  private static Map cache = new HashMap( );
  
  /**
   * Lösche alle Ergebnisse aus dem Cache.
   */
  public static synchronized void flushAll( ) {
    cache.clear( );
  }
  /**
   * Führe eine XSLT-Transformation durch.
   */
  public static synchronized String transform(String xmlFileName,
      String xsltFileName) throws TransformerException {
      
    MapKey key = new MapKey(xmlFileName, xsltFileName);
        
    File xmlFile = new File(xmlFileName);
    File xsltFile = new File(xsltFileName);
        
    MapValue value = (MapValue) cache.get(key);
    if (value == null || value.isDirty(xmlFile, xsltFile)) {
      // dieser Schritt führt die Transformation durch
      value = new MapValue(xmlFile, xsltFile);
      cache.put(key, value);
    }
  
    return value.result;
  }

  // vermeide die Instantiierung dieser Klasse
  private ResultCache( ) {
  }

  /////////////////////////////////////////////////////////////////////
  // eine Hilfsklasse, die einen Schlüssel in der Cache-Map darstellt
  /////////////////////////////////////////////////////////////////////
  static class MapKey {
    String xmlFileName;
    String xsltFileName;
    
    MapKey(String xmlFileName, String xsltFileName) {
      this.xmlFileName = xmlFileName;
      this.xsltFileName = xsltFileName;
    }
    public boolean equals(Object obj) {
      if (obj instanceof MapKey) {
        MapKey rhs = (MapKey) obj;
        return this.xmlFileName.equals(rhs.xmlFileName)
          && this.xsltFileName.equals(rhs.xsltFileName);
      }
      return false;
    }
    
    public int hashCode( ) {
      return this.xmlFileName.hashCode() ^ this.xsltFileName.hashCode( );
    }
  }

  /////////////////////////////////////////////////////////////////////
  // eine Hilfsklasse, die einen Wert in der Cache-Map darstellt
  /////////////////////////////////////////////////////////////////////
  static class MapValue {
    long xmlLastModified; // letzte Änderung der XML-Datei
    long xsltLastModified; // letzte Änderung der XSLT-Datei
    String result;
    
    MapValue(File xmlFile, File xsltFile) throws TransformerException {
      this.xmlLastModified = xmlFile.lastModified( );
      this.xsltLastModified = xsltFile.lastModified( );
      
      TransformerFactory transFact = TransformerFactory.newInstance( );
      Transformer trans = transFact.newTransformer(
        new StreamSource(xsltFile));
      
      StringWriter sw = new StringWriter( );
      trans.transform(new StreamSource(xmlFile), new StreamResult(sw));
      
      this.result = sw.toString( );
    }
    
    /**
     * @return true, wenn die letzte Änderung der XML- oder der XSLT-Datei
     * nach diesem Cache-Eintrag gemacht wurde
     */
    boolean isDirty(File xmlFile, File xsltFile) {
      return this.xmlLastModified < xmlFile.lastModified( )
        || this.xsltLastModified < xsltFile.lastModified( );
    }
  }
}

Das wichtigste Element dieser Klasse ist die Methode transform( ). Diese Methode übernimmt die Namen einer XML-Datei und eines XSLT-Stylesheets und liefert das zugehörige Transformationsergebnis als String zurück. Beim Auftreten eines Fehlers wird eine TransformerException ausgelöst:

public static synchronized String transform(String xmlFileName,
     String xsltFileName) throws TransformerException {

Der Cache wurde mit der Datenstruktur java.util.Map realisiert, die Daten als Paare von Schlüssel und Wert bereitgestellt. Die Hilfsklasse MapKey dient dabei als Schlüssel:

MapKey key = new MapKey(xmlFileName, xsltFileName);

File xmlFile = new File(xmlFileName);
File xsltFile = new File(xsltFileName);

Als nächstes wird der Wert aus dem Cache abgerufen. Die Hilfsklasse MapValue überwacht das Transformationsergebnis und die letzte Änderung jeder Datei. Bei der ersten Anfrage ist der Wert null. Ansonsten ermittelt die Methode isDirty( ), ob eine der Dateien geändert wurde:

  MapValue value = (MapValue) cache.get(key);
  if (value == null || value.isDirty(xmlFile, xsltFile)) {
    // dieser Schritt führt die Transformation durch
    value = new MapValue(xmlFile, xsltFile);
    cache.put(key, value);
  }

  return value.result;
}

Die XSLT-Transformation wird beim Erzeugen eines neuen MapValue durchgeführt. Wenn keine Ausnahmen ausgelöst werden, wird das Transformationsergebnis an den Aufrufer zurückgegeben.

Verglichen mit den Ergebnissen aus der Tabelle Durch Zwischenspeichern erreichte Performance-Steigerung ist dieser Ansatz wesentlich schneller. Wurde die initiale Transformation einmal durchgeführt, beträgt die mittlere Antwortzeit weniger als eine Millisekunde.

Dieser Ansatz ist für Anwendungen mit einer Sammlung von statischen Dateien sehr einfach umzusetzen, nicht aber für Anwendungen, die auf einer Datenbank basieren. Da solche dynamischen Zugriffe bei jedem Aufruf neue XML-Daten erzeugen können, ist es nicht möglich, die Transformationsergebnisse in einem Cache abzulegen. Das größte Problem beim dynamischen Zwischenspeichern sind veraltete Daten. Wenn das Ergebnis einer XSLT-Transformation im Cache gespeichert ist, während sich die zugrundeliegende Datenbank ändert, muß der Cache aktualisiert werden, damit die Nutzer die richtigen Daten sehen.

Angenommen wir wollten das "Diskussionsforum" um das Zwischenspeichern von Ergebnissen erweitern. Da Nachrichten, nachdem sie einmal aufgenommen wurden, nicht mehr geändert werden können, ist es sinnvoll, die Seite »Zeige Nachricht« mit dieser Funktionalität auszustatten. Dazu könnte man einen Cache für eine feste Anzahl von Nachrichten verwenden. Immer wenn ein Nutzer eine Nachricht anzeigt, wird die erzeugte Webseite in den Cache aufgenommen. Wird die maximale Anzahl von Nachrichten überschritten, werden die ältesten Einträge entfernt.

Für sich öfter ändernde Seiten, wie z.B. »Zeige Monat«, muß eine Datenbankanfrage durchgeführt werden, um herauszufinden, wann die aktuellste Nachricht für das entsprechende Board eingegangen ist. Ist die zwischengespeicherte Webseite älter als die neueste Nachricht, muß die Transformation mit den aktuellen Daten erneut durchgeführt werden. Es muß von Fall zu Fall, je nach der Struktur der Datenbank, entschieden werden, ob eine Zwischenspeicherung sinnvoll ist.

Warnung:
Webanwendungen, die auf URL-Rewriting beruhen, um Session-Tracking zu realisieren, können keine Transformationsergebnisse zwischenspeichern, weil, wie unter Session-Tracking ohne Cookies beschrieben, jede URL dynamisch durch die jsessionid kodiert werden muß, wenn keine Cookies aktiviert sind.

Wie bei jeder Form der Optimierung muß auch hier der Nutzen eines Caches gegen die zusätzliche Komplexität abgewogen werden. Dazu ist es sinnvoll, Log-Dateien auszuwerten, um herauszufinden, welche Seiten am häufigsten abgefragt werden und die Optimierung hierauf zu konzentrieren.

Effektiver XSLT-Code

Die XSLT-Transformation kann selbst Ursache großer Performance-Einbrüche sein. Vermeiden Sie besonders bei großen XML-Dokumenten das wiederholte Einlesen von großen Teilen des XML-Baumes. Der Operator // ist in dieser Hinsicht besonders kritisch:

<xsl:apply-templates select="/.//name"/>

Mit dieser Anweisung wird das gesamte Dokument rekursiv beginnend an der Wurzel nach allen <name>-Elementen durchsucht. Da der XSLT-Prozessor nicht weiß, wo diese Elemente auftauchen können, muß jeder Knoten des Dokuments untersucht werden. Ist der entsprechende Pfad bekannt, kann der folgende effiziente Ansatz gewählt werden:

<xsl:apply-templates select="/firma/angestellter/name"/>

Wenn z.B. bestimmte Daten mehrfach in das Ergebnis kopiert werden müssen, können auch Variablen zur Performance-Steigerung zum Einsatz kommen. Um den Namen des Firmenbesitzers darzustellen, könnten Sie folgenden Code mehrfach ausführen:

<xsl:value-of select="/firma/besitzer/name/nachname"/>
<xsl:text> </xsl:text>
<xsl:value-of select="/firma/besitzer/name/vorname"/>

Ein effizienteres und besser lesbares XSLT-Stylesheet weist den Namen einer Variablen zu und verwendet diese jeweils bei der Ausgabe:

<!-- Ausgabe des Wertes der Variablen firmenBesitzer -->
<xsl:value-of select="$firmenBesitzer"/>

Außerdem können Sie, wann immer möglich, Inline-Code schreiben. Anstelle eines Aufrufs von <xsl:apply-templates> zum rekursiven Abarbeiten der XML-Daten geben Sie den aktuellen Knoten direkt mit <xsl:value-of> aus. Das kann zu dupliziertem Code führen, denn Templates dienen ja gerade der Modularisierung eines Stylesheets in wiederverwendbare Funktionsblöcke. Dies ist ein gutes Beispiel für das nötige Abwägen zwischen Übersichtlichkeit und Wartbarkeit auf der einen und der reinen Performance auf der anderen Seite.

Auch das Sortieren im XSLT-Code kann Performance-Probleme verursachen, weil dazu die gesamte Knotenmenge bearbeitet werden muß, bevor eine Ausgabe in den Ergebnisbaum geschrieben werden kann. Wenn es möglich ist, den Inhalt des XML-Dokumentes mit Java vorzusortieren, kann der XSLT-Prozessor das Dokument unter Umständen mit weniger Speicherbedarf transformieren, da ein Teil des Ergebnisses schon ausgegeben werden kann, bevor das gesamte Dokument verarbeitet wurde.

Schließlich kann die Performance durch das Schreiben kleinerer XSLT-Stylesheets gesteigert werden. Deshalb sollten, wann immer möglich, Cascading Style Sheets (CSS) verwendet werden, da die CSS-Style-Anweisungen in einer separaten Datei gespeichert werden und so die XSLT-Daten und der Ergebnisbaum wesentlich kleiner bleiben. JavaScript-Funktionen können auch in eigene Dateien ausgelagert werden, damit der JavaScript-Code nicht direkt im Stylesheet eingebettet werden muß.

CSS wurde in diesem Sinne für das "Diskussionsforum" eingesetzt. Es soll noch einmal betont werden, daß CSS nur zur Definition von Styles, wie Fonts, Einrückungen, Anordnungen und Farben, verwendet wird. Viele dieser Styles können auch direkt im HTML-Code definiert werden:

<h1 align="center">Überschrift</h1>

Durch die Definition der Anordnung in einer separaten CSS-Datei reduziert sich der HTML-Code zu:

<h1>Überschrift</h1>

Der vereinfachte HTML-Code bedingt wiederum eine Vereinfachung des XSLT-Stylesheets. Deshalb stellt CSS eine sinnvolle Ergänzung von XSLT dar.

Interaktionen mit EJB

Enterprise JavaBeans-, oder kurz EJB-Objekte, sind Software-Komponenten, die auf dem Server laufen und Geschäftslogik oder Datenzugriffe kapseln. Da EJBs innerhalb von Anwendungsservern laufen, wird auf sie typischerweise mit dem Interface Java Remote Method Invocation (RMI) zugegriffen. Das heißt, daß Methodenaufrufe von EJB-Komponenten über ein Netzwerk stattfinden und deshalb viel langsamer sind als lokale Methodenaufrufe innerhalb einer VM. Deshalb gibt es beim Senden von Daten vom und zum Anwendungsserver einiges zu beachten.

Das Senden von XML-Daten aus EJBs

Aus der Perspektive von Java und XSLT besteht die Frage darin, wo die XML-Daten erzeugt werden. Hier gibt es im wesentlichen zwei Möglichkeiten. Die erste ist es, die XML-Daten im EJB-Container zu erzeugen und so jedem Client, der die Beans verwenden will, eine reine XML-Schnittstelle zur Verfügung zu stellen. Eine Bean könnte zum Beispiel die folgenden Methoden zur Erzeugung der XML-Daten besitzen:

public String getLeaderXML( ) throws RemoteException;
public String getTeamMembersXML( ) throws RemoteException;
public String getProjectInformation( ) throws RemoteException;

Jede dieser Methoden liefert einen String mit den XML-Daten zurück. Die folgende Abbildung illustriert die Arbeitsweise dieses Modells.

Erzeugen von XML in der EJB-Schicht

Abbildung: Erzeugen von XML in der EJB-Schicht

Hinweis:
Eine Variation dieser Methode wäre die Verwendung einer Hilfsklasse zur Erzeugung der XML-Daten, anstelle der XML-Erzeugung in den Methoden des Beans.

Die EJB-Komponente erzeugt die XML-Daten, bevor eine Antwort an den Client geschickt wird. Die obige Abbildung zeigt zwar DOM, es kann aber auch JDOM oder jede andere API zur XML-Erzeugung verwendet werden. Der Client, ob Servlet-Container oder eigenständiges Java-Programm, bekommt vom Server nur XML-Daten zu sehen.

Jeder dieser XML-Strings kann ein wohlgeformtes XML-Dokument darstellen. Andererseits kann es auch sinnvoll sein, XML-Fragmente zurückzugeben. Der Client hat dann die Möglichkeit, viele dieser Fragmente zu einem komplexen XML-Dokument zusammenzufassen. Das Zusammenfügen der Fragmente erfordert zwar etwas mehr Aufwand vom Client, bietet aber auf der anderen Seite auch ein hohes Maß an Flexibilität.

Dieser Ansatz bietet scheinbar die sauberste Schnittstelle zur EJB-Schicht, denn solange sich die Struktur der XML-Daten nicht ändert, können Client und Server ihre internen Objektmodelle ändern, ohne den Kommunikationspartner zu beeinflussen.

Warnung:
Viele DOM-Implementierungen bestehen nicht aus serialisierbaren Java-Objekten. Außerdem muß ein Client nicht dieselbe DOM-Implementierung verwenden wie der Server. Deshalb ist es keine gute Idee, DOM-Bäume von einem EJB an einen Client zu senden.

Das Erzeugen von XML in der EJB-Schicht bringt aber auch Nachteile mit sich. Zum einen sind XML-Dokumente oft ziemlich groß, wodurch eine Komprimierung des Textes besonders bei großen XML-Dokumenten nötig wird. Dadurch werden zwar die Anforderungen an die Übertragungsbandbreite verringert, die Prozessorauslastung wächst aber sowohl beim Server für die Komprimierung als auch beim Client für die Dekomprimierung. Unterstützung hierfür bieten die Klassen java.util.zip.GZIPInputStream und java.util.zip.GZIPOutputStream.

Zum anderen ist es oft nicht möglich, die gesamte Geschäftslogik einer Anwendung innerhalb der EJB-Komponenten unterzubringen. Obwohl das sicher ein wünschenswertes Ziel wäre, bedeutet es doch, daß der Client für jede Operation eine Anfrage über das Netzwerk an den Server senden muß. Da nur XML-Daten an den Client geschickt werden, wird es sehr schwer, einen Teil der Geschäftslogik im Client unterzubringen.

Das Senden von Objekten aus EJBs

Eine andere Möglichkeit ist es, in den EJB-Komponenten gar kein XML zu verwenden. Statt dessen liefert jede Bean-Methode Instanzen von Hilfsklassen wie Employee oder ProjectInfo zurück. In diesem Ansatz kann auch der Client Geschäftslogik ausführen, indem er lokal die Methoden der zurückgelieferten Objekte aufruft. Die folgende Abbildung illustriert die XML-Erzeugung in diesem Modell.

Erzeugung von XML-Code im Client

Abbildung: Erzeugung von XML-Code im Client

Das Employee-Objekt wird serialisiert, so daß kein langer Text-String, sondern das serialisierte Objekt vom EJB-Container an den Client gesendet wird. Auf dem Clientrechner wird die DOM-API verwendet, um das Employee-Objekt in eine XML-Repräsentation zu überführen, die anschließend an den XSLT-Prozessor übergeben wird. Die Verwendung von DOM auf dem Client bringt es mit sich, daß die XML-Daten nicht komplett in Text konvertiert werden müssen, was die Transformation wiederum etwas beschleunigt.

Hinweis:
Die Erzeugung von XML-Code mit DOM ist heute eine gängige Praxis, stärker automatisierte XML Data-Binding-Technologien gewinnen jedoch an Bedeutung. Die Java Architecture for XML Data Binding von Sun (JAXB) stellt eine standardisierte Java-API zu Verfügung, die XML-Dokumente auf Java-Objekte abbildet und umgekehrt.

Obwohl die Anforderungen an die Übertragungsbandbreite bei serialisierten Objekten vergleichbar mit komprimierten XML-Daten sind, gestalten sich viele Anfragen einfacher, wenn Objekte und nicht XML-Text im Spiel sind. Denken Sie an eine Anwendung im Versicherungsgeschäft. Ein Arbeitnehmer kann einen Ehepartner, Kinder, einen Versorgungsempfänger und bestimmte Versicherungsleistungen haben. Wenn der Nutzer in dieser Anwendung von einer Seite zur anderen wechselt, kann das Objekt Employee in der HttpSession zwischengespeichert werden. Wenn neue Seiten zusätzliche Daten benötigen, können Methoden dieses Employee-Objekts aufgerufen werden:

// ein Ausschnitt aus einem Servlet...
if (employee.isMarried( )) {
  // ermittle den Ehegatten; mache dazu eine Anfrage bei der EJB-Schicht
  // falls der Ehegatte noch nicht nachgefragt und somit nicht im Cache ist
  Person spouse = employee.getSpouse( );
  // erzeuge die XML-Daten für den Ehegatten...
} else {
  // erzeuge XML-Daten für den Angestellten; keine Anfrage bei der EJB-Schicht
}

Dieses Codefragment zeigt, daß keine Anfrage an die EJB-Schicht abgesetzt wird, wenn der Arbeitnehmer ledig ist. Liefert die EJB-Schicht dagegen XML-Daten, ist es nahezu unumgänglich, daß bei jeder darzustellenden Webseite eine neue Anfrage an die EJB-Schicht gemacht werden muß. Der Grund dafür ist, daß die Webschicht nur große XML-Blöcke sieht, was die Integration von Geschäftslogik in dieser Schicht deutlich erschwert.

Der größte Vorteil bei der Versendung von Objekten gegenüber der Versendung von XML ist die Möglichkeit, die EJB-Schicht frei von Präsentationslogik zu halten. Die meisten Webanwendungen kombinieren die Daten von verschiedenen EJBs auf einer Webseite. Endnutzer wünschen häufiger Änderungen am Nutzerinterface als an den zugrundeliegenden Daten. In diesem Fall macht es Sinn, die EJB-Schicht unverändert zu lassen und den Code zur Erzeugung der XML-Daten in der Webschicht zu modifizieren.

   

zum Seitenanfang

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Java & XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Java und XSLT" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de