Geschichte von JavaFX: JavaFX 1, JavaFX 2, JavaFX 8

Ursprünglich wollte Sun/Oracle JavaFX als Flash-Ersatz im Internet positionieren, doch dafür ist die Kombination HTML5 + CSS3 + JavaScript zu attraktiv. JavaFX ist in erster Linie eine großartige GUI-Bibliothek für klassische Client-Anwendungen, die langsam auch auf mobile Endgeräte vorrückt. So nahm die Entwicklung auch unterschiedliche Richtungen an.

JavaFX ist schon sehr lange in Entwicklung und viele interne Swing-Entwickler wurden bei Sun/Oracle auf das Projekt angesetzt – umgekehrt passierte bei AWT/Swing nicht mehr viel, Bugfixes kommen aber immer noch brav. Im Jahr 2007 wurde JavaFX auf der SunOne-Konferenz vorgestellt, Ende 2008 erschien das Release JavaFX 1.0 zusammen mit der Programmiersprache JavaFX Script. Die besondere Sprache machte es einfach möglich, hierarchische Objektgrafen aufzubauen, und bot eine nette Syntax für Object-Binding, sodass Zustände synchronisiert werden konnten.

Im Oktober 2011 erschien JavaFX 2.0 mit vielen Neuerungen und auch Änderungen. So wurde JavaFX Script entfernt, denn Oracle wollte keine weitere Programmiersprache aufbauen, sondern eine pure Java API, die Entwickler dann von unterschiedlichen existierenden Skriptsprachen ansprechen können. Das ist sicherlich eine gute Entscheidung, denn unter JavaScript und Groovy[1] sieht das sehr schlank aus, fast wie mit JavaFX Script. Mit dem Update auf JavaFX 2.0 ändert sich auch die API, sodass alter Code angefasst werden musste. Heute ist JavaFX 1.x ist veraltet, genauso wie die Literatur.

JavaFX entwickelte sich immer mehr als Alternative zu Swing/AWT und so integrierte Oracle im August 2012 das Java FX 2.2 SDK und die Runtime in das JDK 7u6 and JRE 7u6. Der Schritt war ungewöhnlich, denn so große Ergänzungen waren bisher im JRE/JDK noch nie gemacht worden. Neben der Integration bewegte sich auch das ehemals geschlossene JavaFX in Richtung Open-Source und mündete in der Implementierung OpenJFX. Mit dem OpenJDK und OpenJFX lässt sich ein komplett freies Java-System mit GUI-Stack unter der GPL bauen, was strikte Linux-Distributionen freuen wird. Die Offenheit führte schon zu sehr spannenden Experimenten, etwa einer Java-Version für iOS.

Mit Java 8 zieht auch JavaFX 8 fest in die Distribution ein, der nächste Sprung mit 3D-Unterstützung.


[1] http://groovyfx.org/

Best Practice: Teste eine Web-Anwendung in jedem Browser

Negativbeispiel vom Vorzeigeunternehmen Ryanair beim Versuch online einzuchecken und die Board-Karte zu beziehen. Hier im IE:

ryanair

Beim gleichen Vorgang in Chrome:

Segments - Ryanair.com

Also gelingt es in beiden Browsern nicht, die Board-Karte zu bekommen! Die Antwort vom Kundendienst:

Bezugnehmend auf Ihre aktuelle Korrespondenz moechten wir Sie bitten auf unsere Webseite die neueste Version Ihres Browsers benutzen. Auβerdem empfehlen wir Ihnen den Browser Verlauf und die Cookies zu löschen.

Ehr ungewöhnlich, dass man den neusten Browser verwenden soll, viele Unternehmen zwingen sich selbst noch zu so alten Trümmern wie dem IE 6. Und der selbst-aktualisierende Chrome soll nicht aktuell sein? Es ist eher ein Cookie-Problem, was aber nicht beim Anwender liegt, sondern klar bei den Web-Entwicklern. Weil Ryanair also schlechte IT-Produkte hat, müssen Benutzer zu allen möglichen Tricks greifen, um das Problem zu lösen, und wenn das nicht gelingt, auch noch bei der kostenpflichten Hotline anrufen. So macht man auch Geld …

Test-Werkzeuge wie Selenium helfen.

Best Practice: Behebe technische Fehler schnell und professionell

Was im Grunde offensichtlich ist, wird von Ryanair beflissen auf die lange Bank geschoben. Vor ein paar Tagen hatte ich schon das schlechte Fehlerhanding kritisiert, diese Fehlermeldung ist noch übler:

PaymentSportsChange - Ryanair.com

Wenn man einen Dienst anbietet, hat man:

  • Fehler schnell zu beheben (dieser Fehler kommt seit mehren Tagen beim Versuch ein Gepäckstück hinzuzufügen),
  • dem Benutzer in der jeweiligen Landessprache zuerst die Fehlermeldung zu präsentieren (diese große Webseite hat neun lokalisierte Fehlermeldungen untereinander, die Italiener müssen also ganz nach untern scrollen),
  • hinter dem Link “Bitte warten Sie 10 Sekunden und wenn Sie nicht automatisch zurückgeleitet werden, klicken Sie hier.” auch eine Aktion verbinden, und nicht als nutzloses “<a href="#" id="A2">klicken Sie hier</a>” realisieren (JavaScript gibt es auch nicht, sonst könnte man das ja noch verstehen),
  • die Fehlermeldung in das Standard-Template einbetten und keine rohe HTML-Seite präsentieren.

Fazit: Eine schlechte Webseite und schlampige Programmierung kostet dem Interessenten nur Zeit (und damit Geld, war er über die günstigen Flüge ja eigentlich sparen wollte) und natürlich auch dem Unternehmen.

PS: Wer dieses Problem hat, sollte seine Cookies löschen bzw. ausnahmsweise einen anderen Browser nutzen. Als Chrome-Nutzer konnte ich im IE mein Gepäckstück hinzufügen. Die Entwickler von Ryanair habe also irgendwie ein Cookie-Problem.

Thema der Woche: Performant gedacht?

Diskutiere folgende Fragen:

Frage

richtig

falsch

Native Compiler können viel kompakteren Maschinencode erstellen.

   

n+n ist schneller als 2*n.

   

Anweisungen wie n/2, n/4, n*2 sollte man immer durch Verschieben nachbilden.

   

Für eine switch-Anweisung kennt der Compiler zwei unterschiedliche Umsetzungen in Bytecode.

   

Schleifen soll man immer runterzählen. Das ist viel schneller.

   

Über Weak-Reference verwaltet Objekte werden früher freigegeben, als über Soft-Reference verwaltete Objekte.

   

Synchronisierte Methoden sind nicht viel langsamer im Aufruf als nicht-synchronisierte Methoden.

   

Wenn man schnellen Zugriff auf ein Attribut haben muss, dann würde man auf die get()-Funktion verzichten und das Attribut zum Lesen freigeben. Das ist zwar kein Gutes Design, aber im Zugriff viel schneller.

   

Je tiefer die Vererbungshierarchie ist, desto langsamer ist eine Objekterzeugung.

   

Statische Eigenschaften sind im Zugriff immer viel schneller als Objekteigenschaften.

   

Wenn man intern eine Liste als Datenstruktur benutzt, man möchte diese aber sicher nach außen weitergeben (etwa durch getListe()), dann muss man die Liste immer kopieren.

   

Felder beliebiger Größe zu kopieren ist mit System.arraycopy() immer am schnellsten.

   

