Im Angesicht des Web

Java-basierte Web-Frameworks gibt es seit der Einführung der Servlet- und JSP-APIs wie Sand am Meer. Doch haben alle eins gemeinsam: Es sind proprietäre Lösungen. Mit der durch Sun im Jahre 2004 als Final Release veröffentlichten Spezifikation der Java Server Faces existiert zum ersten Mal ein herstellerübergreifender Standard, den dieses dreiteilige Tutorial vorstellt.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen
Lesezeit: 19 Min.
Von
  • Detlef Bartetzko
  • Arvid Hülsebus
  • Lars Röwekamp
Inhaltsverzeichnis

Nach einer fast dreijährigen Spezifikationsphase und etlichen damit verbundenen Diskussionen hat Sun 2004 das erste offizielle Release der JSF-Spezifikation (Java Server Faces) veröffentlicht. Ziel der JSF Expert Group und somit Grund für die relativ lange Spezifikationsphase war es unter anderem, sowohl aus den positiven als auch den negativen Erfahrungen anderer Web- und UI-Frameworks zu lernen und eine Art „Best-of-bread“-Lösung im Java-basierten Webumfeld zu schaffen. So kommt es nicht von ungefähr, dass Craig McClanahan - der „Urvater von Struts“ - federführend an der Erschaffung von JSF beteiligt ist.

Insbesondere dem berechtigten Kritikpunkt an vielen existierenden Web-Frameworks, dass die Komplexität des Web den Fokus zu sehr von der sauberen Umsetzung der Geschäftslogik ablenke, hat die JSF-Spezifikation Rechnung getragen. Intention des zugehörigen Java Specification Request (JSR 127) war es zum einen, klassische Anforderungen aus dem Webumfeld wie Parameter Mapping, Eingabe-Validierung und Konvertierung, Seitennavigation oder Internationalisierung möglichst einfach und transparent zu gestalten und dabei zum anderen den Grundstein für saubere Aufgaben- und Schichtentrennung zu legen.

Mehr Infos

Dies erreichen ein gut durchdachter Request-Lebenszyklus, eine saubere MVC-Trennung (Model-View-Controller) und, damit gekoppelt, ein bisher eher aus dem Desktop-UI-Umfeld bekannter komponentenbasierter View-Aufbau.

Das dreiteilige Tutorial vermittelt dem Leser, wie man mit Hilfe der JSF-Technik elegant komponentenbasierte Webanwendungen entwickelt, diese in den J2EE-Kontext integriert und, durch strikte Trennung von Seitenbeschreibung und Layout, den Schritt hin zur HTML-freien Seite schafft.

Während der erste Teil in die generelle Funktionsweise sowie die Grundlagen von JSF einführt, geht der zweite einen deutlichen Schritt weiter und beschreibt neben einigen fortgeschrittenen JSF-Techniken unter anderem verschiedene Möglichkeiten zur Integration mit „fremden“ Welten wie Spring und Struts. Der dritte und letzte Teil schließlich zeigt, wie man Seitenbeschreibung und Design strikt trennt und fortschrittliche Render-Techniken einsetzt.

Die Entwicklung eines sich durch das gesamte Tutorial ziehenden Beispiels soll der besseren Veranschaulichung dienen. Dabei handelt es sich um eine Adressverwaltung, die als Bestandteil eines CRM-Systems (Customer Relationship Management) für einen fiktiven Kunden anzusehen ist und per Webbrowser bedient werden soll.

Alle zum Nachvollziehen der folgenden Schritte notwendigen Quellen des Beispiels sind über den iX-Listingserver verfügbar. Eine ausführliche Anleitung zu Installation und Deployment findet sich im Kasten „JSF-Adressbuch - erste Schritte“ (in der Print-Version).

Sofern nicht explizit anders erwähnt, bezieht sich das gesamte Tutorial auf die JSF-Spezifikation 1.1. Im Beispiel kommt Java 5 zum Einsatz.

