Java Server Faces (Basics mit Facelet) (JavaEE 8)


Inhalt:

Projekt erstellen
ManagedBean "GeometricModelHandler"
Hilfsklasse "History"
Facelet
"beans.xml"
JSF 2.3 aktivieren
Startseite
JSF-Facet zu bestehendem Projekt zufügen


Dieses Beispiel baut auf der gleichen Logik auf wie die JSP-Beispiele, es existiert also nur eine Web-Anwendung mit minimaler Programmlogik. Bereits durchgeführte Berechnungen werden in der Session gespeichert.
Das Beispiel baut auf JavaEE8 und WildFly 24.0 auf.

Hier gibt es das Projekt als WAR-Export-Datei: JSF.war.
Nach dem Import muss man die JSF-Facet dem Projekt zufügen, siehe JSF-Facet zu bestehendem Projekt zufügen

Für mehr Informationen zu JSF sei auf das JavaEE6-Tutorial, Part II, ab Kapitel 4 verwiesen: http://download.oracle.com/javaee/6/tutorial/doc/index.html
Hier finden sich die Spezifikationen für JSF 2.2 (JavaEE7) und JSF 2.3 (JavaEE8): https://javaee.github.io/javaserverfaces-spec/



Facelets vs. JSP
In den JSF-Spezifikationen 1.x wurden JSP-Seiten für die Abbildung der Oberfläche verwendet. Ab JSF 2 werden stattdessen Facelets verwendet, über als XHTML-Seiten umgesetzt werden. Die Deklaration der JSF-Oberfläche erfolgt über eine "View Declaration Language", von der Facelets und JSP Implementationen sind. Ab JSF 4.0 (JakartaEE10) wurde die JSP-Unterstützung entfernt.
Dieses Beispiel verwendet entsprechend eine XHTML-Seite. Das Vorgängerbeispiel für eine JSP-Seite findet sich hier.

Managed Bean vs. CDI
In früheren JSF-Spezifikationen wurde eine Bean als "Managed Bean" deklariert.
In JSF 4 wurde diese Möglichkeit entfernt. Jetzt wird CDI verwendet.


Projekt erstellen

Beim Erstellen des "Dynamic Web Project" wird die "Dynamic web module version" auf "4.0" gestellt.
Neues Projekt (1)
Außerdem ist es wichtig die Facet "JavaServer Faces 2.3" zuzufügen. Diese ist leider nicht in der "Configuration"-Combobox enthalten, sondern man muss zuerst auf "Modify..." klicken.

In dem erscheinenden Dialog steht bei "Configuration" vermutlich "Custom". In der darunter liegenden Tabelle der zu installierenden Facets wird jetzt ein Haken bei "JavaServer Faces" gesetzt und die Version 2.3 gewählt. Anschließend wird diese Configuration unter einem sinnvollen Namen (in meinem Fall: "WildFly 24 + JSF 2.3") gespeichert.
JavaServer Faces Configuration
Jetzt auf "OK" und weiter im Assistenten.

Im nächsten Schritt ("Java") bleibt alles bei den Defaults.

Im nächsten Schritt "Web Module" lassen wir uns einen Deploymentdescriptor "web.xml" genrieren:
Neues Projekt (2)

Im Schritt "JSF Capabilities" wählen wir "Libary provided by target runtime" aus, da WildFly die JSF-Implementierung bereitstellt.
Unter "URL Mapping Patterns" löschen wir die Vorgabe "/faces/*" und ersetzen sie durch "*.xhtml":
Neues Projekt (3)

Im Verzeichnis "WEB-INF" befindet sich jetzt eine neue Datei "faces-config.xml".

In "web.xml" wurde das Faces-Servlet eingebunden:
	<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>

Da wir die JSTL-Library verwenden wollen (Version 1.2), wird die Datei "jstl-1.2.jar" von
https://repo.maven.apache.org/maven2/jstl/jstl/1.2/ heruntergeladen und nach "WEB-INF\lib" kopiert. Alternativ könnte man auch die in WildFly enthaltene Datei in "%WILDFLY_HOME%\modules\system\layers\base\javax\servlet\jstl\api\main\taglibs-standard-impl-1.2.6-RC1.jar" verwenden.
verwenden. Aber auch diese muss man wohl in "WEB-INF\lib" kopieren - tut man das nicht, kann Eclipse zumindest die Tags in der JSP-Seite nicht auflösen.