Ein String-Objekt aus einem StringBuffer zu erstellen kostet noch einmal Speicher für die gesamte Zeichenkette.

   

""+ganzzahl ist in nettes Idiom zur Konvertierung von Ganzzahlen in Strings, was auch in der Performance nicht schlecht ist.

   

intern() ist eine nützliche Funktion, die mehr Vorteile als Nachteile mit sich bringt.

   

charAt() von String ist schneller als charAt() von StringBuffer.

   

substring() erzeugt ein neues Zeichenfeld-Objekt, das genau den ausgeschnittenen Teil speichert.

   

Liest eine Schleife Zeilen aus einer Datei aus, so kann ein StringTokenizer gut benutzt werden, um die Zeilen zu untersuchen.

   

StringBuffer ist von der Performance ausgezeichnet.

   

Es spielt keine große Rolle, ob man einer HashMap eine Initialgröße gibt oder nicht.

   

Eine Menge von Wahrheitswerten sind in einem boolean-Array gut aufgehoben.

   

Ein BufferedOutputStream um einen beliebigen Stream bringt in jedem Fall einen Geschwindigkeitsvorteil.

   

Eine ArrayList kann viel kompakter serialisiert werden als eine LinkedList.

   

Mit java.nio kann man Dateien viel schneller kopieren als ohne.

   

Ein Server sollte eine Anfrage immer mit einem neuen Thread beantworten.

   

SELECT * FROM Tabelle ist unproblematisch.

   

Es ist egal, ob ich einen ResultSet mit getXXX(String Spaltenname) oder getXXX(int Spaltennummer) abfrage.

   

Für mehrere Anfragen an eine Datenbank ist immer ein PreparedStatement einzusetzen.

   

Eine JTable holt sich beim Scrollen jedes Element neu aus dem Model.

   

Wenn man SwingWorker durch FoxTrot ersetzt, gewinnt man noch einmal Geschwindigkeit bei Swing-Programmen.

   

Ein Obfuscator führt bei Applets zu einer Minimierung der Übertragungszeit.

   

Der Report-Generator JasperReport

JasperReport ist eine weit verbreitete Open-Source-Software unter der LGPL-Lizenz zum Erstellen von Reports. Auf der Basis einer Vorlagendatei im XML-Format setzt JasperReports die Eingabe, die etwa aus einer relationalen Datenbank stammt, in eine Ausgabe um. Als Ausgabeformate unterstützt der Generator drei Typen von Ausgaben: Text-/Zeichenbasierte Dokumente HTML, RTF, CSV, TXT und XML, Dokumente in den Binärdateiformaten PDF, XLS und Export auf ein Grafik-Gerät (Graphics2D und Java Print Service). Der Unterschied ist, dass HTML, XLS und CSV nur eingeschränkte Positionierungen erlauben und nicht wie die anderen Formate pixelgenaue Boxen.