Da es das primäre Ziel dieses ersten Teils ist, einen übersichtlichen und einfachen Einstieg in die Entwicklung mit JSF zu geben, geht der Text weder auf jedes Detail der Spezifikation ein, noch erläutert er jede Implementierungsmöglichkeit. Stattdessen liegt der Fokus auf den grundlegenden Konzepten und Konfigurationen.

Wer eine klassische, größere Webanwendung entwickelt, sollte sich für zwei wichtige Entwurfsmuster entscheiden: Zum einen sollte die gewählte Architektur mehrschichtig sein und zum anderen auf dem MVC-Pattern beziehungsweise dem im Webumfeld etablierten Model2 basieren. Die Mehrschichtigkeit schafft klare Grenzen zwischen den Aufgabenbereichen des Systems: Präsentationsschicht, Geschäftslogik und Backend. Das MVC-Pattern unterstützt diese Aufteilung. Vereinfacht gesprochen sorgt der View für die Darstellung der Daten aus dem Modell in der Präsentationsschicht, während der Controller zwischen den beiden durch die Ansteuerung der Geschäftslogik vermittelt. Das Modell wird dabei im Backend gespeichert.

Der beschriebene Ansatz führt zu einer Entkopplung der einzelnen Komponenten. Änderungen an der Darstellung betreffen beispielsweise nicht zwangsläufig die Geschäftslogik oder das Modell. Eine geeignete Abstraktion vorausgesetzt, ist das Backend austauschbar, ohne dass es erforderlich ist, die Geschäftslogik oder die Darstellung anzupassen.

Das JSF-Framework eignet sich gut für die Umsetzung der obigen Architektur, da es auf MVC basiert. Mit Hilfe des Controllers - in der Rolle einer Fassade - lassen sich leicht Services ansprechen, welche die eigentliche Logik implementieren und die Kommunikation mit dem Backend regeln.

In einigen Punkten unterscheidet sich JSF stark von den meisten anderen Web-Frameworks: Zu jeder dargestellten Seite existiert ein zustandsbehafteter UI-Komponentenbaum, der sie repräsentiert und die aktuellen Werte vorhält. Für die Abarbeitung eines Request definiert die Spezifikation einen Lebenszyklus, der die typischen Aufgaben wie Validierung, Mapping von Parametern und den Aufruf der Geschäftslogik auf einzelne Phasen verteilt. Wie der Client eine UI-Komponente letztendlich darstellt, entscheidet in der Regel ein konfigurierbarer Renderer, da JSF ausgabeunabhängig ist. Die folgenden Ausführungen stellen diese Aspekte von JSF kurz dar.

Eine dargestellte Seite heißt im JSF-Kontext „View“. Dieser besteht aus hierarchisch angeordneten Komponenten der Benutzerschnittstelle, etwa einfachen Eingabefeldern und Knöpfen, aber auch aufwendigeren Bausteinen wie Tabellen, Bäumen und Menüs. Diese Struktur bildet in ihrer Summe einen Baum, dessen Hierarchie den Seitenaufbau widerspiegelt. So kann eine Tabelle beispielsweise wieder Ein- oder Ausgabefelder enthalten und ein Menü eventuell Untermenüs. Für jeden Benutzer der Webanwendung speichert das Framework eine entsprechende Instanz des Komponentenbaums.

Der Komponentenbaum: Seine Wurzel ist immer eine Instanz des Typs UIViewRoot (Abb. 1).

Jede Komponente kann der Programmierer mit einer ID versehen und auf diese Weise ansprechen oder suchen. Grundsätzlich kann sie einen beliebigen Zustand besitzen und diesen über Request-Grenzen hinaus speichern. Dabei ist sowohl eine Speicherung auf dem Client als auch auf dem Server möglich. Alle Komponenten sind von der abstrakten Klasse UIComponent abgeleitet, der wesentlichen Schnittstelle zum Komponentenbaum. Die Wurzel des Baums bildet eine Instanz des Typs UIViewRoot. Abbildung 1 verdeutlicht die Zusammenhänge anhand eines Komponentenbaumes aus dem Beispiel, der Adressverwaltung innerhalb des CRM.

