Startseite

Web Anwendungen mit JSF 2

Dieses Tutorial soll Anfängern helfen, die grundlegenden Aspekte von JSF kennen zu lernen. Der Leser sollte bereits wenigstens mit den Grundlagen des HTTP Protokolls und HTML Dokumenten vertraut zu sein.

Die Anleitung ist für JSF 2.0 bis 2.3 gültig. Ob sie für neuere Versionen passt, weiß ich nicht.

Was ist JSF?

JSF ist eine Komponente der Java EE Standards, vorgesehen für die schnelle Entwicklung von Web Anwendungen im Frontend Bereich. Es trennt die Darstellungsschicht von der Programmlogik, jedoch etwas anders als beim Modell 2 (MVC Pattern).

Die JSF Template Engine erzeugt HTML Seiten auf Basis von Vorlagen, indem sie Platzhalter mit Daten aus Java Beans ausfüllt und Methoden aufgrund von Ereignissen (z.B. Klick auf einen Button) aufruft. Beispiel für so einen Platzhalter:

Sie haben sich mit dem Namen #{user.name} angemeldet.

Ihre Java Klassen enthalten kein HTML und die Templates enthalten keinen Java-Code. Auf diese Weise lässt sich die Arbeit des Java Programmierers sauber von der des HTML Programmierers trennen. Für den Datenaustausch zwischen Java Bean und Webseite validiert und konvertiert JSF die Werte automatisch (z.B. von String nach Integer und zurück).

Facelets

Facelets sind Templates, also Vorlagen für Webseiten mit der Dateiendung *.xhtml. Bei XHTML müssen alle Tags XML konform sein:

<p>
   Das ist der erste Absatz.<br/>
   Nächste Zeile
</p>
<p>
   Das ist der zweite Absatz.<br/>
   Nächste Zeile
</p>

Facelets enthalten neben XHTML Tags auch Ausdrücke aus der Expression Language EL und JSF Tags. EL Ausdrücke sehen so aus:

#{user.name}
#{geld.eingang - geld.ausgang}
#{auto.farbe != rot}

Zum Aufbau von bedingten Abschnitten, Schleifen und anderen Kontrollstrukturen verwendet man JSF Tags. Beispiele:

<ui:fragment rendered="#{user.alter > 12}">
    Du bist alt genug
</ui:fragment>

<ui:repeat var="auto" value="#{meinBean.alleAutos}">
    Das Auto #{auto.name} ist #{auto.farbe}.
</ui:repeat>

Weiterhin gibt es in der Tags zum Erzeugen von HTML Elementen. Zum Beispiel erzeugt das h:selectBooleanCheckbox Tag eine Check-Box, die an eine booleansche Variable gebunden ist. Andere Dateiformate sind theoretisch möglich, aber meines Wissens nach nicht umgesetzt worden.

Performance

JSF bietet ungefähr gleich gute Performance, wie andere Web Frameworks, also mittelmäßige. Einerseits wird die Softwareentwicklung vereinfacht, was andererseits signifikant Rechenleistung kostet. Doch Rechenleistung ist meist billiger, als Entwicklungs-Aufwand.

Für Anwendungen, die Zig-Megabyte große Dokumente in kürzester Zeit erzeugen müssen, würde ich jedenfalls Servlets ohne irgendwelche Frameworks bevorzugen. Doch das kommt nicht oft vor.

Was brauche ich mindestens?

Um eine JSF Anwendung zu entwickeln, brauchst du mindestens

Ladedir das Beispiel-Projekt JSFTest.zip herunter, darauf beziehen sich die folgenden Erklärungen.

Konfiguration

Maven

Das Beispielprojekt benutzt Maven zur Projektveraltung. Dadurch hängt es nicht von einer bestimmten Entwicklungsumgebung ab. Die meisten Entwicklungsumgebungen unterstützen und enthalten Maven bereits, so dass man das Projekt ohne weitere Installation einfach öffnen bzw. importieren kann. Maven downloaded alle benötigten Bibliotheken automatisch von einem öffentlichen Repository und ruft dann den Java Compiler auf. Durch die Unterstützung zahlreicher Plugins hat es sich zum de-facto Standard in der Java Entwicklung etabliert.

Das Maven Projekt wird durch die Datei pom.xml im Hauptverzeichnis konfiguriert:

<?xml version='1.0' encoding='UTF-8' ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    <packaging>war</packaging>

    <!-- Unique identifier of this project -->
    <groupId>de.stefanfrings</groupId>
    <artifactId>JSFTest</artifactId>
    <version>1.0.7-SNAPSHOT</version>

    <!-- Text encoding and min. required Java version --->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>
    
        <!-- JSF API --->
        <dependency>
            <groupId>javax.faces</groupId>
            <artifactId>javax.faces-api</artifactId>
            <version>2.2</version>
        </dependency>
        
        <!-- JSF implementation --->
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.faces</artifactId>
            <version>2.2.20</version>
        </dependency>
        
        <!-- Servlet API --->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <!-- Provided by Tomcat, do not include in *.war file --->
            <scope>provided</scope>
        </dependency>
        
    </dependencies>

    <build>
        <!-- Produce a *.war file name without version number --->
        <finalName>${project.name}</finalName>
    </build>
</project>

Wenn sie lieber die alten Versionen JSF 2.0 oder 2.1 verwenden wollen, müssen sie in allen Dateien im webapp Verzeichnis die Zeichenfolge "xmlns.jcp.org" durch "java.sun.com" ersetzen.

Maven setzt eine bestimmte Verzeichnisstruktur voraus, die das vorliegende Projekt demonstriert. Man compiliert das Projekt mit dem Befehl mvn clean install. Das Ergebnis ist die Datei target/JSFTest.war.

Anschließend kannst du die Datei JSFTest.war in ihrem Applikationsserver deployen. Bei Tomcat geht das am einfachsten auf seiner Manager-Seite bei "Select WAR file to upload".

Servlets

Jede Java Web Anwendung hat als erste Konfigurationsdatei die webapp/WEB-INF/web.xml. JSF ist ein Aufsatz auf die Servlet API, folglich ist ein Servlet zu konfigurieren:

<?xml version='1.0' encoding='UTF-8' ?>
<web-app version="3.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <display-name>JSF Test Application</display-name>

    <context-param>
        <description>Development or Production</description>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    
    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

Mit dem Kontext-Parameter definiert man den Projekt-Status. In der Entwicklungsphase erzeugt JSF auf Kosten der Performance detailliertere Fehlermeldungen. Der Wert kann mit Application.getProjectStage() abgefragt werden.

