Stylesheet-Kompilierung

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

XSLT ist eine Programmiersprache, die mittels XML-Syntax ausgedrückt wird. Das ist zwar kein Vorteil für den Computer, dafür aber für die Interpretierbarkeit durch einen Menschen. Bevor das Stylesheet verarbeitet werden kann, muß es erst in ein internes, maschinenlesbares Format übersetzt werden. Diese Vorgehensweise sollte Ihnen vertraut sein, weil dasselbe bei jeder Hochsprache geschieht. Sie, also der Programmierer, arbeiten mit einer Hochsprache, und ein Interpreter oder Compiler wandelt diese Sprache in ein Maschinenformat, das vom Computer ausgeführt werden kann.

Interpreter analysieren Quellcode und übersetzen ihn bei jeder Ausführung in Maschinencode. Im Fall von XSLT wird dadurch erforderlich, daß das Stylesheet mit einem XML-Parser in den Speicher eingelesen, in ein Maschinenformat übersetzt und dann auf Ihre XML-Daten angewandt wird. Das offensichtliche Problem ist hier die Performance, besonders wenn Sie bedenken, daß Stylesheets sich selten verändern. Normalerweise werden Stylesheets zu einem frühen Zeitpunkt im Entwicklungsprozeß definiert und bleiben statisch, während die XML-Daten mit jeder Clientanfrage dynamisch erzeugt werden.

Eine bessere Methode ist, das XSLT-Stylesheet nur einmal in den Speicher zu parsen, in Maschinenformat zu kompilieren und die maschinenlesbare Darstellung dann zur wiederholten Nutzung im Speicher zu lassen. Das wird Stylesheet-Kompilierung genannt und unterscheidet sich nicht vom Konzept der Kompilierung in einer beliebigen Programmiersprache.

Templates-API

Verschiedene XSLT-Prozessoren implementieren die Stylesheet-Kompilierung jeweils anders, so daß JAXP aus Gründen der Konsistenz das Interface javax.xml.transform.Templates enthält. Das ist ein relativ simples Interface mit der folgenden API:

public interface Templates {
  java.util.Properties getOutputProperties( );
  javax.xml.transform.Transformer newTransformer( )
    throws TransformerConfigurationException;
}

Die Methode getOutputProperties( ) liefert eine identische Kopie der mit dem <xsl:output>-Element assoziierten Eigenschaften, wie method="xml", indent="yes" und encoding="ISO-8859-1". Vielleicht erinnern Sie sich, daß java.util.Properties (eine Unterklasse von java.util.Hashtable) eine Zuordnung von Werten zu Schlüsseln, die Eigenschaftsnamen darstellen, ermöglicht. Da eine identische Kopie zurückgeliefert wird, können Sie die Properties-Instanz verändern und auf eine zukünftige Transformation anwenden, ohne das durch die Templates-Instanz repräsentierte, kompilierte Stylesheet zu beeinträchtigen.

Die Methode newTransformer( ) ist gebräuchlicher und erlaubt Ihnen eine neue Instanz einer Klasse zu erlangen, die das Transformer-Interface implementiert. Es ist dieses Transformer-Objekt, das Ihnen die Ausführung von XSLT-Transformationen gestattet. Da die Implementierung des Templates-Interface durch JAXP verborgen wird, muß es durch die folgende Methode von javax.xml.transform.TransformerFactory erzeugt werden:

public Templates newTemplates(Source source)
  throws TransformerConfigurationException

Wie in vorangegangenen Beispielen kann das Source-Objekt das XSLT-Stylesheet von vielen Speicherorten erhalten, unter anderem über einen Dateinamen, einen System-Identifikator oder sogar einen DOM-Baum. Unabhängig vom ursprünglichen Speicherort ist der XSLT-Prozessor gehalten, das Stylesheet in eine optimierte, interne Darstellung zu kompilieren.

Ob das Stylesheet tatsächlich kompiliert wird, liegt an der Implementierung. Sie können sich aber darauf verlassen, daß sich die Performance im Verlauf der nächsten paar Jahre kontinuierlich steigern wird, da diese Tools sich stabilisieren und die Anbieter die Zeit haben, Optimierungen anzuwenden.