Fast alle Eigenschaften einer Komponente lassen sich mit „Value Bindings“ versehen, um den aktuellen Wert an einen bestimmten Speicherort zu binden. Mit Hilfe solcher Bindings können zum Beispiel Eingabefelder ihre Daten im Modell speichern und wieder daraus lesen. In der Regel speichert eine Eingabekomponente zusätzlich den übertragenen Wert lokal, um diesen unabhängig von Konvertierungen im Fehlerfall wieder anzeigen zu können. Dieses geschieht für den Entwickler einer JSF-Seite transparent, das heißt, ohne ihm zusätzlichen Aufwand zu verursachen. Die folgende Zeile zeigt exemplarisch ein Value Binding zwischen dem Modell einer Adresse innerhalb einer Tabellenzeile und der zugehörigen Ausgabekomponente eines View zur Darstellung einer Adressliste:

<h:outputText  value="#{row.address.firstName}"/>

Für Komponenten, die Kommandos darstellen, etwa Buttons oder Links, kann der Entwickler analog „Method Bindings“ spezifizieren. Dieses Binding referenziert eine Methode, die das Programm beim Auslösen der Aktion ausführen soll. Beispielsweise löst das in der folgenden Zeile dargestellte Method Binding das Anlegen einer neuen Adresse mit Hilfe eines entsprechenden Controllers aus:

<h:commandButton 
action="#{controller.createAddress}"
value="New"/>

Nimmt der Server einen Faces Request entgegen, verarbeitet er diesen gemäß des Request-Lebenszyklus (Request Processing Lifecycle, RPL) in sechs Phasen:

  • Restore View
  • Apply Request Values
  • Process Validations
  • Update Model Values
  • Invoke Application
  • Render Response

Alle Phasen arbeiten dabei auf einem gemeinsamen Kontext: dem „Faces Context“. Dieser enthält unter anderem eine Referenz auf den Komponentenbaum sowie eventuelle Fehlermeldungen, erzeugt von den einzelnen Phasen beziehungsweise der Geschäftslogik.

Im Regelfall arbeitet eine Anwendung alle Phasen nacheinander ab. Kommt es allerdings in einer Phase zu besonderen Ereignissen, etwa Validierungsfehlern, wird die Verarbeitung verkürzt. Im beschriebenen Fall ruft das Programm zum Beispiel die nachfolgende Geschäftslogik nicht mehr auf.

Das Folgende stellt die Phasen des Request-Lebenszyklus im Detail vor.

Restore View: Für einen ankommenden „Faces Request“ stellt die JSF-Implementierung entweder den für den aktuellen Benutzer bereits gespeicherten Komponentenbaum wieder her oder, falls kein solcher existiert, generiert einen neuen leeren Komponentenbaum. In diesem Fall geht es danach unmittelbar zur letzten Phase „Render Response“ über. Die Identifizierung des Baums ist über die aufrufende URL jeweils eindeutig.

Apply Request Values: Den Komponentenbaum füllt das JSF-Framework mit den Werten aus dem Request wie HTML-Parameter, HTTP-Header oder Cookies. Jede Komponente entscheidet selbstständig, wie diese aussehen, in der Regel durch einen zugeordneten Renderer. Liegt dem Request ein Kommando, etwa ein gedrückter Button, zu Grunde, erzeugt die zugehörige Komponente einen „Action Event“, den sie später auswertet, um die Geschäftslogik aufzurufen.