Die Beispielapplikation bindet das JSF Servlet an das URL-Muster *.xhtml. HTTP Requests mit dieser Endung werden durch JSF beantwortet. Du musst das Servlet entweder wie in diesem Beispiel an die Dateiendung binden, oder an einen Pfad, z.B. facelets/*.

Manche Entwickler binden JSF an die Dateieindung *.jsf. Die Templates müssen aber trotzdem *.xhtml heissen. Die Datei index.xhtml ruft man dann im Web-Browser als http://localhost:8080/JSFTest/index.jsf auf.

Faces-Config

Die Konfiguration des JSF Frameworks befindet sich in der Datei webapp/WEB-INF/faces-config.xml. Ab Version 2.0 kann JSF aber weitgehend durch die viel bequemeren Annotationen konfiguriert werden. Deswegen ist die Datei inzwischen optional.

Ich will nur kurz zeigen wie die Datei webapp/WEB-INF/faces-config.xml in dem Beispielprojekt aussieht:

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2" 
    xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    <application>
        <resource-bundle>
            <base-name>texte</base-name>
            <var>messages</var>
        </resource-bundle>
    </application>

    <lifecycle>
        <phase-listener id="nocache">de.butterfly.DisableCache</phase-listener>
    </lifecycle>

</faces-config>

Die Einträge erkläre ich weiter unten in den relevanten Kapiteln.

Facelets

Zuerst möchte ich dir zeigen, wie ein JSF Template (Facelet) auszusehen hat. Man kann diese Templates bereits nutzen, ohne eine einzige Zeile Java Programmcode schreiben zu müssen. Das erste Template ist webapp/index.xhtml mit folgendem Inhalt:

index.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Startseite</title>
    </h:head>

    <h:body>
        Hallo.
        <br/>
        <br/>
        Die Zahl ist: #{param['zahl']}
        <br/>
        Deim Browser ist ein: #{header['User-Agent']}
        <br/>
        Das Cookie "JSESSIONID" hat den Wert: #{cookie['JSESSIONID'].value}
        <br/>
        Der Username in der Session lautet: #{sessionScope['userName']}
        <br/>
        Deine Session-ID ist: #{session.id}
        <br/>
        Drei mal Sieben ist: #{3*7}
        <br/>
        <br/>
        <a href="index.xhtml?zahl=12">klick mich</a>
    </h:body>
</html>


Dieses Beispiel zeigt einige Spezial-Variablen, die vom JSF Framework gesetzt werden: Request Parameter, Request Header, Cookie-Werte und die HTTP Session.

Alle Map basierten Objekte kann man mit der Syntax name['key'] ansprechen. Listen und Arrays spricht man im Prinzip genau so an: name[index]. Wenn du dieses Beispiel ausprobierst, wirst du keinen Usernamen sehen, weil dieses Session-Attribut nicht gesetzt ist, also null ist.

Das obige Beispiel mit der Session-ID ruft vom HttpSession Objekt die Methode getId() auf und beim Cookie ruft es die Methode getValue() auf.

Für booleansche Werte gibt es noch die is-Methode. Zum Beispiel würde user.erwachsen die Methode getErwachsen() oder isErwachsen() vom user-Objekt aufrufen.

Dieses Prinzip lässt sich intuitiv auf beliebig viele Unter-Objekte anwenden. Mit user.daten.adresse.PLZ rufst du vom User-Objekt die Methode getDaten() auf, von dem Daten-Objekt wird die Methode getAdresse() aufgerufen und von der Adresse wird die Methode getPLZ() aufgerufen.

Wenn du auf den Link "klick mich" klickst, wir die Seite erneut geladen, aber mit dem URL Parameter "zahl=12". Dieser spiegelt sich in der obersten Ausgabe "Die Zahl ist: #{param['zahl']}" wieder.

Listen und Beans

Im nächsten Beispiel zeige ich dir, wie du mit einem Template Daten aus einem Java Bean anzeigen. In diesem Fall soll es eine Liste mit Namen sein. In allen folgenden Java Codes habe ich der Übersicht wegen die import Statements weg gelassen.

Namen.java:

@ManagedBean
@ApplicationScoped
public class Namen {

    private ArrayList<String> meineNamen=new ArrayList<String>();
    
    public Namen() {
        meineNamen.add("Silke");
        meineNamen.add("Marvin");
        meineNamen.add("Pumuckl");
    }

    public int getAnzahl() {
        return meineNamen.size();
    }

    public List<String> getListe() {
        return meineNamen;
    }
}

Die Annotation @ManagedBean bewirkt, dass das Bean automatisch bei Bedarf instantiiert wird und der Template Engine bekannt gemacht wird. Es ist egal, in welchem Package die Klasse liegt. Bei Managed-Beans zählt nur der einfache Name.

Die Annotation @ApplicationScoped legt fest, daß dieses Bean genau einmal geladen wird und dann solange im Speicher bleibt, wie die Applikation läuft. Die Variante @RequestScoped würde hingegen bewirken, daß für jeden HTTP Request eine neue eigene Instanz des Beans erzeugt würde. Ein @SessionScoped Bean wird in der HTTP-Session abgelegt und ggf. wieder verwendet. Für interaktive Webseiten gibt es den @ViewScoped, welcher solange existiert, wie der Benutzer die aktuelle Seite benutzt.

Erstelle dazu passend das Template webapp/liste.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Listen</title>
    </h:head>

    <h:body>
        Die Liste enthält #{namen.anzahl} Elemente.
        <br/>
        <br/>
        <ui:repeat var="einName" value="#{namen.liste}">
           #{einName}<br/>
        </ui:repeat>    
        <br/>
        <br/>

        <h:outputText value="Die Liste ist leer" rendered="#{namen.anzahl==0}"/>

        <ui:fragment rendered="#{namen.liste.size()!=0}">
            Die Liste ist nicht leer
        </ui:fragment>
    </h:body>
</html>

Das Tag ui:repeat wird verwendet, um die Elemente von Listen anzuzeigen. Alternativ gibt es das Tag ui:dataTable speziell zum Erzeugen von Tabellen.

Das Tag h:outputText ist nützlich, um die Ausgabe an eine Bedingung anzuknüpfen. Alternativ kannst du auch ui:fragment verwenden. Wegen einem Bug markieren die meissten Entwicklungsumgebungen das rendered Attribut vom ui:fragment Tag als fehlerhaft. Ignoriere die Meldung einfach. Dieses Attribut ist definitiv gültig, da es spezifiziert ist und auch tatsächlich funktioniert.

Beachte, wie ich die Länge der Liste abgefragt habe. Im ersten Fall wird mit namen.anzahl die Methode Namen.getAnzahl() aufgerufen. Weniger Tipparbeit hat man, wenn man die Liste direkt befragt, wie das Beispiel darunter zeigt. Da die Methode der Liste aber nicht getSize() heisst, sondern einfach nur size() muss man hier die Klammern mitschreiben.

Wenn du im Internet nach Methoden für bedingte Ausgaben suchst, wirst du ganz sicher auf die Tags c:if und c:choose stossen. Verwende diese Tags nicht! Das sind nämlich JSTL Tags für JSP. Wenn du in einem Template JSTL und JSF Tags vermischt einsetzen, kommt es früher oder später zu unerwarteten Fehlfunktionen. Ich finde es von Oracle besonders gemein, dass sie die JSTL und JSP Tags in einem gemeinsamen Dokument zusammengefasst haben, ohne davor zu warnen.

Schaue dir die Beschreibung der Expression Language an, um herauszufinden welche Möglichkeiten sie für die Formulierung von Ausdrücken bietet.

Formulare

Bei Formularen kümmert sich JSF um die Validierung, Typ-Konvertierung und den Austausch der Eingabefelder zwischen dem HTML Formular und der Bean-Klasse. Bei Klicks auf Aktions-Buttons werden Methoden einer Controller-Klasse aufgerufen.

Man kann für Daten-Bean und Aktions-Controller separate Klassen definieren. Oft ist es jedoch einfacher, beides miteinander zu kombinieren, wie in diesem Beispiel:

@ManagedBean
@SessionScoped
public class PersonFormular implements Serializable {

    private static final long serialVersionUID=1L;

    private String name=null;
    private Integer einkommen=null;

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

    public void setEinkommen(Integer einkommen) {
        this.einkommen=einkommen;
    }

    public String getName() {
        return name;
    }

    public Integer getEinkommen() {
        return einkommen;
    }

    public String speichern() {        
        if (einkommen==101) {
            FacesContext.getCurrentInstance().addMessage("das_einkommen",
                    new FacesMessage("101 ist ausnahmsweise nicht erlaubt"));
            return null;
        }
        else {
            System.out.println("Speichere "+name+", "+einkommen);
            return "gespeichert";
        }
    }

    public String abbrechen() {
        return "abgebrochen";
    }
}

Dieser Controller hat zwei private Felder, welche die Werte aus dem Formular aufnehmen sollen. Dazu passend hat er Setter und Getter Methoden, wie man es von einem Bean erwartet.

Die Methode speichern() soll aufgerufen werden, wenn der Benutzer auf den Sende-Button des Formulars klickt. Die Methode abbrechen() soll aufgerufen werden, wenn der Benutzer auf den Abbrechen-Button klickt.

Zu diesem Formular gehören drei Templates, die in einem eigenen Unterverzeichnis liegen, so daß man erkennen kann, welche Templates zusammen gehören. Datei webapp/person/person.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Formular</title>
    </h:head>

    <h:body>
        <h:messages/>
        <h:form prependId="false">

            Name:
            <h:inputText id="der_name" value="#{personFormular.name}" 
                required="true" requiredMessage="Du musst einen Namen eingeben" />
            <h:message for="der_name"/>
            <br/>

            Einkommen:
            <h:inputText id="das_einkommen" value="#{personFormular.einkommen}" 
                validatorMessage="Die Zahl muss im Bereich 100-100000 liegen" 
                converterMessage="Das ist keine Zahl">
                <f:validateLongRange  minimum="100" maximum="100000"/>
            </h:inputText>
            <h:message for="das_einkommen"/>
            <br/>

            <h:commandButton value="Speichern" action="#{personFormular.speichern}"/>
            <h:commandButton value="Abbrechen" action="#{personFormular.abbrechen}"/>

        </h:form>
    </h:body>
</html>

Datei webapp/person/gespeichert.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Startseite</title>
    </h:head>

    <h:body>
        Deine Eingabe wurde gespeichert.
        <br/>
        <a href="person.xhtml">Weiter</a>
    </h:body>
</html>

Datei webapp/person/abgebrochen.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Startseite</title>
    </h:head>

    <h:body>
        Du hast die Eingabe abgebrochen.
        <br/>
        <a href="../index.xhtml">Weiter</a>
    </h:body>
</html>

Das Eingabeformular sieht im Web-browser so aus:

Schaue dir im Web Browser an, was für einen HTML Quelltext die Template Engine erzeugt hat.

Das h:inputText Tag erzeugt ein einfaches Formularfeld, aber auch Java Code zur Validierung der Eingabe. Das Attribute required="true" legt fest, daß in dem Namens-Feld eine Eingabe erforderlich ist. Das Attribute requiredMessage legt den Meldungstext fest, der im Fehlerfall erscheinen soll.

Das Eingabefeld für das Einkommen darf der Benutzer aber leer lassen. Dafür prüfen wir hier, ob die Zahl zwischen 100 und 100000 liegt. Die converterMessage wird angezeigt, wenn die Eingabe nicht in einen Integer umgewandelt werden kann. Wie du siehst, findet die Typumwandlung ganz automatsich statt. JSF wandelt alle einfachen Datentypen automatisch um, also Integer Long, Float, usw.

Das Tag h:messages zeigt alle Fehlermeldungen als Liste an. Wer die Fehlermeldungen lieber neben den Eingabefeldern anzeigen möchte, muß jedem Eingabefeld eine ID geben und das Tag h:message zusammen mit der ID benutzen.

Schauen Sie sich die speichern() Methode an. Hier wird eine weitere Variante der Validierung vorgeführt. In diesem Fall wird eine Fehlermeldung erzeugt, wenn das Einkommen=101 ist. Diese Art der Validierung bietet sich besonders bei entweder/oder Feldern an, wo der Benutzer wenigstens eins von beiden Felder ausfüllen muss.

Es gibt noch eine dritte Möglichkeit, Eingaben zu validieren. Du kannst mit dem Attribut validator beim h:inputText eine eigene Klassen-Methode angeben, die zur Validierung verwendet werden soll. Auf sehr ähnliche Art kannst du mit dem Attribut converter eine eigene Methode zur Typ-Konvertierung anbieten.

Die Aktions-Methoden speichern() und abbrechen() geben immer einen String zurück. Dieser String bestimmt, welches Template als Nächstes angezeigt werden soll. Der besondere Wert null bewirkt, daß das aktuelle Formular erneut angezeigt wird.

Beachte, daß ich die Controller Klasse als @SessionScoped gekennzeichnet habe. Wenn du das Formular schon einmal aufgefüllt haben und es später erneut aufrufen, siehst du deine alten Eingaben wieder.

Wenn du die Attribute validatorMessage und converterMessage weg lässt, verwendet JSF Meldungstexte aus jsf-impl.jar:/com/sun/faces/resources/*.properties.

Internationalisierung

Bei der Internationalisierung geht es darum, einzelne Textabschnitte in mehreren Sprachen bereit zu stellen. Diese Text-Stücke legst du in einem Satz Properties-Dateien ab, welches in der faces-config.xml registriert wird.

Datei webapp/WEB-INF/classes/texte.properties:

welcome=Herzlich Willkommen
and=und
goodbye=auf Wiedersehen
prefer_number=Bevorzugst Du die Zahl {0} oder {1}?

Datei webapp/WEB-INF/classes/texte_en.properties:

welcome=Welcome
and=and
goodbye=goodbye
prefer_number=Do you prefer the number {0} or {1}?

Beide Dateien enthalten einige Texte. Der unterste (Bevorzugst Du die Zahl {0} oder {1}?) enthält Platzhalter für Variablen.

Datei webapp/WEB-INF/faces-config.xml

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    <application>
        <resource-bundle>
            <base-name>texte</base-name>
            <var>messages</var>
        </resource-bundle>
    </application>

</faces-config>

Hiermit werden die Dateien texte*.properties ins JSF Projekt eingebunden. Deren Inhalt wird den Templates unter dem Namen messages bereit gestellt.

Das Folgende Template webapp/hello.xhtml zeigt wie man diese Properties verwenden kann:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

<f:view locale="#{facesContext.externalContext.requestLocale}">
    <h:head>
        <title>JSF Test Internationalisierung</title>
    </h:head>

    <h:body>
        #{messages.welcome}
        #{messages['and']}
        #{messages.goodbye}

        <br/><br/>

        <h:outputFormat value="#{messages.prefer_number}">
            <f:param value="7"/>
            <f:param value="13"/>
        </h:outputFormat>

        <br/><br/>
        2000+1/3=
        <h:outputText value="#{2000+1/3}">
            <f:convertNumber maxFractionDigits="4"/>
        </h:outputText>

        <br/><br/>
        <h:form>
            <h:inputText value="#{sessionScope['zahl']}">
                <f:converter converterId="integerConverter"/>
            </h:inputText>
            <h:commandButton type="submit" value="Senden"/>
        </h:form>

    </h:body>
</f:view>
</html>

Auf dem Bildschirm sieht es so aus, wenn ich in meinen Web-Browser die bevorzugte Sprache auf Englisch stelle:

Das Tag f:view ist neu. Über dessen locale Attribut bestimmst du die Darstellungssprache. Anstatt einen festen Wert (wie z.B. "de") anzugeben, benutzen wir die Sprache, die der aufrufende Benutzer in seinem Web-Browser eingestellt hat. Für englische Benutzer wird die englische Properties Datei verwendet und für deutsche Benutzer wird die deutsche Datei verwendet.

Ohne das f:view Tag würde die Applikation sich stattdessen an der Sprache des Servers orientieren. Wenn der Server z.B. ein deutsches Windows ausführt, würden die Webseiten auf deutsch erscheinen, auch wenn der Benutzer ein Engländer ist.

Bei der Meldung für "and" bzw. "und" musste eine andere Syntax gewählt werden, weil "and" kein gültiger Java Methoden-Name ist. Bei der gewählten alternativen Syntax wird "and" aber als String gewertet, nicht als Methoden-Name.

Darunter siehst du, wie bei prefer_number die Platzhalter {0} und {1} mit konkreten Werten ausgefüllt werden. In diesem Fall wird {0} durch die "7" ersetzt und {1} wird durch die "13" ersetzt.

Darunter siehst du, wie man Zahlen formatiert. Der f:numberConverter unterstützt viele weitere Parameter, mit denen du die Formatierung beeinflussen kannst. Für Datum und Uhrzeit verwenden f:convertDateTime. Die beiden Konverter berücksichtigen die Landes-spezifische Schreibweise des Benutzers, sofern du entweder ein f:view Tag wie oben gezeigt drumherum bauen oder den Paramater locale verwendets.

Wie man eigene Konverter schreibt, zeigt das letzte Beispiel mit dem Formular-Feld. Hier kannst du eine Zahl in hexadezimaler schreibweise eingeben, die dann als Integer in der Session gespeichert wird und als natürliche Zahl formatiert ausgegeben wird. Aus "0xFF" wird nach Klick auf den Senden-Button eine "255". Der Konverter wird dabei zweimal aufgerufen. Beim Senden des Formulars wird die getAsObject(...) Methode aufgerufen, und beim Laden der Seite wird getAsString(...) aufgerufen.

IntegerConverter.java:

@FacesConverter("integerConverter")
public class IntegerConverter implements Converter {

    public Object getAsObject(FacesContext facesContext, 
      UIComponent uiComponent, String param) {
        return Integer.decode(param);
    }

    public String getAsString(FacesContext facesContext, 
      UIComponent uiComponent, Object obj) {
        return String.valueOf(obj);
    }
}

Die Annotation @FacesCoverter benötigt als Parameter einen Namen, entsprechen dem converterId Parameter im Template. Die Konvertierung von hexadezimaler Schreibweise zu Integer übernimmt hier die Methode Integer.decode().

Properties Dateien

Beachte folgende Regel, für das Laden von Properties Dateien:

Diese Regel führt unter Umständen zu einem unerwarteten Verhalten. Ein Beispiel:

In diesem Fall sieht der engliche Benutzer unerwartet deutsche Texte. Denn es gibt keine Datei mit der Endung _en, doch weil es eine Datei in der Sprache des Servers gibt (nämlich _de) wird diese Ersatzweise herangezogen. Aus genau dem gleichen Grund würde ein schwedischer Benutzer ebenfalls deutsche Seiten sehen, obwohl er warscheinlich mit englischem Text viel mehr anfangen kann.

Du kannst dieses merkwürdige Verhalten umgehen, indem du entweder sicherstellst, dass die Sprache der Standard-Datei der Sprache des Server entspricht (wie im obigen Beispiel, dort sind beide deutsch) oder indem du eine leere Properties Datei mit Sprach-Endung anlegen, die der Sprache der Standard-Datei entspricht. Also zum Beispiel eine texte.properties in englisch, dazu eine leere texte_en.properties, sowie beliebig viele Übersetzungen in anderen Sprachen.

JSF enthält bereits Properties Dateien für die Meldungen der eingebauten Validatoren (siehe vorheriges Kapitel). Es gibt viele Möglichkeiten, diese Meldungen durch eigene zu ersetzen. Ich bevorzuge folgende Methode: Kopiere die Dateien jsf-impl.jar:/com/sun/faces/resources/*.properties in ihr Source-Verzeichnis (also nach src/main/java/com/sun/faces/resources) und editiere sie dort. Der Classloader sucht dort nämlich zuerst und wird daher ihre modifizierten Dateien bevorzugt laden.

Falls du einmal in Java Code ein solches Property verwenden musst, tue das so:

public static String getWording(String key, Object... args) {
    FacesContext ctx=FacesContext.getCurrentInstance();
    ResourceBundle bundle=ctx.getApplication().getResourceBundle(ctx, "messages");
    String message=bundle.getString(key);
    return MessageFormat.format(message, args);
}

String result=getWording("prefer_number",7,13);

Oder binde das ResourceBundle via DependencyInjection ein, siehe nächstes Kapitel.

Dependency Injection

Du hast gelernt, daß JSF die Managed-Beans automatisch bei Bedarf instantiiert und der Template Engine (also den Facelets) verfügbar macht. Mittels Dependency Injection kannst du solche Beans auch anderen Klassen zur Verfügung stellen. Die folgende Klasse demonstriert dies.

Injection.java:

@ManagedBean
@RequestScoped
public class Injection {

    @ManagedProperty(value="#{namen}")
    private Namen namen;
    
    public void setNamen(Namen namen) {
        this.namen=namen;
    }
    
    public Namen getNamen() {
        return namen;
    }

    public String getSecondName() {
        return namen.getListe().get(1);
    }
}

Die Annotation @ManagedProperty bewirkt, daß beim Erzeugen des Objektes "Injection" auch eine Instanz vom Objekt "Namen" erzeugt wird, und dieses dann über den Setter setNamen() zugewiesen wird. Wenn du @ManagedProperty benutzt, muss immer auch ein passender Setter vorhanden sein.

Datei webapp/injection.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Dependency Injection</title>
    </h:head>

    <h:body>
        Der zweite Name ist #{injection.secondName}.
        <br/><br/>
        Das Objekt injection ist #{injection.toString()}.
        <br/><br/>
        Das Objekt namen ist #{injection.namen.toString()}.
    </h:body>
</html>

Dieses Facelet ruft die Methode Injection.getSecondName() auf, welche den zweiten Namen aus der bereits bekannten Namens-Liste liefert. Spannender sind die beiden folgenden Zeilen, welche die Hash-Codes der beteiligten Objekte anzeigen. Wenn du diese Webseite mehrmals lädst (F5 drücken), dann siehst du, dass das Objekt "injection" bei jedem Request immer wieder neu erzeugt wird, während das Objekt "namen" immer das Gleiche bleibt. Dies liegt an den unterschiedlich festgelegten Scopes.

Die Webseite sieht so aus:

Serialisierung

Beans, die SessionScoped oder ViewScoped sind, müssen serialisierbar sein, weil sie in der Session des Benutzers gespeichert werden. Der Server (z.B. Tomcat) lagert die Session in Dateien aus, wenn die Applikation neu gestartet wird. Und im Cluster werden Sessions eventuell zwischen den Nodes synchronisiert. Um Performance und Speicherbedarf zu optimieren, sollte man vermeiden, große Mengen von Daten in die Session zu schreiben.

Eine Klasse ist dann serialisierbar, wenn sie mit "implements Serializable" deklariert wurde. Alle Felder der Klasse müssen ebenfalls entweder serialisierbar sein, oder als "transient" gekennzeichnet sein, sonst schlägt die Serialisisierung zur Laufzeit fehl. Die folgenden beiden Fehlerszenarien sind typisch:

  1. Wenn eine SessionScoped oder ViewScoped Klasse nicht serialisierbar ist, geht der Status der Klasse bei Neustart der Applikation verloren. Als Entwickler solltest du daher testen, ob die Applikation (nicht der ganze Server) korrekt gestoppt und wieder gestartet werden kann. Die entsprechende Warnmeldung von Tomcat lautet: "Cannot serialize session attribute ...". Wenn die Applikation ohne Warnmeldung neu gestartet werden kann und dabei den Status der Beans nicht vergisst, dann wird die Applikation warscheinlich auch im Cluster funktionieren.
  2. Nach dem Zurücklesen serialisierter Objekte sind alle transienten Felder null oder 0. DU kannst die Methode readResolve() überschreiben, um transiente Felder nach dem Deserialisieren zu initialisieren.

Die Klasse TransientTest und die Webseite transient.xhtml aus dem Test-Projekt demonstrieren dies.

TransientTest.java:

@ManagedBean
@SessionScoped
public class TransientTest implements Serializable {
    private static final long serialVersionUID = 1L;
    
    Date date=new Date();    
    transient Integer a=1;
    transient Integer b;
    transient Integer c;
    
    public TransientTest() {
        b=2;
        c=3;
    }
    
    private Object readResolve() {
        c=4;
        return this;
    }
    
    public Date getDate() {
        return date;
    }
    
    public Integer getA() {
        return a;
    }
    
    public Integer getB() {
        return b;
    }
    
    public Integer getC() {
        return c;
    }
    
}
Datei webapp/transient.xhtml:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

    <h:head>
        <title>JSF Test Transient</title>
    </h:head>

    <h:body>
        Object created: #{transientTest.date}<br/>
        <br/>
        a=#{transientTest.a}<br/>
        b=#{transientTest.b}<br/>
        c=#{transientTest.c}<br/>
    </h:body>
</html>

Rufe die Webseite auf:

Re-Starte dann den Server und lade die Webseite neu, ohne den Browser zwischenzeitlich zu schließen:

Am unveränderten Datum erkennst du, dass das managed Bean den Neustart des Servers überlebt hat. Das Datum ist das einzige serialisierbare Feld der Klasse. Die Werte der transienten Felder (a, b und c) sind verloren gegangen. C ist allerdings beim Wiederherstellen des Objektes mit einem neuen Wert initalisiert worden.

Phase Listener

Eventuell ist dir beim Testen aufgefallen, dass der Web Browser machmal eine veraltete Version der Webseite aus seinem Cache angezeigt hat, anstatt stets die aktuelle Version vom Server abzurufen. Du kannst dies verhindern, indem du einen sogenannten "Phase Listener" hinzufügst, der zu allen HTTP Antworten zwei HTTP Response Header hinzufügt:

package de.butterfly;

import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpServletResponse;

public class DisableCache implements PhaseListener {

    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }

    public void afterPhase(PhaseEvent event) {
    }

    public void beforePhase(PhaseEvent event) {
        FacesContext facesContext = event.getFacesContext();
        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
        
        response.addHeader("Pragma", "no-cache");
        response.addHeader("Cache-Control", "no-cache");
    }
}

Diese Klasse wird von JSF immer aufgerufen, wenn eine HTTP Antwort generiert wird. Sie fügt vor das XHTML Dokument folgende HTTP Header ein:

Die folgende Ergänzung in Datei webapp/WEB-INF/faces-config.xml aktiviert die neue Java Klasse:

    <lifecycle>
         <phase-listener id="nocache">de.butterfly.DisableCache</phase-listener>
     </lifecycle>

Von nun an wird der Web Browser keine JSF Seiten mehr zwischenspeichern.

Zusammenspiel mit CDI

Java Server Faces wurde mit einer eigenen Bean Verwaltung entwickelt, die in Konkurrenz zu CDI steht. Ab JSF 2.3 wird über "deprecated" Markierungen empfohlen, nur noch CDI zu benutzen. Allerdings steht diese Technologie nur auf den großen Java EE Servern (wie z.B. Glassfish, Wildfly und Websphere) zur Verfügung.

Wenn man CDI nutzt, ergeben sich folgende Ersetzungen:

javax.faces.bean.ManagedBean        ->   javax.inject.Named (oder weg lassen)
javax.faces.bean.ApplicationScoped  ->   javax.enterprise.context.ApplicationScoped
javax.faces.bean.RequestScoped      ->   javax.enterprise.context.RequestScoped
javax.faces.bean.SessionScoped      ->   javax.enterprise.context.SessionScoped
javax.faces.bean.ViewScoped         ->   javax.enterprise.context.ConversationScoped

Wenn man jedoch einen Applikationsserver ohne CDI Unterstützung (wie Tomcat) verwendet, ist es weiterhin noch in Ordnung, die Annotationen und Bean Verwaltung von JSF zu benutzen.

Nachwort

Ich hoffe, daß du nun einen groben Eindruck davon hast, wie man JSF einsetzen kann. Auf dieser Webseite habe ich längst nicht alle Funktionen von JSF beschrieben, darum schaue dir bei Gelegenheit die weitergehenden Informationen an, auf die ich verwiesen habe.

Achte bei der Suche nach Dokumentation auf die richtige Version. Die Dokumentation von JSF 1.2 ist zu alt. Lasse dich nicht dazu verleiten, JSP Tags aus der JSTL Bibliothek in ihren Facelets zu verwenden, da dies zu sehr schwer greifbaren Fehlfunktionen führen kann.

Testen die Serialisierung gründlich, um sicherzustellen, dass die Applikation in Produktion nach einem Server-Ausfall oder gewolltem Neustart funktioniert.

Versuche nicht, die Kommunikation zwischen Web-Browser und managed Bean am Framework vorbei zu implementieren. Denn dies führt zu sporadischen Fehlfunktionen, z.B. dass einzelne Benutzereingaben nicht gespeichert werden oder Aktions-Methoden nach Klick auf Buttons nicht aufgerufen werden.

Weiterführende Doku:

Eine interessante Alternative zu Tomcat ist Jetty, damit machst du deine eigene Anwendung zum Applikationsserver, so dass man den Tomcat nicht separat installieren muss.

Schaue dir mal das Projekt Lombok an. Es erspart dir einige Tipparbeit und macht die Quelltexte der Beans übersichtlicher.