Anmerkung: Die im Beispiel "JSP3" beschriebenen zusätzlichen Schritte (c.tld aus der JAR-Datei in ein Unterverzeichnis von WEB-INF legen und ein Element <jsp-config> mit der Verbindung von URI zu TLD-Datei in "web.xml" einfügen) sind normalerweise nicht nötig, Eclipse und JBoss sind intelligent genug die zur URI passende TLD-Datei selbst zu finden. Nur wenn diese nicht eindeutig wäre oder (aus welchen Gründen auch immer nicht identisch ist mit der in der TLD deklarierten) müssen diese zwei Schritte erfolgen.


ManagedBean "GeometricModelHandler"

Es wird eine Java-Klasse "GeometricModelHandler" zugefügt.
Sie benötigt folgende Definition:
import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@Named(value="geometricModelHandler")
@SessionScoped
public class GeometricModelHandler implements Serializable
{
Das Interface java.io.Serializable muss implementiert werden, da die Bean im Session Scope liegen soll. Soll sie im Request Scope liegen, ist dies nicht nötig.
Tut man das nicht, führt es zu folgender Fehlermeldung beim Deploy:
...
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-000072: Bean declaring a passivating scope must be passivation capable.  Bean:  Managed Bean [class de.fhw.komponentenarchitekturen.knauf.jsf.GeometricModelHandler] with qualifiers [@Default @Any @Named]
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bean.ManagedBean.checkType(ManagedBean.java:220)
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bean.AbstractBean.initializeAfterBeanDiscovery(AbstractBean.java:108)
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bean.ManagedBean.initializeAfterBeanDiscovery(ManagedBean.java:124)
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$AfterBeanDiscoveryInitializerFactory.doWork(ConcurrentBeanDeployer.java:111)
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.bootstrap.ConcurrentBeanDeployer$AfterBeanDiscoveryInitializerFactory.doWork(ConcurrentBeanDeployer.java:102)
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:62)
	at org.jboss.weld.core@3.1.9.Final//org.jboss.weld.executor.IterativeWorkerTaskFactory$1.call(IterativeWorkerTaskFactory.java:55)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)

Rückblick auf die Deklaration von Managed Beans in früheren JSF-Versionen: Dafür gab es zwei Möglichkeiten:

Variante 1:
Dies konnte über die Annotation javax.faces.bean.ManagedBean erfolgen. Der in diesem Beispiel verwendete Session Scope wurde mittels javax.faces.bean.SessionScoped definiert:
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean(name="geometricModelHandler")
@SessionScoped()
public class GeometricModelHandler
{

Variante 2:
Es konnte über einen Eintrag in "faces-config.xml" erfolgen:
	<managed-bean>
		<description>
			This Managed Bean performs the calculation of surface and volume of the cube.
			It stores a list of all user inputs for the current session.
		</description>
		<managed-bean-name>geometricModelHandler</managed-bean-name>
		<managed-bean-class>
			de.fhw.komponentenarchitekturen.knauf.jsf.GeometricModelHandler</managed-bean-class>
		<managed-bean-scope>session</managed-bean-scope>
	</managed-bean>


Jetzt werden der Managed Bean Properties zugefügt:
  private double dblA = 0;
  private double dblB = 0;
  private double dblC = 0;
  
  public double getA()
  {
    return dblA;
  }
  public void setA(double dblA)
  {
    this.dblA = dblA;
  }

  public double getB()
  {
    return dblB;
  }
  public void setB(double dblB)
  {
    this.dblB = dblB;
  }

  public double getC()
  {
    return dblC;
  }
  public void setC(double dblC)
  {
    this.dblC = dblC;
  }  

Es gibt get-Properties für die aktuell berechneten Werte sowie die Historie:
  private double dblOberflaeche = 0;
  private double dblVolumen = 0;
  
  private History history = new History();

  public History getHistory()
  {
    return this.history;
  }
  