Process Validations: In dieser Phase erfolgt die Validierung der aus dem Request gelesenen Werte. Dies geschieht entweder durch registrierte Validatoren oder in der Komponente selbst. Auswertungsfehler erzeugen Meldungen und markieren die Komponente als nicht gültig. Im Fall einer fehlgeschlagenen Validierung überspringt die Implementierung, nachdem sie alle Komponenten ausgewertet hat, die folgenden Phasen und leitet die Ausgabe direkt an die „Render Response“-Phase. Ein einfaches Beispiel für eine Validierung ist die Überprüfung einer Mindestlänge für ein Eingabeelement.

Vor der Validierung erfolgt die Konvertierung des übertragenen Werts in das Zielformat der Komponente. Dies geschieht entweder durch den Renderer oder durch registrierte Konverter. Konvertierungsfehler behandelt die Anwendung analog zu Validierungsfehlern. Ein klassisches Beispiel im Bereich der Konvertierung ist das Überführen des Eingabetextes in ein Datums- oder Zahlenformat.

Update Model Values: Nachdem alle Konvertierungen und Validierungen erfolgreich waren, gelten die übertragenden Daten als syntaktisch und semantisch korrekt. Folglich schreibt die Anwendung die Werte der Eingabefelder mit Hilfe der Value Bindings der Komponenten in das angebundene Modell. Wurde zum Beispiel, wie oben gezeigt, eine Eingabekomponente direkt mit einem Attribut der Klasse Address gebunden, erfolgt an dieser Stelle der Aufruf der zugehörigen Setter-Methode auf der Instanz eines Adressobjekts.

Invoke Application: Zu Beginn dieser Phase ist das Datenmodell aktualisiert, und dem Aufruf der Geschäftslogik steht nichts mehr im Wege. Zu diesem Zweck löst das Framework die Action Events für diese Phase aus. Dies führt in der Regel zu einem Methodenaufruf, der mit Hilfe eines Method Binding innerhalb der jeweiligen Command-Komponente genauer spezifiziert ist. Im Rahmen des oben dargestellten Aufrufs durch einen Command-Button käme es zum Beispiel zu einem Aufruf der Methode createAddress() innerhalb einer Objektinstanz, die unter dem Namen controller ansprechbar ist.

Render Response: Die Anwendung liefert einen so genannten „Outcome“ zurück. Ein Outcome repräsentiert einen symbolischen Wert, der auf der Basis von Navigationsregeln den nächsten zu erstellenden View definiert. Kam es nicht zur Ausführung der Logik, sondern hat die Anwendung etwa wegen eines Validierungsfehlers die Phase „Invoke Application“ übersprungen, zeigt sie immer den View an, der den Request generiert hat. Dies entspricht der aufrufenden Seite und ermöglicht das erneute Anzeigen eines Webformulars - zum Beispiel einer Maske zur Adresserfassung - inklusive Fehlermeldungen und Eingabehilfen. In der Phase „Render Response“ wird auch der Zustand des Komponentenbaums gespeichert, der zum neuen View gehört.

Java Server Faces sind ausgabeunabhängig. Wie eine Komponente auf dem Client erscheint, wie eine Anwendung ihre Parameter kodiert und dekodiert, entscheidet ein zugeordneter „Renderer“. Für die Ansteuerung der Renderer sorgt ein „ViewHandler“.

Für HTML definiert der JSF-Standard eine JSP-Tag-Library, die jede spezifikationskonforme Implementierung umsetzen muss und die daher in diesem Teil des Tutorials angewendet werden soll. Diese Implementierung ist eine klassische Model-2-Architektur: Nachdem das FacesServlet den Request wie beschrieben verarbeitet hat, leitet es auf eine JSP-Seite weiter. Diese übernimmt dann das Rendering der neuen Seite. Dabei greifen die einzelnen Tags auf den Komponentenbaum zu und ändern oder erzeugen gegebenenfalls Komponenten. Im Allgemeinen gibt es zu jedem JSP-Tag eine zugehörige Komponente im Baum.

Adressbuch des CRM-Beispiels: Die zwei JSF-Views zeigen eine Liste mit den vorhandenen Adressen beziehungsweise die Details eines ausgewählten Eintrags (Abb. 2).