Die folgende Abbildung zeigt das Verhältnis der Instanzen von Templates und Transformer.

Beziehung zwischen Templates und Transformer

Abbildung: Beziehung zwischen Templates und Transformer

Die sichere Verwendung von Threads ist ein wichtiger Punkt in jeder Java-Anwendung, besonders im Zusammenhang mit dem Web, wo viele Nutzer dasselbe Stylesheet nutzen. Wie obige Abbildung zeigt, ist eine Instanz von Templates zur Verwendung mit Threads geeignet und stellt ein einzelnes Stylesheet dar.

Während des Transformationsprozesses muß der XSLT-Prozessor die Statusinformationen aufrechterhalten und je nach aktuellem Client bestimmte Eigenschaften ausgeben. Dazu muß für jede gleichzeitige Transformation eine Instanz von Transformer eingesetzt werden.

Transformer ist eine abstrakte Klasse in JAXP, deren Implementierungen nicht zu umfangreich ausfallen sollten. Das ist ein wichtiges Ziel, weil Sie viele Kopien von Transformer erzeugen werden, während die Zahl der Templates-Objekte relativ gering bleibt. Transformer-Instanzen sind für die Verwendung in Threads vor allem deshalb nicht geeignet, weil sie Statusinformationen über die aktuelle Transformation enthalten. Ist die Transformation jedoch beendet, können diese Objekte wiederverwendet werden.

Ein Stylesheet-Cache

XSLT-Transformationen werden im allgemeinen auf einem gemeinsam genutzen Webserver mit einer großen Zahl gleichzeitiger Benutzer durchgeführt, daher macht es Sinn, wann immer möglich Templates-Objekte zu verwenden, um die Performance zu optimieren. Da jede Instanz von Templates für die Verwendung in Threads geeignet ist, sollte idealerweise eine einzelne Kopie von vielen Clients gemeinsam genutzt werden. Das reduziert die Häufigkeit, mit der Ihr Stylesheet in den Speicher geparst und kompiliert werden muß, ebenso wie den gesamten Speicherbedarf Ihrer Anwendung.

Der Code im folgenden Beispiel zeigt einen benutzerdefinierten XSLT-Stylesheet-Cache, der die niederen Aufgaben im Zusammenhang mit der Erstellung von Templates-Instanzen automatisiert und die Instanzen im Speicher ablegt. Dieser Cache hat den zusätzlichen Vorteil, daß er das lastModified-Flag der zugrundeliegenden Datei überprüft und sich selbst lädt, wenn das Stylesheet modifiziert wurde. Das ist wärend der Entwicklung einer Webanwendung außerordentlich nützlich, weil Sie einfach nach Änderungen an Ihrem Stylesheet auf den Aktualisieren-Knopf in Ihrem Webbrowser klicken müssen, um die Ergebnisse der letzten Änderungen zu betrachten.

Beispiel: StylesheetCache.java

package com.oreilly.javaxslt.util;

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

/**
 * Eine Tool-Klasse, die XSLT-Stylesheets zwischenspeichert.
 */
public class StylesheetCache {
   // XSLT-Dateinamen werden auf MapEintrag-Instanzen abgebildet
   // (MapEintrag wird weiter unten definiert)
   private static Map cache = new HashMap( );
  
   /**
     * Alle Stylesheets werden aus dem Speicher gelöscht und so der Cache geleert.
     */
    public static synchronized void flushAll( ) {
        cache.clear( );
    }
  
  /**
   * Ein spezifisches Stylesheet wird aus dem Speicher entfernt.
   *
   * @param xsltDateiname der Name des zu entfernenden Stylesheets.
   */
  public static synchronized void flush(String xsltDateiname) {
     cache.remove(xsltDateiname);
  }
  