  public double getVolume()
  {
    return this.dblVolume;
  }
  
  public double getSurface()
  {
    return this.dblSurface;
  } 

Schließlich wird die Methode calculate zugefügt, die beim Klick auf "Submit" aufgerufen wird und die Berechnung durchführt sowie die aktuelle Berechnung der Historie zufügt.
  public String calculate()
  {
    //Calculate the values and store in member variables:
    this.dblVolume = this.dblA * this.dblB * this.dblC;
    this.dblSurface = 2 * (this.dblA * this.dblB) + 2 * (this.dblA * this.dblC) + 2 * (this.dblB * this.dblC);
    
    //Add to history
    SideLengths sideLengthCurrent = new SideLengths();
    sideLengthCurrent.setA(this.dblA);
    sideLengthCurrent.setB(this.dblB);
    sideLengthCurrent.setC(this.dblC);
    this.history.addSideLength(sideLengthCurrent );
    
    //Return value does not matter, because this simple sample has no navigation rules.
    return null;
  } 
Die Rückgabe einer solchen Submit-Methode ist normalerweise eine Navigationsregel die die Zielseite zurückgibt. Kommt dabei null zurück so gelangt man automatisch wieder zu der Seite die das Formular abgeschickt hat.


Hilfsklasse "History"

In der Klasse "History", in der die bereits durchgeführten Berechnungen abgelegt werden, ergeben sich zwei kleine Änderungen:
  public List<SideLengths> getSideLengths()
  {
    return this.vectorSideLengths;
  }
  
  public int getSize()
  {
    return this.vectorBerechnungen.size();
  } 
Die Methode getSize ist nötig um mittels <c:if>-Tag auf das Vorhandensein von Elementen zu prüfen (nur dann wird die Tabelle der bereits durchgeführten Berechnungen angezeigt).

getSideLengths liefert die SideLengths-Objekte als generic Liste zurück. Diese wird später über das JSF-Tag <h:dataTable> durchlaufen.


Facelet

Wir fügen ein Facelet "index.xhtml" zu.
Die Seite verwendet die JSTL-Core-Library.
Sie sieht so aus:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!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:ui="http://java.sun.com/jsf/facelets"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:c="http://java.sun.com/jsp/jstl/core">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Simple JSF sample</title>
</head>
<body>
<f:view>
  <h:form id="formGeometricModelInput">
    <h:panelGrid columns="3">
      
      <c:if test="#{geometricModelHandler.volume > 0.0}">
      	<h:outputText value="Volume:"/>
	    <h:outputText id="volume" value="#{geometricModelHandler.volume}"></h:outputText>
        <h:outputText value=""></h:outputText>
      
	    <h:outputText value="Surface:"/>
        <h:outputText id="surface" value="#{geometricModelHandler.surface}"></h:outputText>
        <h:outputText value=""></h:outputText>
      </c:if>
      
      <h:outputText value="Side a:"></h:outputText>
      <h:inputText label="Side A" id="a" value="#{geometricModelHandler.a}"></h:inputText>
      <h:message for="a"></h:message>
      
      <h:outputText value="Side b:"></h:outputText>
      <h:inputText label="Side B" id="b" value="#{geometricModelHandler.b}"></h:inputText>
      <h:message for="b"></h:message>
      
      <h:outputText value="Side c:"></h:outputText>
      <h:inputText label="Side C" id="c" value="#{geometricModelHandler.c}"></h:inputText>
      <h:message for="c"></h:message>
      
      <h:commandButton id="calculate" value="Calculate" action="#{geometricModelHandler.calculate}"></h:commandButton>
      <h:outputText value=""></h:outputText>
      <h:outputText value=""></h:outputText>
      