Nach der Erläuterung der grundsätzlichen Funktionsweise sowie der wesentlichen Konzepte von JSF folgt nun die Umsetzung der Adressverwaltung des Beispiel-CRMs sowie der damit verbundenen Einbindung der JSF-Technik in die eigene Webanwendung.

Zur initialen Erstellung einer einfachen JSF-Anwendung sind lediglich vier Schritte notwendig, die hier kurz skizziert und im Weiteren im Detail ausgeführt werden:

  • Integration: Erweiterung des Web-Deployment-Deskriptors web.xml um einen Eintrag eines JSF-Einstiegspunkts.
  • Controller-Implementierung: Umsetzung der Geschäftslogik sowie die Konfiguration von „Managed Beans“ in der JSF-eigenen Konfigurationsdatei faces-config.xml.
  • View-Implementierung: Erstellen der JSF Seiten, eventuell unter Verwendung von Value- und Method Bindings.
  • Navigation: Deklarieren der Navigationsregeln in faces-config.xml.

JSF-Programme sind Webanwendungen, die nach dem Front-Controller-Pattern funktionieren. Dies bedeutet, dass ein einzelnes Servlet - in diesem Fall das FacesServlet - die Requests entgegennimmt und anschließend die weitere Verarbeitung einleitet. Gemäß der Java-Servlet-Spezifikation erfolgt die Deklaration dieses Servlets im Deployment-Deskriptor web.xml im Verzeichnis WEB-INF, wobei der Servlet-Name frei wählbar ist:

<servlet>
<servlet-name>
FacesServlet
</servlet-name>
<servlet-class>
javax.faces.webapp.FacesServlet
</servlet-class>
</servlet>

Ein Servlet-Mapping im selben Deskriptor definiert, welche URLs die Anwendung an das FacesServlet leitet:

<servlet-mapping>
<servlet-name>
FacesServlet
</servlet-name>
<url-pattern>
/faces/*
</url-pattern>
</servlet-mapping>

Der Name des Servlets muss dem der vorab definierten Servlet-Deklaration entsprechen. Für das URL-Pattern stehen zwei Alternativen zur Verfügung: Ein Präfix-Mapping der Form „/faces/*“ wie im Beispiel oder ein Suffix-Mapping der Art „*.jsf“. Die Beispielanwendung leitet alle Requests mit dem Präfix „/faces/*“ an das FacesServlet, das den RPL realisiert.

Damit der Anwendung die JSF-Bibliotheken (API und Implementierung) zur Verfügung stehen, muss sie der Klassenpfad enthalten. Typischerweise liegen die dafür notwendigen Dateien im Verzeichnis WEB-INF/lib. Dieser Ordner (und damit alle darin enthaltenen Bibliotheken) fügt ein zur Java Enterprise Edition konformer Servlet-Container automatisch dem Klassenpfad der Webanwendung hinzu.

Derzeit stehen zwei etablierte Implementierungen für die JSF-Spezifikation zur Verfügung. Zum einen die von Sun herausgegebene Referenzimplementierung und zum anderen die unter der Apache-2.0-Lizenz stehende JSF-Implementierung MyFaces, die eine gute Alternative zu der Sun-eigenen Variante ist. Für beide gilt, dass sich die notwendigen Bibliotheken auf zwei JARs aufteilen. Während die im Sun-Umfeld benötigten Bibliotheken unter den Namen jsf-api.jar und jsf-impl.jar zu finden sind, heißen ihre Pendants im MyFaces-Umfeld myfaces-api.jar und myfaces-impl.jar. Je nach gewählter JSF-Implementierung und -Version können weitere Bibliotheken notwendig sein.

Bei der Implementierung der Logik kommt ein Controller zum Einsatz, der im Wesentlichen zwischen der Präsentations- und der Serviceschicht vermittelt. Er enthält zum einen die Adressliste und die jeweils aktuell editierte Adresse, zum anderen bietet er zwei Methoden zum Anlegen und Speichern von Adressen an. Diese Methoden delegiert er an einen Backend-Service. Der Controller ist in Listing 1 in Ausschnitten dargestellt.

Mehr Infos

Listing 1: Controller für die Adressverwaltung

public class Controller {
private Address currentAddress = new Address();
private List<AddressRow> addressRows = new ArrayList<AddressRow>();
public Controller() {
...
}
public String createAddress() {
...
}
public String editAddress() {
...
}
public Address getCurrentAddress() {
return currentAddress;
}
public List<AddressRow> getAddressRows() {
return addressRows;
}
}

Damit die JSF-Laufzeitumgebung den Controller steuern kann, wird er als Managed Bean in faces-config.xml registriert. Managed Beans sind Bestandteil der JSF-Spezifikation und eignen sich gut zur Speicherung temporärer Daten, die etwa mit der Session oder dem Request assoziiert werden. Die Deklaration einer Managed Bean zeigt Listing 2.

Mehr Infos

Listing 2: Deklaration einer Managed Bean

<managed-bean>
<description>
Controller für die Adressverwaltung
</description>
<managed-bean-name>controller</managed-bean-name>
<managed-bean-class>addressbook.web.Controller</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
Mehr Infos

Listing 3: Die Adressliste

<f:view>
<h:form>
<h:dataTable value="#{controller.addressRows}" var="row">
<h:column>
<f:facet name="header">
<h:outputText value=""/>
</f:facet>
<h:selectBooleanCheckbox value="#{row.selected}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="First Name"/>
</f:facet>
<h:outputText value="#{row.address.firstName}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Last Name"/>
</f:facet>
<h:outputText value="#{row.address.lastName}"/>
</h:column>
</h:dataTable>
<h:commandButton action="#{controller.createAddress}" value="New"/>
<h:commandButton action="#{controller.editAddress}" value="Edit"/>
</h:form>
</f:view>

Da der Controller im Beispiel zustandsbehaftet ist, benötigt jeder Benutzer eine eigene Instanz. Der Scope session sorgt dafür, dass die Lebenszeit der Managed Bean an die der Benutzersession gebunden ist. Insgesamt gibt es vier Scopes:

  • request: Die Applikation erzeugt die Instanz für jeden Request neu.
  • session: Der Lebenszyklus der Instanz ist an die Benutzersession gebunden.
  • application: Es gibt nur eine Instanz innerhalb der gesamten Anwendung (Singleton).
  • none: Die Anwendung legt die Instanz an, speichert sie aber nicht in einem eigenen Scope. Dies ist bei Hierarchien von Managed Beans sinnvoll.

Der Controller wird unter dem Namen controller an die Session gebunden und auf diese Weise von den UI-Komponenten referenziert.

Managed Beans sind POJOs, also „Plain Old/Ordinary Java Objects“, die über einen Standardkonstruktor verfügen müssen und durch öffentliche Setter-Methoden konfiguriert werden können.

Wie es mit dem kleinen JSF-Projekt weitergeht, erfahren Sie in der aktuellen Print-Ausgabe, erhältlich im gut geführten Zeitschriftenhandel.

Teil 2: Fortgeschrittene JSF-Techniken und die Integration mit Spring und Struts.

Teil 3: Trennung von Seitenbeschreibung und Layout sowie Einsatz fortschrittlicher Render-Techniken.

Mehr Infos

iX-TRACT

  • Die Java-Server-Faces-Spezifikation soll die Entwicklung von Webanwendungen auf der Basis von Java standardisieren.
  • Mit Java Server Faces lassen sich ausgabeunabhängige Weboberflächen aus hierarchisch angeordneten UI-Komponenten erstellen.
  • Das Framework selbst basiert auf dem MVC-Entwurfsmuster und unterstützt so die saubere Trennung von Darstellung und Modell sowie deren Vermittlung durch einen Controller.