JasperReport greift auf weitere Open-Source-Bibliothek zurück, etwa auf iText (http://itextpdf.com/) für die PDF-Ausgabem, JFreeChart (http://www.jfree.org/jfreechart/) für diverse Diagramme oder Apache POI zu Generieren von Excel XLS Dokumenten (dann aber ohne Charts). Rund um JasperReport (http://community.jaspersoft.com/project/jasperreports-library) ist ein Ökosystem mit weiteren Tools entstanden. JasperReports wird zusammen mit anderen Tools von der JasperSoft Corporation (http://www.jaspersoft.com/) entwickelt. Das Unternehmen hat weiterhin die freie Swing-Anwendung iReport (http://community.jaspersoft.com/project/ireport-designer) zur Erstellung von JasperReport-Vorlagendateien im Portfolio. JasperAssistant (http://www.jasperassistant.com/) ist ein Beispiel für eine kommerzielle Software (129 US Dollar für eine Lizenz), um die Vorlagendateien grafisch in Eclipse zu erstellen.

Ein Report besteht bei JasperReport aus so genannten Bands, die auch Report-Sektionen genannt werden. Es lassen sich drei Typen von Sektionen ausmachen:

  • Sektionen, die nur genau einmal im Report auftauchen können, wie der Titel am Anfang, oder eine Zusammenfassung am Ende.
  • Sektionen, die auf jede Seite stehen können, wie die Seitenkopf- und Fußzeile.
  • Eine Detail-Sektion, die einer Tabelle mit Einträgen aus Datenquellen wie Datenbanken ähnelt.

Alle Bands haben eine feste Reihenfolge und eine benutzerdefinierte Höhe und Breite. Die Reihenfolge der Segmente ist: Hintergrund, Titel, Seitenkopf, Spaltenkopf, Gruppierungskopf, Detail, Gruppierungsende, Spaltenende, Seitenende, letztes Seitenende, Zusammenfassung.

Die Elemente dieser Bands nennen sich Report-Objekte. JasperReport definiert zwei Textelemente, vier grafische Objekte und einen Sub-Report – ein Sub-Report ist ein im Report eingebetteter Unter-Report. Dass es als Report-Objekte zweimal Textfelder gibt, liegt daran, dass JasperReport zwischen statischem Text, etwa für einen festen Spaltennamen, und einem dynamischen Text unterscheidet. Der dynamische Text kann sich während der Reporterstellung ändern, wenn etwa ein Feld aus einer Datenbank stammt oder die Seitenzahl angezeigt wird. Zu den grafischen Report-Objekten zählen Linie, Rechteck (auch abgerundet) und Ellipse. Alle weisen eine Vielzahl von Eigenschaften auf, wie Strichtypen, Dicken und Farben. Dazu kommen Bilder, die gewünscht skaliert und positioniert vom Datensystem, Datenbank oder über das Netzwerk kommen können.

Während bei statischen Textelemente sich der Werte nicht ändern, stellen dynamischen Textelemente so genannte Ausdrücke dar. Ein Ausdruck wird zur Laufzeit berechnet und als String in den Report gesetzt. Der Ausdruck kann auf verschiedene Datenspeicher zurückgreifen:

  • Parameter. Die Parameter sind Werte aus einer java.util.Map, die vor dem Aufbau eines Reports initialisiert wurde.
  • Felder. Sie ergeben sich aus Einträgen einer Datenquelle, wie der Datenbank oder einer Sammlung von Hibernate-Objekten.
  • Einträge aus einem Resource-Bundle.
  • Variablen. Während des Reports kann eine neu definierte Variable einen Wert zwischenspeichern. JasperReport bietet einige vorbelegte Variablen, zum Beispiel für die aktuelle Seite oder die aktuelle Spalte in der Verarbeitung.

Die Ausdrücke können mit den bekannten Operatoren aus Java zu komplexeren Ausdrücken zusammengesetzt werden, denn JasperReport erlaubt die gesamte Syntax der Sprache Java. Variablenzugriffe stehen in einer besonderen Notation: $P{name}, $F{name}, $K{name} und $V{name}.

Eine interessante Alternative ist Eclipse BIRT (http://www.eclipse.org/birt/phoenix/).

Daten speichern in Google App Engines Datastore

Die Google App Engine (GAE) ist eine freie Hosting-Plattform auf der Java Web-Applikationen deployt werden können. Google bietet für die GAP eine Reihe von Diensten an, etwa zur Datenspeicherung, Mail-Versand/-Empfang, Messaging und mehr. Wer sich für die GAE entscheidet und Daten speichern möchte, dem steht quasi unendliche Speicherkapazität zur Verfügung. Google speichert die Daten in einer Cloud, der Google BigTable (http://en.wikipedia.org/wiki/BigTable). Sie ist im Wesentlichen ein riesiger verteilter Assoziativspeicher.

Der Datestore – so wird die Datenspeicher für die Google App Engine genannt – speichert Entities. Eine Entity hat einen Typ (Entity-Kind genannt) und eine Menge von Schlüssel/Werte-Paaren. Eine konkrete Entity für eine Person könnte etwa die Schlüssel-Wert-Menge { name = „Chris“, age = 36 } speichern. Die Entities sind nicht mit klassischen Tabellenzeilen von relationalen Datenbanken zu vergleichen, das sie schemalos sind: Es gibt also keine Spalten mit festen Datentypen. Es können also beliebig viele „Spalten“ für gewisse Entities hinzukommen. Eine Person in der Datastore kann wie im ersten Beispiel Name und Alter speichern, eine andere Person Name und Schuhgröße. Die Anzahl Schlüssel ist wirklich beliebig; eine Tatsache die vom relationalen Design totales Umdenken erfordert.

Zum Zugriff auf den Datastore steht auf unterster Schicht eine Low-Level API (https://developers.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/package-summary). Diese API bietet grundlegende CRUD-Operationen und ist mit 10 Interfaces und etwas mehr als 50 Klassen leicht verständlich und übersichtlich. Wenn eine Person gespeichert werden soll, sieht das so aus:

Entity chris = new Entity( "Person" );
chris.setProperty("name", "chris");
chris.setProperty("age", 20 );
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(chris);

Jede Entity ist durch einen Schlüssel gekennzeichnet. Nach dem Speichern einer Entity lässt sich dieser Schlüssel über getKey() erfragen. Ist der Schlüssel bekannt, kann eine Anfrage an den Datastore gemacht werden.

Key key = chris.getKey();
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity e = datastore.get( key );
Long age = (Long) alice.getProperty("age");

Die Anfragen werden über den Entity-Typ gestellt, der in Key steckt. Alle Entities haben einen Entity-Kind und die Suche auf dem Datastore ist immer mit diesem Entity-Kind verbunden.

Der Datastore bietet eine hohe Lese- und Anfrage-Geschwindigkeit ist aber nicht unbedingt komfortablen.

https://developers.google.com/appengine/docs/java/datastore/?hl=de gibt auch auf Deutsch ein paar Hinweise. Aufbauend auf dieser Low-Level API implementiert Google die Standards Java Persistence API (JPA) und Java Data Objects (JDO). Im Open-Source-Bereich gibt es noch https://code.google.com/p/objectify-appengine/.

Dynamisch. Praktisch. Gut Mit dem BeanUtils Properties einfach beherrschen

Die Java-Beans sind ein allgegenwärtiges Gut. Zwar sind die Anforderungen minimal, aber für viele Bereiche wie grafische Oberflächen, JSPs, OR-Mappern sind sie nicht wegzudenken. Damit eine Bean zur Bean wird, muss eine Klasse

  • öffentlich sein
  • einen öffentlichen Standard-Konstruktur besitzen und
  • öffentliche Zugriffsfunktionen anbieten.

Nach dieser Vorgabe ist Folgendes eine gültige Bean:

class Weihnachtsmann
{
  private String name;

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }
}

Die Zugriffsfunktionen kapseln die internen Eigenschaften und sind nach einem festen Muster aufgebaut. Für ein Attribut String name würden die beiden Methoden void setName(String name) und String getName() angelegt. Für boolean-Eigenschaften ist die Vorsilbe is- üblich. Wenn die Beans nicht nach dem Schema aufgebaut sind, kann eine zusätzliche BeanInfo-Beschreibung helfen.

Während auf der einen Seite die Bean als Datencontainer Einsatz findet, sind sie noch aus einen anderen Grund reizvoll: durch ihren festen Aufbau lässt sich leicht mit Reflection/Instrospection auf die Bean und ihre Eigenschaften zugreifen. Nicht umsonst beschreiben Bean grafische Komponenten, so dass ein GUI-Builder völlig automatisch die Eigenschaften auslesen kann und Manipulationen erlaubt. Ein Exemplar einer Bean lässt sich über ihren Namen einfach anlegen; mit einem bekannten Class-Objekt liefert newInstance() ein neues Exemplar.

Class beanClass = Class.forName( "Weihnachtsmann" );
Object santa = beanClass.newInstance(); 

Für das Auslesen der Methodennamen definiert Java Introspection und für den Zugriff Reflection. Vor dem Zugriff auf die Bean-Properties sind jedoch die Präfixe get- und set- zu setzen und Datentypkonvertierungen durchzuführen.

Um den Umgang mit Bean-Eigenschaften zu vereinfachen, hat die Apache-Gruppe in ihren Commons-Paket die BeanUtils aufgenommen. Die aktuelle Version der BeanUtils ist 1.6.1 und ist beim Indianer-Server http://jakarta.apache.org/commons/beanutils/ zuhause. BeanUtils setzt zwei weitere Pakete aus Commons voraus: Collections und Logging. Der prominenteste Nutzer der Bibliothek ist Struts.

Bean-Eigenschaften lesen und setzen

Eine Bean-Property kennzeichnet ähnlich wie ein Servlet/JSP-Attribut zwei Merkmale: Einen Namen (immer ein String) mit einer Belegung (beliebiger Objekttyp). Um bei einer Bean eine Property zu setzen, müssen ihr Name und die neue Belegung natürlich bekannt sein, beim Lesen nur der Name. Auslesen und Setzen von Eigenschaften erlauben zwei statische Funktionen der Utility-Klasse org.apache.commons.beanutils.PropertyUtils: PropertyUtils.getSimpleProperty(Object bean, String name) und PropertyUtils.setSimpleProperty( Object bean, String name, Object value). Um unserem Weihnachtsmann einen Namen zu geben — und diesen gleich wieder auszulesen — ist zu schreiben:

PropertyUtils.setSimpleProperty( santa, "name", "Santa Clause" );
String name = (String) PropertyUtils.getSimpleProperty( santa, "name" );

Die Methoden sind jedoch nicht ohne ausführliche Fehlerbehandlung auszuführen; auftreten kann eine IllegalAccessException, IllegalArgumentException (eine RuntimeException), InvocationTargetException und NoSuchMethodException.

Zu bedenken ist, dass eine Bean-Property ganz spezielle Methoden erzwingt. Das geht so weit, dass BeanUtils nur genau eine get-Methode erwartet. Gibt es neben der erforderlichen get-Methode eine weitere, so liefert BeanUtils beim Zugriff einen Fehler. Der Zugriff auf bounds einer java.awt.Component gelingt daher nicht, da neben der parameterlosen Variante von getBound() auch getBounds(Rectange) existiert — ein Aufruf ähnlich wie der Folgende funktioniert daher nicht und führt zu einer NoSuchMethodException: PropertyUtils.getSimpleProperty(new Label(), "bounds");

PropertyUtils, BeanUtils und Konvertierungen

Neben der Klasse PropertyUtils, die bisher Verwendung fand, gibt es noch zusätzlich die Klasse BeanUtils. Sie erinnert von der Methodenvielfalt an PropertyUtils, doch sie führt für Datentypen mögliche Konvertierungen durch. Das Beispiel zum Weihnachtsmann soll erweitert werden, so dass er ein Attribute int alter besitzt. Der Versuch, das Attribut mit einem Wert vom Datentyp String über PropertyUtils.setProperty() zu setzen scheitert mit einer IllegalArgumentException. Ein int muss mit einem Integer-Objekt belegt werden. Wird jedoch anstelle der Klasse PropertyUtils die Klasse BeanUtils gesetzt, so funktioniert alles, denn BeanUtils führt die notwendigen Konvertierungen von String in int selbst aus.

BeanUtils.setProperty( santa, "alter", "612" );

Ergibt sich bei der Konvertierung ein Fehler, so macht das nichts, denn dann wird die Zuweisung nicht durchgeführt. (Apropos Konvertierungen: Die Firma Lockheed, die die Raumsonde Mars Climate Orbiter in England konstruierte, nutze die englische Einheit Inch, die NASA in Amerika Zentimeter; beim Landeversuch crashte die Sonde 1999 auf den Mars.)

Die Konvertierung aus einem String für die unterschiedlichsten Datentypen nimmt BeanUtils automatisch vor. Im oberen Fall findet eine Konvertierung von String in ein Integer Objekt statt. Die Converter selbst sind Klassen, die die Schnittstelle org.apache.commons.beanutils.Convert implementieren und auf diese Weise von einem String in BigDecimal, BigInteger, boolean, Boolean, byte, Byte, char, Character, Class, Double, float, Float, int, Integer, long, Long, short, Short, String,

java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp und mehr konvertieren. Den passenden Konverter sucht ConvertUtils.lookup(). Falls also BeanUtils.setProperty() vor der Aufgabe steht, einen passenden Typ herauszufinden, so sucht es zuerst nach dem Typ (von alter wäre das int) und anschließend den passenden Konverter. Da ConvertUtils.lookup(int.class) den IntegerConverter liefert, darf er die Zeichenfolgen umwandeln. Diese Konvertierung ist die übliche Konvertierung mit Integer.parseInt().

Falls die Standard-Konverter einen Typ nicht abdeckt, kann mit register() ein neuer Konverter eingehängt werden. Nicht nötig ist das für sprachabhängige Konvertierungen, denn hier bietet das Paket schon Hilfe an. So gibt es den IntegerConverter in der lokalisierten Fassung auch als IntegerLocaleConverter. Zum Nachschlagen dient aber nicht mehr ConvertUtils, sondern LocaleConvertUtils. Die Basisklassen für lokalisierte eigene Konvertiert ist LocaleConverter. Es gibt für alle normalen Konverter auch lokalisierte Konverter. Das lokalisierte Parsen scheint allerdings noch nicht so ganz ausgereift.

Eine Liste aller Eigenschaften

Eine Belegung aller Eigenschaften liefert BeanUtils.describe() beziehungsweise PropertyUtils.describe().

Map description = BeanUtils.describe( new JButton() );

Der Assoziativspeicher unter dem Namen description enthält als Schlüssel alle Properties und als Werte die Belegungen der Eigenschaften.

verifyInputWhenFocusTarget=true
displayedMnemonicIndex=-1
width=0
...

Formular-Parameter übertragen

In JSPs erfreut eine Schreibweise den Entwickler, die alle Parameterwerte eines Requests in eine Bean überträgt. Das Tag <jsp:setProperty> wird dann mit dem Attribut property="*" belegt.


Diese Möglichkeit sieht zwar die JSP-Spezifikation vor, doch in Servlets fehlt diese Möglichkeit. Sie wird aber insbesondere für MVC-Frameworks interessant, in dem der Controller selbständig die Formulardaten in eine Bean überträgt. Das kann schnell nachimplementiert werden. Die Servlet-API bietet in ServletRequest die Funktion getParameterNames(), um mit einer Enumeration über die Parameter zu laufen. Für jeden der Parameter kann setProperty() auf einer passenden Bean aufgerufen werden.

for ( Enumeration params = request.getParameterNames(); params.hasMoreElements(); ) {
  String name = (String) params.next();
  if ( name != null )
    BeanUtils.setProperty( bean, name, request.getParameterValues(name) );
}

getParameterValues() liefert ein Feld, was von der Bean auch verarbeitet werden muss. Sonst tut’s bei eindeutigen Parametern auch getParameter().

Werte einer Map in eine Bean kopieren

Aufgabe: Eine Ini-Datei enthält Schlüssel/Werte-Paare und alle Schlüssel sollen auf Eigenschaften einer Bean übertragen werden. Für Schlüssel soll es also entsprechende set- und get-Methoden geben. Eine einfache Aufgabe mit den passenden Klassen. Zum Laden der Paar-Datei findet Properties und die Objektfunktion load() Einsatz. Ein Properties-Objekt ist eine spezialisierte Hashtable — eine dumme Designentscheidung Begründung ? —, und alle Schlüssel lassen sich aufzählen. An dieser Stelle kommen wieder die BeanUtils ins Spiel. Die statische Funktion setProperty() sieht jeden Schlüssel als Property und überträgt den Wert auf die Bean.

Unter der Annahme, dass alle Eigenschaften in einer Map vorliegen, bietet BeanUtils eine feine Funktion: static void populate(Object bean, Map properties). Die Map schreibt Schlüssel als String vor, die Werte sind beliebige Objekte. populate() iteriert über die Schlüssel von properties und initialisiert über viele setProperty()-Funktionen die Werte der Bean. Die Aufgabe löst sich somit in wenigen Zeilen Quellcode.

Die Funktion populate() ließe sich auch bei Formular-Parametern verwenden, wenn denn die Schlüssel/Werte-Paare in einer Map vorliegen würden. Doch das können sie so ohne weiteres nicht, denn ein Schlüssel darf durchaus mehrmals mit verschiedenen Werten vorkommen — eine Map kann das nicht unterstützen. Gilt, dass überladene Parameter nicht zu erwarten sind, können von Hand die Parameter in eine Map einsortiert werden, so dass doch wieder populate() nutzbar ist. Seit der Servlet 2.3 Spezifikation bietet HttpServletRequest die Methode getParameterMap(), was direkt eine Map liefert. Sie speichert die Werte jedoch immer als Felder; wenn der Schlüssel einmalig ist, dann ist das Feld nur ein Element groß.

Indexierte Properties

Da ein Weihnachtsmann immer Rentiere besitzt, soll ihn eine zusätzliche Klasse Rentier für die Zugtiere bei der Arbeit unterstützen. Er soll in einem internen Feld zugtier die Tiere speichern und dafür auch zwei neue Zugriffsfunktionen definieren.

class Weihnachtsmann
{
  private Rentier[] zugtier = new Rentier[8];
  public Rentier getZugtier( int index ) {
    return zugtier[index];
  }
  public void setZugtier( int index, Rentier zugtier ) {
    this.zugtier[index] = zugtier;
  }
}

Ist ein Weihnachtsmann aufgebaut und mit setZugtier() ganz klassisch ein Rentier zugewiesen kann im nächsten Schritt die Eigenschaft erfragt werden.

Weihnachtsmann w = new Weihnachtsmann();
w.setZugtier( 0, new Rentier() );
Rentier z = (Rentier) PropertyUtils.getIndexedProperty( w, "zugtier", 0 );

Diese indexierten Eigenschaften lassen sich mit getIndexedProperty() erfragen und mit setIndexedProperty() setzen; die Signaturen sind PropertyUtils.getIndexedProperty(Object bean, String name, int index) und PropertyUtils.setIndexedProperty(Object bean, String name, int index, Object value).

Rentier rudolph = new Rentier();
PropertyUtils.setIndexedProperty( w, "zugtier", 1, rudolph );

Eine Alternative zu den Funktionen, die den Index als Parameter erwarten ist eine Schreibweise, die den Index in den Eigenschaftennamen kodiert. Die Schreibweise ist bekannt von Arrays: name[index].

Rentier zt = (Rentier) PropertyUtils.getIndexedProperty( w, "zugtier[1]" )

Mapped Properties

Für indexierte Properties ist es üblich, dass der Index eine positiver ganzzahliger Wert ist. Eine Erweiterung der Java-Beans gehen die BeanUtils mit den Mapped Properties, was indexierte Beans mit Schlüsseln sind — das Konzept erinnert an einen Assoziativspeicher. Genauso wie indexierte Properties mit eckigen Klammern einen Index aufnehmen, gibt es eine Abkürzung auch bei den abgebildeten Eigenschaften. Dabei wird der Schlüssel in runden Klammern hinter die Eigenschaft gesetzt. Die Signaturen sind PropertyUtils.getMappedProperty(Object bean, String name) mit den alternativen Parametern (Object bean, String name, String key), (Object bean, String name, Object value) und (Object bean, String name, String key, Object value).

Ein Beispiel: System.getProperties() liefert ein Properties mit den Systemeigenschaften. Die Objektmethode getProperty() liefert zu einem gegebenen Schlüssel den assoziierten Wert. BeanUtils lässt mit getMappedProperty() die abgebildeten Eigenschaften erfragen.

Properties props = System.getProperties();
System.out.println( PropertyUtils.getMappedProperty( props, "property(user.name)" ) );

Wo ist der Vorteil gegenüber props.get("user.name") ;

Der Schlüssel muss nicht in einfachen oder doppelten Anführungszeichen eingeschlossen werden, wie es etwa die Expression Language unter JSP 2.0 oder Velocity vorschreibt.

Geschachtelte Properties

Um eine Liste aller Zeichensätze des Betriebssystems auszulesen, lässt sich über GraphicsEnvironment getLocalGraphicsEnvironment() aufrufen.

GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();

Ausdruck Der Name des ersten Fonts erfragt

String s = ge.getAllFonts()[0].getName();

Wie sieht das nun mit den BeanUtils aus? Sicherlich ist es denkbar, für den indexierten Property einmal getIndexedProperty() und für den Namen setSimpleProperty() aufzurufen, aber commons.beanutils kann es besser, und zwar mit getNestedProperty().

Object o = PropertyUtils.getNestedProperty( ge, "allFonts[0].name" );

Während mit getNestedProperty() eine Eigenschaft ausgelesen werden kann, lässt sie sich mit setNestedProperty() setzen. Die Signaturen für set- und get sind: PropertyUtils.getNestedProperty(Object bean, String name) und PropertyUtils.setNestedProperty(Object bean, String name, Object value).

Nun sind drei Möglichkeiten bekannt, auf Bean-Eigenschaften zuzugreifen: Einfache Properties (getSimpleProperty(), setSimpleProperty()), indexierte Properties (getIndexedProperty()/setIndexedProperty()), mapped Properites und geschachtelte Properites (getNestedProperty()/setNestedProperty()). Um den Umgang zu vereinfachen, hilft die allgemeine Funktion getProperty(), die sich den passenden Typ heraussucht. Damit lässt sich alles mit nur einer Art von Funktion beschreiben: PropertyUtils.getProperty(Object bean, String name) und PropertyUtils.setProperty(Object bean, String name, Object value).

Kopien von Bean-Eigenschaften

Stehen zwei Beans gegenüber, die sich Properties teilen, so kopiert BeanUtils. copyProperties() die Eigenschaften. Die Klasse Point definiert genauso wie Rectangle das Property x und y. Die Belegungen der Eigenschaften lassen sich übertragen, so dass das Rectangle alles annimmt, was in Point definiert wird:

Point p = new Point( 10, 20 );
Rectangle rec = new Rectangle(); 
BeanUtils.copyProperties( rec /* = */, p );
System.out.println( rec ); // java.awt.Rectangle[x=10,y=20,width=0,height=0]

Intern geht copyProperties() alle Eigenschaften vom Punkt durch, findet x, y und auch location. Für diese drei Eigenschaften werden die entsprechenden set-Methoden beim Rectangle-Objekt gesucht; sie werden nur für x und y gefunden, denn setLocation() ist überladen und so ungültig.

Ein Beispiel, unter dem Bean-Attribute zu kopiert sind, liefert Struts. Die Action Form Beans werden zwar allzu oft als Model eingesetzt und weiterverarbeitet, das ist aber falsch. Die Form Beans sind lediglich eine Hilfs-Struktur, die nur für die Formulardaten eine Berechtigung haben. Demnach sind sie auch nur in einem Struts-Durchlauf bekannt und werden nicht als Datencontainer weitergegeben. Trennt man allerdings Form-Bean von tatsächlicher Model-Bean (falls eine Model-Bean benötigt wird) sieht das Model sehr ähnlich der Form-Bean aus und entsteht leider doppelter Quellcode, insbesondere für die set- und get-Methoden. Weiterhin ist lästig, dass bei einer gültigen Formularbelegung alle Form-Eigenschaften auf die Model-Bean übertragen werden. Das artet aus zu einer Reihe von Aufrufen der Art

modelBean.setXXX( formBean.getXXX() );

An diese Stellt helfen die BeanUtils und die praktische Funktion copyProperties(). Sie überträgt automatisch die Eigenschaften der Form-Bean in die Model-Bean.

BeanUtils.copyProperties( modelBean, formBean );

Bean-Objekte klonen

Auf copyProperties() aufbauend lassen sich Kopien von Beans anlegen lassen. Da das Kopieren auf Grund der Eigenschaften und set/get-Methoden geschieht, ist eine Implementierung der Schnittstelle Clonable nicht nötig. Mit der clone()-Methode teilt cloneBean() jedoch mit, dass die Kopie wieder eine flache und keine tiefe ist: static Object cloneBean(Object bean).

Dynamische Beans

Bisher waren es existierende Beans, die über wirkliche set- und get-Methoden verfügten. Mit den BeanUtils lassen sich jedoch auch "virtuelle" Beans erzeugen, also Beans, die mit den BeanUtils verwaltet werden können, aber keine passenden set- und get-Funktionen besitzen. Diese Spezial-Beans implementieren die Schnittstelle DynaBean und verwalten über diese Schnittstelle Schlüssel/Werte-Paare. Genauso wie eine Klasse den Bauplan für Objekte beschreibt, beschreibt die Schnittstelle DynaClass den Bauplan für DynaBean-Ojekte. Exemplare der virtuellen Beans führen zuerst über die DynaClass. Die BeanUtils bringen eine Basisimplementierung in Form der Klasse BasicDynaClass mit; ihre Eigenschaften werden als Array von DynaProperty-Objekten übergeben.

DynaProperty[] dynaProp = new DynaProperty[] {
  new DynaProperty( "schenker", String[].class ),
  new DynaProperty( "bringer", Weihnachtsmann.class ),
  new DynaProperty( "volumen", int.class )
};

BasicDynaClass dynaClass = new BasicDynaClass( "geschenk", null, dynaProp ); 

Die BasicDynaClass beschreibt für ein Geschenk ein Feld von Schenkern (wie "Mama", "Papa"), den Weihnachtsmann und die Größe des Geschenks.

Nach der Beschreibung der Klasse bildet die Objektfunktion newInstance() ein Exemplar der dynamischen Bean:

DynaBean paket = dynaClass.newInstance();

Alternativ zu dieser Schreibweise lässt sich ein Exemplar BasicDynaBean bilden, der im Konstruktor die beschreibende Klasse annimmt: paket = new BasicDynaBean(dynaClass);

Diese DynaBean besitzt über die Schnittstelle Methoden zum Erfragen, Setzen oder Löschen von Attributbelegungen. Die Methode set() setzt zum Beispiel eine Eigenschaft.

paket.set( "schenker", new String[1] );
paket.set( "bringer", new Weihnachtsmann() );

Die set()-Funktion ist überladen, um auch indexierte und abgebildete Eigenschaften zu unterstützten. Das gleiche gilt für die get-Funktion. Während bei den herkömmlichen Beans der Zugriff get<Property>() steht, so heißt der nun get("<Property>").

System.out.println( paket.get("volumen") );

Da das BeanUtils-Paket bei den bekannten Funktionen wie PropertyUtils.getProperty() neben den "normalen" Beans auch die DynaBean unterstützen, müssen die über die Schnittstelle DynaBean vorgeschriebenen Funktionen nicht verwendet werden.

PropertyUtils.setSimpleProperty( paket, "volumen", new Integer(123) );
Integer volumen = (Integer) PropertyUtils.getSimpleProperty( paket, "volumen" );

Da im Beispiel vorher schon das Feld der Schenker mit einem String-Array der Länge eins initialisiert wurde, lässt sich ein konkreten Element einfach mit der allgemeinen Funktion setProperty() setzen.

PropertyUtils.setProperty( paket, "schenker[0]", "Ulli" );
System.out.println( PropertyUtils.getProperty(paket, "schenker[0]") ); // Ulli

Mantel um existierende Beans

Die DynaBeans bietet einen einheitlichen Standard für einfache, indexierte oder abgebildeten Eigenschaften über setXXX() und getXXX()-Funktionen. Um auch existierende Beans als DynaBeans zu verwaltet, sind die Klasse WrapDynaBean und WrapDynaClass definiert. Eine WrapDynaBean legt sich um eine bekannten Bean, so dass diese sich als DynaBean verwalten lässt.

Weihnachtsmann myWeih = new Weihnachtsmann();
DynaBean wrapper = new WrapDynaBean( myWeih );
wrapper.set( "name", "Schlittenheld" );
String nameW = (String) wrapper.get( "name" );
System.out.println( "Wrapper: " + nameW + ", Bean: " + myWeih.getName() );
wrapper.set( "alter", new Integer(891) );
int alterW = ((Integer) wrapper.get( "alter" )).intValue();
System.out.println( "Wrapper: " + alterW + ", Bean: " + myWeih.getAlter() );

DynaBeans in Struts *

In Struts führt die Implementierung der Form-Beans für die Abbildung der Formulardaten zu lästigen Aufwand. Die DynaBeans helfen, diesen Aufwand zu minimieren, da keine wirkliche Bean-Klasse mehr zu implementieren ist, sondern die "virtuelle" Bean Verwendung findet. In der struts-config.xml wird zunächst unter form-bean wie üblich die Bean eingetragen. Allerdings ist der Typ nun nicht der Form-Bean Typ, sondern DynaActionForm. In den folgenden Elementen der XML-Datei wird genau die schon bekannten DynaProperty für eine DynaClass eingetragen — nur nicht in Java, sondern in einer Konfigurationsdatei.

<form-bean name="dynaWeihnachtsmannForm"
           type="org.apache.struts.action.DynaActionForm">
  <form-property name="name" type="java.lang.String"/>
  <form-property name="alter" type="java.lang.Integer"/>
</form-bean>

Da Struts mit den HTML-Tags die DynaBeans berücksichtigt, muss an der View (JSP-Seite) keine Veränderung vorgenommen werden. Nur die Action muss eine Veränderung erfahren, da ja ohne DynaBeans das ActionForm aus der execute()-Funktion auf die konkrete Bean typangepasst würde, um die entsprechenden set- und get-Funktionen zu nutzen. Dieser Programmcode muss nun umgebaut werden. Statt dessen gibt Struts eine Klasse DynaActionForm vor, zu der eine Typanpassung vorgenommen wird. Über diese Klasse lässt sich mit get- und set-Funktionen die Bean nutzen.

public ActionForward execute(
 ActionMapping mapping, ActionForm form,
 HttpServletRequest request,
 HttpServletResponse response )
  throws ServletException, IOException
{
  DynaActionForm weihForm = (DynaActionForm) form;
  String name = weihForm.get( "name" );
  ...

Nicht umsonst sieht der Zugriff auf die Attribute über DynaActionForm so aus wie schon im vorangehenden Beispiel beschrieben: DynaActionForm implementiert die Schnittstelle org.apache.commons.beanutils.DynaBean.

Die DynaActionForm ersetzt vollständig die Form-Bean, wobei eine Sache auf der Streckte bleibt: die Validierung. Für die Überprüfung der Formularbelegungen wurde in der Unterklasse von ActionForm die Methode validate() überschrieben. Bei den DynaBean gibt es aber keine eigene Bean-Klasse! Eine Lösung ist eine Validierung über das Validator-Framework (auch eine Jakarta Commons), also über eine XML-Datei, eine andere ist doch wieder eine Unterklasse. Diese spezielle Bean-Klasse leitet von der schon bekannten DynaActionForm ab und implementiert wie bekannt die validate()-Funktion. In struts-config.xml muss natürlich DynaActionForm wieder verschwinden und der Name der Unterklasse eingesetzt werden.

DynaBeans für Datenbanken

Die DynaBeans finden auch an anderer Stelle Verwendung: bei Datenbanken. Ein DynaBeans-Objekt kann eine Zeile eines ResultSet repräsentieren. Um das zu erreichen, legt sich ein ResultSetDynaClass-Objekt um ein ergebnislieferndes ResultSet.

ResultSetDynaClass rsdc = new ResultSetDynaClass( rs, true );

ResultSetDynaClass implementiert die Schnittstelle DynaClass. Das ist wichtig, denn DynaClass verwaltet die Beschreibung der Eigenschaften über DynaProperty-Objekte. Bei der ResultSetDynaClass werden die Namen der Spalten zu den Namen der Eigenschaften. Der Konstruktor von ResultSetDynaClass ist überladen und verträgt ein zusätzliches boolean, damit die Namen der Eigenschaften nicht exakt in Groß- und Kleinbuchstaben übereinstimmen müssen. Ist die Belegung true, so reichen alle Namen in Kleinbuchstaben. Wichtiger ist aber die Funktion iterator() von ResultSetDynaClass, die der Reihe nach die Zeilen als DynaBean liefert; die Beschreibung der Klassen-Informationen war ja wichtiges Teil in den DynaProperty-Objekten.

for ( Iterator rows = rsdc.iterator(); rows.hasNext(); ) {
  DynaBean row = (DynaBean) rows.next();
  System.out.println( row.get("bla") );
}

Nach dem Durchlaufen kann das ResultSet geschlossen werden, das bedeutet, dass zur Zeit des Durchlaufens eine Verbindung zur Datenbank bestehen muss.

Die Frage ist nun, was ein ResultSetDynaClass bringt; zum Durchlaufen tut es auch das ResultSet. Ein Vorteil ist, dass die Daten, die über die DyaBean veröffentlicht werden, leicht auf andere Beans kopiert werden können; man erinnere sich an BeanUtils.copyProperties(ziel, quelle). Oftmals müssen die Datenbankinhalte auf das Model einer Applikation übertragen werden, so dass ein ResultSetDynaClass dazu gut geeignet ist.

Eine Eigenschaft der Klasse BasicDynaBean (und BasicDynaClass) ist ihre Serialisierungs-Möglichkeit, da die Klasse die Schnittstelle java.io.Serializable implementiert, und somit persistent gemacht werden kann. Eine normale DynaBean implementiert diese Schnittstelle nicht, so dass man an dieser Stelle die Serialisierung nicht erzwingt. Aber natürlich kann eine andere Klasse DynaBean und Serializable gleichzeitig implementieren und das wäre sogar praktisch, denn könnte die Daten gleich abgespeichert werden. Wurde zum Beispiel eine BasicDynaBean angelegt, können Daten auf diese Bean übertragen werden und sie lässt sich später abspeichern. Es wäre doch praktisch, wenn auch die Zeilen vom ResultSetDynaClass serialisierbar wären. Dann wäre das Problem gelöst, wie sich Datenbankinhalte einfach übertragen lassen — O.K., ein RowSet ist auch eine Lösung. Die Antwort aus den BeanUtils ist die Klasse RowSetDynaClass, die Serializable implementiert. RowSetDynaClass liest aus einem ResultSet alle Zeilen aus und speichert sie, so dass sie serialisierbar werden.

RowSetDynaClass rsdc = new RowSetDynaClass( rs );

Nach dieser Zeile kann das ResultSet rs schon geschlossen werden. Der Zugriff auf die Zeilen geschieht anschließend mit getRows(), was eine java.util.List mit den bekannten DynaBean-Objekten liefert. Mit dem serialisier-fähigen RowSetDynaClass lassen sich die Inhalte über RMI leicht transportieren, ohne dass viel Programmcode entwickelt werden muss.

Links

GUI-Tipp: Verschicke EMails mit der richtiger Kodierung

Also nicht so:

Hi Christian,<BR><BR>We have received a request to permanently delete your account. Your account has been deactivated from the site and will be permanently deleted within 14 days. <BR><BR>If you did not request to permanently delete your account, please login to Facebook to cancel this request:<BR><BR>https://www.facebook.com/login.php<BR><BR><BR>Thanks,<BR>The Facebook Team

Session-Verwaltung in Servlets mit HttpSession

Die Servlet-API bietet die Klasse HttpSession an, eine Bibliothek auf hohem Niveau für die Verwaltung einer Sitzung. Sie basiert entweder auf Cookies oder URL-Rewriting, doch wird das von der API transparent gehalten. Als Programmierer bekommen wir so gut wie gar nichts davon mit. Falls der Client keine Kekse mag, wandeln wir alle Informationen in URLs um, die wir dann anbieten. Ein Sitzungsobjekt verwaltet die gesicherten Daten auch selbstständig in einer Datenstruktur. Hier fällt für uns keine Arbeit an.

Das mit einer Sitzung verbundene Objekt HttpSession

Jede Sitzung ist mit einem Sitzungsobjekt verbunden, das die Klasse HttpSession abbildet. Bei JSPs repräsentiert das implizite Objekt session die aktuelle Sitzung.

Werte mit einer Sitzung assoziieren und auslesen

Um Informationen mit der Sitzung zu verbinden, verwenden wir die Methode setAttribute(), die einen Schlüssel und einen Wert verbindet. Daten werden mit getAttribute() wieder aus der Datenstruktur gelockt, so wie es das folgende Beispiel zeigt:

List l = (List) session.getAttribute( "artikel" );

Hier verbinden wir mit einem Schlüssel eine Liste von Waren. Im Hintergrund werden die Informationen auf der Serverseite gesichert. Die Informationen selbst werden nicht in Cookies oder in der URL abgelegt, daher spielt die Größe der Daten auch keine Rolle. Ein HttpSession-Objekt verwaltet einen Assoziativspeicher, der die Wertepaare speichert. Es ist günstig, die Elemente serialisierbar zu gestalten, um die Daten dauerhaft zu speichern.

Werfen wir abschließend einen Blick auf das Programmstück, das eine neue Ware hinzufügt:

List l = (List) session.getAttribute( "artikel" );
if ( l == null )
{
  l = new ArrayList();
  session.setAttribute( "artikel", l );
}
l.add( w );

interface javax.servlet.http.HttpSession

  • Object getAttribute( String name )

    Liefert das mit name verbundene Objekt; null, wenn es keine Assoziation gab.
  • Enumeration getAttributeNames()

    Liefert eine Aufzählung aller mit der Sitzung verbundenen Objekte.
  • void setAttribute( String name, Object value )

    Bindet name mit dem Objekt value an die Sitzung. Existierte das Objekt, wird es ersetzt. Angemeldete HttpSessionBindingListener werden über die Methode value Bound() beziehungsweise valueUnbound() informiert.
  • void removeAttribute( String name )

    Entfernt das Attribut von der Sitzung. Ungültige Namen werden ignoriert. HttpSession BindingListener werden durch Aufruf von valueUnbound() informiert.

Alle Methoden liefern eine IllegalStateException, wenn die Sitzung ungültig ist. Die Methoden putValue() und setValue() sind veraltet und wurden durch setAttribute() und getAttribute() ersetzt.

URL-Rewriting

Das Session-Management sollte im Prinzip unabhängig von der technischen Umsetzung sein. Doch leider greift das Sitzungsmanagement beim URL-Rewriting schon sehr stark ein: Bei jedem Verweis auf eine neue Seite muss die URL entsprechend angepasst werden, weil die Sitzungs-ID mitgeschickt werden muss. Cookies verwalten die Sitzungs-ID völlig anders. Das bedeutet: Werden Cookies eingesetzt, ändert sich die URL nicht und jeder kann problemlos auf eine neue Seite verweisen. Nur beim URL-Rewriting muss an die URL eine Sitzungskennung angehängt werden.

Beispiel Eine URL für einen Cookie besitzt keine Sitzungskennung.

  • http://localhost/servlet/URLRewritingSession

Mit URL-Rewriting sieht das dann etwa so aus:

  • http://localhost/servlet/URLRewritingSession;jsessionid=abcde234

Wenn wir innerhalb eines Servlets auf eine andere generierte Seite verweisen wollen, haben wir eine URL vor uns, zu der wir verzweigen möchten. Die Servlet-API kümmert sich darum, an eine Benutzer-URL die Sitzungs-ID automatisch anzuhängen. Dazu dienen die HttpServletResponse-Methoden encodeURL() und encodeRedirectURL().

Beispiel: Aufgrund einer Formularbestätigung soll auf eine JSP-Seite mit dem Namen validate.jsp verwiesen werden:

<form action='<%= response.encodeURL("/validate.jsp") %>'>

Werden der Verweis und die Kodierung aus Versehen vergessen, ist dies das Ende der Sitzung. Ob eine Sitzung mit einem Cookie behandelt wird, lässt sich mit isRequestedSessionIdFromCookie() testen. Dann kann aufgrund einer Fallunterscheidung encodeURL() verwendet werden oder nicht. Allgemein ist es aber nicht schlecht, grundsätzlich alle Verweise innerhalb einer Webapplikation mit encodeURL() zu sichern. Im Fall von Cookies wird zwar keine Kennung angehängt, eine spätere Umstellung gestaltet sich aber einfacher, falls der Nutzer die Cookies einmal ausschaltet.

Zusätzliche Informationen

Ein Sitzungsobjekt verwaltet neben den assoziierten Daten noch weitere Informationen. Jede Sitzung bekommt eine eindeutige ID, die sich mit getId() erfragen lässt. Ist die Sitzung neu und hat der Client noch nie eine Verbindung gehabt, gibt isNew() den Wert true zurück. Existiert dann die Sitzung, gibt getCreationTime() ein long zurück – kodiert sind wie üblich die vergangenen Millisekunden seit dem 1.1.1970 –, in dem sich das Erstellungsdatum erfragen lässt. Dagegen erfragt getLastAccessedTime() die Zeit, die seit dem letzten Zugriff durch den Client vergangen ist. Falls der Server die Informationen dauerhaft speichert und der Cookie nicht abläuft, erlaubt dies Meldungen der Art: »Schön, Sie nach zwei Wochen zum fünften Mal bei unserer Partnervermittlung wiederzusehen. Hat’s wieder nicht geklappt?«

Das Ende der Sitzung

Eine Sitzung ist nicht automatisch unendlich lange gültig. Bei Cookies lässt sich der Gültigkeitszeitraum einstellen. Auch Sitzungsobjekte lassen sich in der Zeit anpassen. Die Methode setMaxInactiveInterval() setzt den Wert, wie lange eine Sitzung gültig ist. Ist der Wert negativ, zeigt er an, dass die Sitzung nicht automatisch beendet wird. Die entsprechende Methode getMaxInactiveInterval() liefert die Zeit in Sekunden, in der eine Sitzung gültig ist.

interface javax.servlet.http.HttpSession

  • long getCreationTime()

    Gibt in Millisekunden ab dem 1.1.1970 an, wann die Sitzung eröffnet wurde.
  • String getId()

    Liefert eine eindeutige Kennung, die die Sitzung identifiziert.
  • long getLastAccessedTime()

    Gibt in Millisekunden ab dem 1.1.1970 zurück, wann der Client zum letzten Mal auf den Server zugegriffen hat.
  • int getMaxInactiveInterval()
  • void setMaxInactiveInterval( int interval )

    Liefert und setzt die Zeit, für die der Servlet-Container die Sitzung aufrechterhalten soll, bis sie ungültig wird.
  • boolean isNew()

    Der Rückgabewert ist true, wenn die Sitzung neu ist.

Beispiel Zum Schluss wollen wir ein Programm formulieren, das alle diese Informationen auf einmal ausgibt.

<%@ page language="java" import="java.util.*" %>
<%
int cnt = 0;
if ( session.isNew() )
{
out.println( "Willkommen Neuling!\n" );
}
else
{
out.println( "Hallo, alter Freund!\n" );
String o = (String) session.getAttribute( "cnt" );
if ( o != null )
cnt = Integer.parseInt( o );
cnt++;
}
session.setAttribute( "cnt", ""+cnt );
%>
<p>
Session-ID: <%= session.getId() %> <p>
Erzeugt am: <%= new Date(session.getCreationTime()) %> <p>
Letzter Zugriff: <%= new Date(session.getLastAccessedTime()) %> <p>
Ungültig in Minuten: <%= session.getMaxInactiveInterval()/60 %> <p>
Anzahl Zugriffe: <%= cnt %>

Das Programm liefert beispielsweise folgende Ausgabe:

Hallo, alter Freund!
ID: 91410050092487D9B5D0D2A7A3D0F072
Erzeugt am: Fri Jan 18 20:16:49 CET 2002
Letzter Zugriff: Fri Jan 18 20:23:33 CET 2002
Ungültig in Minuten: 30
Anzahl Zugriffe: 4

Die ID sieht bei jedem Server anders aus. Der Webserver von Sun erzeugt beispielsweise ganz andere Kennungen.

Automatisches Neuladen von Servlet-Seiten

Über das implizite Objekt response lassen sich Antworten von der JSP-Seite an den Client formulieren. Das Setzen von Content-Type ist für nahezu alle Servlets unabdingbar. Daneben gibt es noch weitere, die beispielsweise für Cookies interessant sind. Ein spezieller Header kann auch das Caching beeinflussen (mit dem Datum der letzten Modifizierung) oder die Seite nach einer bestimmten Zeit neu laden. Letzteres wollen wir verwenden, um eine einfache Ausgabe zu erzeugen, die jede Sekunde neu geladen wird. (Die Seite darf jedoch nicht im Cache liegen. Um das Caching explizit auszuschalten, sollte Pragma: no-cache gesetzt werden. Bei einer lokalen Installation spielt dies jedoch keine Rolle.

<%! private String result = "*"; %>
<% response.setHeader( "Refresh", "1" ); %>
<%= result += "*" %>

Dieses Servlet erzeugt eine Reihe von Sternchen, wobei es sich die Zeichenkette jede Sekunde neu vom Server holt. Das dargestellte Programm zeigt in einfacher Weise auf, was sich noch wesentlich komplexer mit Threads anstellen lässt. Im Hintergrund hätten wir einen Thread starten können, der ständig eine neue Berechnungen durchführt, die wir dann in der println()-Zeile hätten ausgeben können.

In Servlets Seiten über HTTP-Redirect umlenken

Ist eine Seite nicht mehr korrekt, kann sie umgelenkt werden. Hierfür wird ein spezieller Header gesetzt.

sendRedirect()

Dazu dient die Methode sendRedirect(String), die auf eine neue Seite verweist. Als Argument kann eine relative oder absolute URL aufgeführt werden, die auf eine temporäre neue Seite weist. Wir könnten auch mit setHeader() arbeiten, müssten dann aber von Hand den Statuscode ändern, der für Umleitungen auf 302 gesetzt sein muss. Die Arbeit können wir uns sparen. Nach dem Setzen der Umleitung sollte nicht mehr in die Ausgabe geschrieben werden.

Wozu kann nun diese Umleitung eingesetzt werden? Zum Beispiel, um über Formular-Parameter zu externen Seiten weiterzuleiten:

response.sendRedirect( url );

Nach der Umleitung steht der Ort der neuen Seite in der URL-Zeile des Browsers. Das folgende Programm verweist nun einfach auf ein anderes Servlet. Die Pfadangabe kann absolut oder relativ sein.

String url = "http://www.tutego.de/";
response.sendRedirect( url );

Was passiert beim Umlenken?

Technisch gesehen ist eine Umlenkseite eine ganz normale Webseite. Das wirkliche Umlenken ist eine Fähigkeit des Browsers und nicht des Servers. Dies ist wichtig anzumerken, weil eigene Programme, die URL-Verweise aufbauen, hier oft nicht korrekt vorgehen.

Das Servlet setzt beim sendRedirect() den Content-Type auf "text/html". Wichtig sind zwei weitere Informationen: die eine in der Statuszeile und die andere im Header. In der Statuszeile wird die Nummer 302 gesendet, die das Umlenken bezeichnet. Die Information darüber, wohin verwiesen wird, steht in einem weiteren Header namens »Location«. Somit können wir unser Redirect prinzipiell auch selbst ausformulieren, indem wir Folgendes schreiben:

response.setStatus( 302 );
response.setContentType( "text/html" );
response.setHeader( "Location", url );

Der String url ist dann eine Referenz auf die neue Seite. Der Verweis auf die externe Seite muss dann natürlich absolut sein. Dies regelt jedoch sendRedirect() automatisch.