      <ui:remove>Print history:</ui:remove>
      <c:if test="#{geometricModelHandler.history.size > 0}">
        <ui:remove>A DataTable with three columns will contain all calculations of the current session.
         The current iteration element will be put in a variable named "sideLengthCurrent"</ui:remove>
        <h:dataTable value="#{geometricModelHandler.history.sideLengths}" var="sideLengthCurrent">
          <h:column>
            <f:facet name="header">
              <h:outputText value="A"/>
            </f:facet>
            <h:outputText value="#{sideLengthCurrent.a}"></h:outputText>
          </h:column>
          <h:column>
            <f:facet name="header">
              <h:outputText value="B"/>
            </f:facet>
            <h:outputText value="#{sideLengthCurrent.b}"></h:outputText>
          </h:column>
          <h:column>
            <f:facet name="header">
              <h:outputText value="C"/>
            </f:facet>
            <h:outputText value="#{sideLengthCurrent.c}"></h:outputText>
          </h:column>
        </h:dataTable>
      </c:if>
    </h:panelGrid>
  </h:form>
</f:view>
</body>
</html>

Die Elemente im Einzelnen:


"beans.xml"

Beim Deploy wird die Anwendung folgende Fehlermeldung anzeigen:
21:28:51,922 ERROR [stderr] (ServerService Thread Pool -- 57) javax.faces.FacesException: Unable to find CDI BeanManager
21:28:51,922 ERROR [stderr] (ServerService Thread Pool -- 57) 	at com.sun.jsf-impl@2.3.14.SP04//com.sun.faces.application.applicationimpl.Version.isJsf23(Version.java:62)
21:28:51,922 ERROR [stderr] (ServerService Thread Pool -- 57) 	at com.sun.jsf-impl@2.3.14.SP04//com.sun.faces.application.applicationimpl.ExpressionLanguage.addELResolver(ExpressionLanguage.java:140)
...
Lösung:
Es muss eine Datei "WEB-INF\beans.xml" mit folgendem Inhalt angelegt werden:
<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_2_0.xsd"
    version="2.0" bean-discovery-mode="annotated">
</beans>


JSF 2.3 aktivieren

Siehe https://github.com/eclipse-ee4j/mojarra/blob/2.3/README.md#user-content-activating-cdi-in-jakarta-faces-23: Auch wenn in "faces-config.xml" die Version 2.3 eingetragen ist, läuft JSF trotzdem teilweise im 2.2-Modus. Um dies zu umgehen, muss irgendwo bei einer Klasse die entsprechende JSF-Version per Annotation deklariert werden.
Der Übersichtlichkeit halber habe ich das auf einer eigenen Klasse "Jsf23Activator" getan:

package de.fhw.komponentenarchitekturen.knauf.jsf;

import javax.enterprise.context.ApplicationScoped;
import javax.faces.annotation.FacesConfig;

@ApplicationScoped
@FacesConfig(version = FacesConfig.Version.JSF_2_3)
public class Jsf23Activator {
}
Meine Beispiel nutzen zwar keine Features, für die dies nötig wäre, aber ich erkläre diese Lösung trotzdem.


Startseite

Die Anwendung liegt in einer Datei "index.xhtml". Diese ist nicht als "Welcome file" registriert, deshalb kann man die Anwendung nicht ohne Angabe einer Unterseite aufrufen. Deshalb wird sie in "web.xml" zugefügt:
  <welcome-file-list>
    <welcome-file>index.xhtml</welcome-file>
  </welcome-file-list>


Die Anwendung findet sich unter dieser URL:
http://localhost:8080/JSF/index.xhtml.


JSF-Facet zu bestehendem Projekt zufügen

Falls man bereits ein Webprojekt ohne JSF-Unterstützung hat (z.B. nach dem Import der WAR-Datei dieses Beispiels), lässt sich dies leicht nachtragen.
Man geht in die Properties des Projekts in den Punkt "Project Facets". Dort aktiviert man die Facet "JavaServer Faces" und setzt die Version auf "2.3". Außerdem prüft man, ob die Facet "Dynamic Web Project" die richtige Version hat.
Facet zufügen (1)
Wie beim Erzeugen des Projekts wird die Variante "Library Provided by Target Runtime" gewählt.
Falls es bereits eine "faces-config.xml" im Projekt gibt, sollte man außerdem die Checkbox "Configure JSF servlet in deployent descriptor" zurücksetzen.
Facet zufügen (2)



Stand 06.01.2023
Historie:
25.12.2022: Erstellt aus vorherigem Beispiel, das den veralteten Ansatz einer JSP-Seite verwendete.
06.01.2023: @ManagedBean durch CDI ersetzt.