Java EE / Jakarta EE
Jakarta Faces (JSF) 4 : composants, cycle de vie et backing beans CDI
Intermédiaire12 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Jakarta Faces (JavaServer Faces avant le renommage Jakarta) est le framework web à composants de la plateforme, et une grande partie du logiciel d'entreprise en dépend encore. Ce guide explique le cycle de vie d'une requête en six phases, comment les pages Facelets se lient aux backing beans CDI via l'Expression Language, comment fonctionnent conversion, validation, navigation et Ajax, et ce que Faces 4.0 a supprimé, pour que vous puissiez maintenir sereinement des applications Faces existantes et juger quand l'outil reste le bon choix.
Ce que vous allez apprendre
- Expliquer le cycle de vie d'une requête Faces et la différence entre une requête initiale et un postback
- Construire une page avec des templates Facelets, des composants standard et un backing bean CDI
- Choisir le bon scope de bean et comprendre pourquoi les beans @ViewScoped doivent être sérialisables
- Appliquer convertisseurs, validateurs, Bean Validation et mises à jour partielles en Ajax
- Identifier ce qui a changé dans Faces 4.0 et pourquoi Faces est considéré comme legacy pour les nouveaux front-ends
Jakarta Faces aborde les interfaces web tout autrement qu'un framework JavaScript : le serveur conserve un arbre de composants pour chaque page, le navigateur renvoie les formulaires vers cet arbre, et le framework convertit, valide et applique les valeurs aux beans Java avant de rendre la vue suivante. Si vous venez de Spring MVC ou d'une stack REST + React, ce modèle à état explique à la fois pourquoi Faces était si productif pour les équipes 100 % Java (pas de JavaScript, pas de contrat JSON, un binding typé vers votre domaine) et pourquoi il est passé de mode quand les single-page applications et le scaling horizontal sont devenus la norme. Beaucoup de logiciels métier tournent encore dessus ; l'objectif ici est donc d'être à l'aise : savoir ce qui se passe à chaque requête pour pouvoir déboguer.
Où se situe Faces et ce qu'il est aujourd'hui
Faces est une servlet. La FacesServlet est automatiquement mappée sur *.xhtml (et sur /faces/*, *.faces et *.jsf pour les anciens liens) dès que le conteneur détecte que Faces est utilisé ; une requête vers /shop/orders.xhtml traverse donc le conteneur de servlets décrit dans le guide des servlets et aboutit dans Faces. Les pages s'écrivent en Facelets, un dialecte XHTML ; les backing beans sont des beans CDI ordinaires ; l'Expression Language (EL) relie les deux.
L'historique des versions compte, car l'API que vous voyez dans une base de code trahit son âge :
| Version | Année | Points marquants |
|---|---|---|
| JSF 1.0 / 1.2 | 2004 / 2006 | Vues JSP, faces-config.xml pour tout |
| JSF 2.0 | 2009 | Facelets comme technologie de vue par défaut, annotations, navigation implicite, Ajax intégré, view scope |
| JSF 2.2 | 2013 | Attributs pass-through adaptés à HTML5, Faces Flows, upload de fichiers, vues stateless |
| JSF 2.3 | 2017 | Intégration CDI complète (@Inject FacesContext), <f:websocket>, prise en charge des dates et heures java.time |
| Jakarta Faces 3.0 | 2020 | Passage au namespace jakarta.faces, aucun changement fonctionnel |
| Jakarta Faces 4.0 | 2022 | Suppression de @ManagedBean et des scopes jakarta.faces.bean, fin du support des vues JSP, namespaces XML courts (jakarta.faces.html), mapping automatique sans extension, @ClientWindowScoped |
Il existe deux implémentations : Eclipse Mojarra (l'implémentation compatible, embarquée dans GlassFish, WildFly et Payara) et Apache MyFaces (embarquée dans Open Liberty et TomEE). Les bibliothèques de composants (PrimeFaces en tête) fournissent les widgets riches que la plupart des applications utilisent réellement, et OmniFaces comble les manques avec ses utilitaires. Tomcat n'embarque pas Faces : vous ajoutez vous-même l'implémentation et un conteneur CDI, une raison de plus de préférer un serveur Web Profile pour travailler avec Faces.
Le cycle de vie d'une requête
Comprendre les six phases est de loin ce qu'il y a de plus utile à apprendre sur Faces, car presque toutes les questions du type « ça ne marche pas » reviennent en fait à « quelle phase s'est arrêtée, et pourquoi ».
- Restore View : pour un postback, l'arbre de composants sauvegardé de la page est restauré (depuis la session par défaut) ; pour un
GETinitial, un nouvel arbre est construit et le cycle de vie saute directement à Render Response. - Apply Request Values : chaque composant de saisie lit sa chaîne brute soumise dans les paramètres de la requête. Les composants dont l'attribut
renderedvaut false sont ignorés, et les composants d'action mettent leurs événements en file d'attente à ce stade. - Process Validations : les chaînes soumises sont converties vers le type cible, puis validées. Tout échec ajoute un
FacesMessage, marque le composant comme invalide et fait sauter le cycle de vie à Render Response : le modèle n'est jamais modifié, et aucune méthode d'action ne s'exécute. - Update Model Values : les valeurs converties et valides sont écrites dans les propriétés de bean référencées par
value="#{bean.property}". - Invoke Application : les méthodes d'action et les action listeners s'exécutent ; la valeur de retour de l'action détermine la navigation.
- Render Response : la vue est rendue en HTML et son état sauvegardé pour le postback suivant.
Placer immediate="true" sur une saisie ou une commande déplace son traitement dans la phase Apply Request Values ; c'est ainsi que les boutons « Annuler » échappent à la validation du reste du formulaire. Ajouter <h:messages globalOnly="false" /> à une page pendant le développement affiche les échecs de conversion et de validation qui resteraient sinon invisibles.
Une page et son backing bean
Un formulaire typique : une page de recherche de commandes avec un filtre, un tableau de résultats et un bouton.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
<ui:define name="title">Orders</ui:define>
<ui:define name="content">
<h:form id="search">
<h:messages />
<h:outputLabel for="customer" value="Customer" />
<h:inputText id="customer" value="#{orderSearch.customer}" required="true"
requiredMessage="Enter a customer name" />
<h:selectOneMenu id="status" value="#{orderSearch.status}">
<f:selectItem itemLabel="Any" noSelectionOption="true" />
<f:selectItems value="#{orderSearch.statuses}" />
</h:selectOneMenu>
<h:commandButton value="Search" action="#{orderSearch.search}">
<f:ajax execute="@form" render="@form results" />
</h:commandButton>
</h:form>
<h:dataTable id="results" value="#{orderSearch.results}" var="o" rendered="#{not empty orderSearch.results}">
<h:column><f:facet name="header">Id</f:facet>#{o.id}</h:column>
<h:column><f:facet name="header">Total</f:facet>
<h:outputText value="#{o.total}"><f:convertNumber type="currency" currencyCode="EUR" /></h:outputText>
</h:column>
<h:column><f:facet name="header">Placed</f:facet>
<h:outputText value="#{o.placedAt}"><f:convertDateTime type="localDate" /></h:outputText>
</h:column>
</h:dataTable>
</ui:define>
</ui:composition>
</html>
Le backing bean est un bean CDI. @Named l'expose à l'EL sous le nom orderSearch (le nom de la classe avec une initiale minuscule), et @ViewScoped le maintient en vie tant que l'utilisateur reste sur cette vue, y compris à travers les postbacks Ajax :
package shop.web;
import java.io.Serializable;
import java.util.List;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named
@ViewScoped // jakarta.faces.view.ViewScoped, not the removed jakarta.faces.bean one
public class OrderSearch implements Serializable {
@Inject
OrderService orderService; // a CDI or enterprise bean; transactions live there, not here
private String customer;
private OrderStatus status;
private List<Order> results = List.of();
public void search() { // no return value: stay on the same view
results = orderService.find(customer, status);
}
public OrderStatus[] getStatuses() { return OrderStatus.values(); }
// getters and setters for customer, status, results
}
Remarquez ce que le bean ne fait pas : il n'ouvre pas de transaction, ne construit pas de SQL et ne détient pas l'EntityManager. Cantonnez le backing bean au rôle de modèle de vue et déléguez à un service ; le guide des session beans montre à quoi ressemble ce service.
Choisir un scope
| Scope | Durée de vie | Usage typique | Point d'attention |
|---|---|---|---|
@RequestScoped | Une requête | Pages sans état, formulaires simples sans Ajax | Les valeurs sont perdues entre deux postbacks ; le rendu conditionnel basé sur le bean ne fonctionne plus |
@ViewScoped | Une vue, à travers les postbacks vers la même page | La plupart des pages avec formulaires, tableaux et Ajax | Doit être Serializable ; état stocké par vue |
@FlowScoped("checkout") | Un flow multipage avec une entrée et une sortie définies | Assistants (wizards) | Nécessite une définition de flow |
@SessionScoped | La session HTTP | Utilisateur connecté, préférences | Fait grossir la mémoire ; jamais pour les données de formulaire propres à une page |
@ApplicationScoped | L'application | Données de référence, caches | Doit être thread-safe |
@ClientWindowScoped (4.0) | Un onglet ou une fenêtre de navigateur | État propre à un onglet | Dépend du mode client-window |
Les beans à scope vue et session sont passivation-capable : le conteneur peut les sérialiser sur disque ou les répliquer sur un autre nœud, donc chaque champ doit être sérialisable ou transient. Un proxy d'enterprise bean injecté ne pose pas de problème ; un EntityManager injecté, si.
Facelets : templates, inclusions et composants composites
Les pages Facelets se composent. Un template déclare des points d'insertion avec <ui:insert name="content" /> ; une page utilise <ui:composition template="…"> et les remplit avec <ui:define>. <ui:include src="…"> intègre des fragments, <ui:param> leur transmet des valeurs, et <ui:repeat> itère sans générer de tableau. Tout ce qui se trouve hors de <ui:composition> dans une page qui utilise un template est ignoré, ce qui surprend ceux qui ajoutent du balisage en haut du fichier.
Les composants composites permettent d'empaqueter un fragment de balisage avec sa propre interface dans une bibliothèque de ressources : <shop:address value="#{customer.address}" /> devient ainsi un composant réutilisable sans écrire de Java. Pour tout ce qui est plus complexe (rendu personnalisé, comportement JavaScript), les bibliothèques de composants comme PrimeFaces ont en général déjà fait le travail.
Deux détails de l'EL font gagner des heures. #{…} est une expression différée, que Faces évalue pendant le cycle de vie et dans laquelle il peut aussi écrire ; ${…} est évaluée immédiatement lors de la construction de la page et est en lecture seule. Dans Facelets, utilisez #{…} partout. Ensuite, les getters peuvent être appelés plusieurs fois par requête, une fois par phase qui a besoin de la valeur : un getter doit donc être peu coûteux et sans effet de bord. Chargez les données dans une méthode d'action ou une méthode @PostConstruct et mettez-les en cache dans un champ.
Conversion, validation et messages
Chaque saisie passe par la conversion (de la chaîne vers le type) et la validation (règles portant sur le type) avant d'atteindre le modèle. Il existe des convertisseurs standard pour les nombres et les dates (<f:convertNumber>, <f:convertDateTime> avec prise en charge de java.time depuis la 2.3), et Faces résout automatiquement les convertisseurs pour les types primitifs, les enums et BigDecimal. Pour vos propres types, implémentez Converter<T> et enregistrez-le avec @FacesConverter(forClass = Money.class) ; depuis Faces 2.3, convertisseurs et validateurs peuvent être des beans CDI grâce à @FacesConverter(managed = true), ce qui leur permet d'injecter des services.
La validation comporte trois niveaux, appliqués dans cet ordre :
required="true": vérifié avant la conversion ; les chaînes vides ne sont traitées comme null par défaut que sijakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULLest positionné, ce que devrait faire presque toute application.- Les validateurs Faces :
<f:validateLength>,<f:validateLongRange>,<f:validateRegex>,<f:validateBean>et les classes@FacesValidatorpersonnalisées. - Bean Validation : les contraintes comme
@NotBlankou@Size(max = 100)sur la propriété du bean sont validées automatiquement dès qu'un fournisseur Bean Validation est présent, ce qui est toujours le cas sur un serveur Jakarta EE. Les règles restent ainsi à un seul endroit, partagées avec votre couche REST et vos entités JPA.
Les échecs deviennent des objets FacesMessage rattachés à un identifiant de composant ; <h:message for="customer" /> en affiche un, <h:messages /> les affiche tous. Le code applicatif peut ajouter ses propres messages avec FacesContext.getCurrentInstance().addMessage(...), et les messages survivent à une redirection si vous appelez context.getExternalContext().getFlash().setKeepMessages(true).
Navigation et pattern Post-Redirect-Get
Depuis la 2.0, la navigation est implicite : une méthode d'action qui renvoie "orders" rend orders.xhtml ; renvoyer null ou void rend de nouveau la vue courante. Après un simple postback, en revanche, la page rendue et l'URL du navigateur divergent : l'URL affiche toujours la page qui a été postée, et un rafraîchissement du navigateur soumet à nouveau le formulaire. Ajoutez ?faces-redirect=true à l'outcome (return "orders?faces-redirect=true";) pour envoyer une redirection et obtenir un GET propre ; ajoutez &includeViewParams=true pour propager les valeurs de <f:viewParam>. Les blocs <navigation-rule> explicites dans faces-config.xml sont toujours pris en charge, et vous en trouverez dans le code de l'ère JSF 1.x, mais le nouveau code en a rarement besoin.
Pour les pages que l'on peut mettre en favori et qui chargent leurs données depuis l'URL, <f:metadata><f:viewParam name="id" value="#{orderView.id}" /><f:viewAction action="#{orderView.load}" /></f:metadata> lie un paramètre de requête et exécute une méthode lors du GET initial : c'est l'équivalent Faces d'une méthode de contrôleur GET.
De l'Ajax sans JavaScript
<f:ajax> imbriqué dans un composant transforme son événement par défaut (clic pour les boutons, changement pour les saisies) en postback partiel. execute désigne les composants dont les valeurs sont soumises et traitées (@this, @form, @all, ou des identifiants), et render ceux qui sont rendus de nouveau à partir de la réponse. Le cycle de vie complet s'exécute toujours côté serveur pour le sous-arbre concerné : validation, mise à jour du modèle et méthode d'action se comportent exactement comme lors d'un postback complet ; seuls le transport et la mise à jour du DOM diffèrent. Les identifiants de render doivent se résoudre par rapport au naming container (un composant situé dans h:form id="search" a pour client id search:customer) ; un identifiant inexistant échoue au rendu avec un message clair, alors qu'un identifiant existant mais non rendu (parce que rendered="false") ne met silencieusement rien à jour. Enveloppez les zones rendues conditionnellement dans un <h:panelGroup id="wrapper"> toujours rendu, et ciblez celui-ci.
Pourquoi Faces est legacy, et ce qui plaide encore pour lui
Le modèle a des coûts bien réels. L'état par vue stocké en session limite le scaling horizontal, sauf à recourir à la sauvegarde d'état côté client ou aux sticky sessions. Chaque interaction est un aller-retour vers le serveur ; un comportement client riche implique de lutter contre le framework ou de s'appuyer sur le JavaScript d'une bibliothèque de composants. Tester une page Faces exige un test d'intégration avec un navigateur ou un conteneur. Et le marché de l'emploi front-end est passé aux frameworks TypeScript : recruter sur Faces devient plus difficile chaque année.
Les arguments pour le conserver, voire parfois le choisir : des équipes entièrement Java, des applications internes où une bibliothèque de composants couvre tous les widgets nécessaires, aucun contrat d'API séparé à maintenir, et une sécurité gérée par le serveur plutôt que par un flux de jetons. Si vous construisez un nouveau produit exposé au public, utilisez un framework JavaScript branché sur des endpoints Jakarta REST. Si vous maintenez une application Faces, gardez-la sur une implémentation supportée (Faces 4.x avec PrimeFaces 13+), migrez vers CDI les classes @ManagedBean restantes, et résistez à la tentation d'ajouter du JavaScript qui contourne l'arbre de composants.
Erreurs fréquentes
- Bean
@ViewScopedqui n'est pasSerializable. Fonctionne sur un nœud, échoue avecNotSerializableExceptiondès qu'il y a réplication de session ou passivation. ImplémentezSerializableet marqueztransientles champs non sérialisables. - Faire du travail dans les getters. Un getter qui interroge la base s'exécute plusieurs fois par requête, et encore à chaque appel Ajax. Chargez dans une action ou dans
@PostConstruct. - Condition
renderedsur un bean à scope requête. Pendant Apply Request Values, le bean est une instance neuve, la condition vaut false, et l'action du bouton ne se déclenche jamais. Utilisez@ViewScoped. - Composant de commande hors d'un
<h:form>. Rien n'est soumis ; le stade Development le signale. - Formulaires imbriqués. HTML invalide ; le navigateur soumet le mauvais. Un formulaire par soumission logique, plusieurs formulaires par page.
- Mauvaise cible
renderen Ajax. Rendez un conteneur toujours présent, pas le composant rendu conditionnellement lui-même. - Stocker l'état de la page en session. Cela entraîne des données périmées d'un onglet à l'autre et une consommation mémoire qui grimpe. Utilisez le scope vue pour l'état de la page et le scope session uniquement pour l'utilisateur.
- Oublier
faces-redirect=true. L'URL a une page de retard et un rafraîchissement soumet à nouveau le formulaire.
À retenir
- Faces conserve un arbre de composants côté serveur et exécute un cycle de vie en six phases à chaque postback ; un échec de conversion ou de validation saute directement au rendu sans toucher au modèle.
- Les pages Facelets se lient aux beans CDI via des expressions EL différées
#{…}; Faces 4.0 a supprimé l'ancien modèle@ManagedBeanet les vues JSP. - Utilisez
@ViewScopedpour l'état de la page et rendez ces beansSerializable; laissez transactions et persistance dans la couche service. - Les contraintes Bean Validation posées sur les propriétés du bean sont validées automatiquement, et
<h:messages>rend les échecs visibles. - Renvoyez
outcome?faces-redirect=truedepuis les actions pour obtenir le comportement Post-Redirect-Get ; utilisez<f:viewParam>et<f:viewAction>pour les pages que l'on peut mettre en favori. <f:ajax>exécute le même cycle de vie sur un sous-arbre et rend de nouveau les composants désignés ; ciblez des identifiants qui existent toujours.- Faces est legacy pour les nouveaux front-ends, mais il est activement maintenu et largement déployé : maîtrisez le cycle de vie et vous saurez bien le prendre en charge.
Sources et lectures complémentaires
Questions fréquentes
- JSF est-il mort ? Faut-il l'apprendre en 2026 ?
- C'est une technologie legacy pour les nouvelles interfaces (la plupart des projets greenfield utilisent un framework JavaScript qui dialogue avec des endpoints REST), mais elle est loin d'être morte. Jakarta Faces 4.0 est sorti en 2022, PrimeFaces et MyFaces sont activement maintenus, et un vaste parc d'applications métier internes tourne dessus. Si vous travaillez sur du Java d'entreprise, vous la croiserez, et comprendre son cycle de vie rend la maintenance nettement moins pénible.
- Qu'est-ce qui remplace @ManagedBean dans Faces 4.0 ?
- CDI. Annotez la classe avec @Named pour la rendre accessible depuis l'EL, et donnez-lui un scope CDI : @RequestScoped, @ViewScoped (du package jakarta.faces.view), @SessionScoped, @ApplicationScoped, @FlowScoped ou @ConversationScoped. Le package jakarta.faces.bean et ses scopes ont été entièrement supprimés en 4.0, tout comme la prise en charge de JSP comme technologie de vue.
- Pourquoi mon bouton de commande n'appelle-t-il pas la méthode d'action ?
- Les causes habituelles, par ordre de fréquence : le bouton n'est pas dans un h:form ; une erreur de conversion ou de validation quelque part dans le formulaire a silencieusement arrêté le cycle de vie à Process Validations (ajoutez un composant h:messages pour la voir) ; l'attribut rendered du bouton ou d'un parent a été évalué à false pendant Apply Request Values parce que sa condition repose sur un bean à scope requête ; ou des formulaires imbriqués.
- Sauvegarde d'état côté serveur ou côté client ?
- Côté serveur (le défaut), l'arbre de composants est conservé en session et seul un jeton part vers le navigateur : c'est plus sûr et plus léger pour le réseau, mais cela consomme de la mémoire serveur par vue. Côté client, l'arbre est sérialisé et chiffré dans un champ caché, ce qui permet de scaler horizontalement sans sticky sessions au prix de pages plus lourdes. La plupart des applications gardent le défaut et plafonnent le nombre de vues par session.