  /**
   * Hole eine Transformer-Instanz für den angegebenen XSLT-Dateinamen.
   * Ein neuer Eintrag wird dem Cache hinzugefügt, wenn dies die erste Anfrage
   * für den angegebenen Dateinamen ist.
   *
   * @param xsltDateiname der Dateiname eines XSLT-Stylesheets.
   * @return ein Transformationskontext für das angegebene Stylesheet.
   */
  public static synchronized Transformer neuerTransformer(String xsltDateiname)
        throws TransformerConfigurationException {
    File xsltDatei = new File(xsltDateiname);
          
    // ermittle, wann die Datei auf der Festplatte zuletzt geändert wurde
    long xslLastModified = xsltFile.lastModified( );
    MapEintrag eintrag = (MapEintrag) cache.get(xsltDateiname);
          
    if (eintrag != null) {
       // wenn die Datei neuer ist als das gespeicherte Stylesheet,
       // dann entferne die Referenz auf den Eintrag
       if (xslLastModified > eintrag.lastModified) {
           eintrag = null;
       }
    }
  
    // erzeuge einen neuen Eintrag im Cache, falls nötig
    if (eintrag == null) {
        Source xslSource = new StreamSource(xsltDatei);
  
        TransformerFactory transFact = TransformerFactory.newInstance( );
        Templates templates = transFact.newTemplates(xslSource);
          eintrag = new MapEintrag(xslLastModified, templates);
          cache.put(xsltDateiname, eintrag);
        } 

    return eintrag.templates.newTransformer( );
  }

  // verhindere die Instantiierung dieser Klasse
  private StylesheetCache( ) {
  }

  /**
   * Diese Klasse repräsentiert einen Wert im Map-Objekt.
   */
  static class MapEintrag {
    long lastModified; // wann wurde die Datei modifiziert
    Templates templates;
    
    MapEintrag(long lastModified, Templates templates) {
       this.lastModified = lastModified;
       this.templates = templates;
    }
  }
}

Da diese Klasse ein Singleton ist, hat Sie einen private-Konstruktor und ausschließlich statische Methoden. Darüber hinaus ist jede Methode als synchronized deklariert, in der Absicht, Probleme mit Threads zu verhindern.

Das Herz dieser Klasse ist der Cache selbst, der als java.util.Map implementiert wurde:

private static Map cache = new HashMap( );

Obwohl HashMap nicht für eine Verwendung mit Threads geeignet ist, eliminiert die Tatsache, daß all unsere Methoden als synchronized gekennzeichnet wurden, grundsätzlich sämtliche Probleme der Gleichzeitigkeit. Jeder Eintrag im Map-Objekt enthält ein Schlüssel/Wert-Paar, das den Dateinamen eines XSLT-Stylesheets auf eine Instanz der Klasse MapEintrag abbildet. MapEintrag ist eine verschachtelte Klasse, die den Überblick über das kompilierte Stylesheet und die Uhrzeit der letzten Änderung an der Stylesheet-Datei hat:

static class MapEintrag {
    long lastModified; // wann wurde die Datei modifiziert
    Templates templates;
  
    MapEintrag(long lastModified, Templates templates) {
         this.lastModified = lastModified;
         this.templates = templates;
    }
}

Um Einträge aus dem Cache zu entfernen, gibt es zwei Methoden:

public static synchronized void flushAll( ) {
    cache.clear( );
}
public static synchronized void flush(String xsltDateiname) {
    cache.remove(xsltDateiname);
}

Die erste Methode entfernt schlichtweg alles aus dem Map-Objekt, während die zweite ein einzelnes Stylesheet entfernt. Ob Sie diese Methoden nutzen, liegt an Ihnen. Die Methode flushAll sollte beispielsweise zwecks ordentlicher Speicherbereinigung aus der destroy( )-Methode eines Servlets aufgerufen werden. Wenn Sie viele Stylesheets in einer Webanwendung haben, könnte auch jedes Servlet bestimmte Stylesheets über die flush(...)-Methode entfernen wollen. Wenn der Parameter xsltDateiname nicht gefunden wurde, ignoriert die Map-Implementierung die Anfrage.

Der Großteil der Interaktion mit dieser Klasse geschieht über die Methode neuerTransformer, die die folgende Signatur hat:

public static synchronized Transformer neuerTransformer(String xsltDateiname)
  throws TransformerConfigurationException {

Der XSLT-Dateiname wird als Parameter übergeben, damit die Überprüfung des letzten Dateizugriffs möglich wird. Wir nutzen die Klasse java.io.File zur Ermittlung der letzten Änderung an der Datei, die es dem Cache ermöglicht, sich selbst neu zu laden, wenn Änderungen am Stylesheet gemacht wurden. Hätten wir einen System-Identifikator oder ein InputStream-Objekt statt eines Dateinamens verwendet, hätte dieses selbständige, erneute Laden nicht realisiert werden können. Als nächstes wird das File-Objekt erzeugt und das lastModified-Flag geprüft:

File xsltDatei = new File(xsltDateiname);

// ermittle, wann die Datei auf der Festplatte zuletzt geändert wurde
long xslLastModified = xsltDatei.lastModified( );

Das kompilierte Stylesheet, dargestellt durch eine Instanz von MapEintrag, wird dann über das Map-Objekt geholt. Wenn der Eintrag gefunden wurde, wird dessen Zeitstempel mit dem Zeitstempel der aktuellen Datei verglichen und erlaubt so das automatische Neu-laden:

MapEintrag eintrag = (MapEintrag) cache.get(xsltDateiname);

if (eintrag != null) {
   // wenn die Datei neuer ist als das gespeicherte Stylesheet,
   // dann entferne die Referenz auf den Eintrag
   if (xslLastModified > eintrag.lastModified) {
        eintrag = null;
  }
}

Danach erzeugen wir einen neuen Eintrag im Cache, wenn die Objektreferenz des Eintrags immer noch null ist. Das wird durch Kapselung des File-Objekts in ein StreamSource-Objekt, Instantiierung einer Instanz von TransformerFactory und Verwendung dieser Factory-Instanz zur Erzeugung unseres Templates-Objekts erreicht. Das Templates-Objekt wird dann im Cache gespeichert, so daß es vom nächsten Client wiederverwendet werden kann:

 

// erzeuge einen neuen Eintrag im Cache, falls nötig
if (eintrag == null) {
    Source xslSource = new StreamSource(xsltDatei);

    TransformerFactory transFact = TransformerFactory.newInstance( );
    Templates templates = transFact.newTemplates(xslSource);

    eintrag = new MapEintrag(xslLastModified, templates);
    cache.put(xsltDateiname, eintrag);
  }

Zu guter Letzt wird ein nagelneues Transformer-Objekt erzeugt und an den Aufrufer zurückgeliefert:

return eintrag.templates.newTransformer( );

Die Rückgabe eines neuen Transformer-Objekts ist ein kritischer Punkt, weil nur das Templates-Objekt in Zusammenhang mit Threads verwendet werden kann, die Transformer-Implementierung jedoch nicht. Jeder Aufrufer bekommt seine eigene Kopie des Transformer-Objekts, damit mehrere Clients nicht miteinander kollidieren.

Eine potentielle Verbesserung in diesem Design könnte das Hinzufügen eines Zeitstempels letzterZugriff zu jedem MapEintrag-Objekt sein. Ein anderer Thread könnte dann alle paar Stunden ausgeführt werden, um Map-Einträge aus dem Speicher zu entfernen, wenn auf diese seit einem gewissen Zeitpunkt nicht mehr zugegriffen wurde. In den meisten Webanwendungen stellt das jedoch kein Problem dar, aber wenn Sie eine große Anzahl von Seiten haben, von denen auf einige nur selten zugegriffen wird, könnte das eine Möglichkeit sein, den Speicherbedarf des Cache zu reduzieren.

Eine weitere potentielle Änderung liegt darin, statt eines Dateinamens javax.xml.transform.Source-Objekte als Parameter an die neuerTransformer-Methode zu übergeben. Das würde allerdings die Funktionalität des automatischen Neuladens für alle Source-Typen unmöglich machen.

   

<< 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