Java EE / Jakarta EE

Jakarta Faces (JSF) 4: componentes, ciclo de vida y backing beans CDI

Intermedio12 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

Jakarta Faces (JavaServer Faces hasta el cambio de nombre a Jakarta) es el framework web basado en componentes de la plataforma, y buena parte del software empresarial sigue dependiendo de él. Esta guía explica el ciclo de vida de una petición en seis fases, cómo las páginas Facelets se enlazan con backing beans CDI mediante el Expression Language, cómo funcionan la conversión, la validación, la navegación y Ajax, y qué eliminó Faces 4.0, para que puedas mantener con confianza aplicaciones Faces existentes y decidir cuándo sigue siendo la herramienta adecuada.

Lo que aprenderás

  • Explicar el ciclo de vida de una petición Faces y la diferencia entre una petición inicial y un postback
  • Construir una página con plantillas Facelets, componentes estándar y un backing bean CDI
  • Elegir el scope de bean adecuado y entender por qué los beans @ViewScoped deben ser serializables
  • Aplicar converters, validadores, Bean Validation y actualizaciones parciales con Ajax
  • Identificar qué cambió en Faces 4.0 y por qué Faces se considera legacy para nuevos front-ends

Jakarta Faces plantea las interfaces web de un modo muy distinto a cualquier cosa construida con un framework JavaScript: el servidor mantiene un árbol de componentes por cada página, el navegador envía los formularios de vuelta a ese árbol y el framework convierte, valida y aplica los valores a beans Java antes de renderizar la siguiente vista. Si vienes de Spring MVC o de una stack REST con React, ese modelo con estado explica tanto por qué Faces era tan productivo para equipos solo Java (sin JavaScript, sin contratos JSON, con binding tipado a tu dominio) como por qué cayó en desgracia cuando las single-page applications y el escalado horizontal se convirtieron en la norma. Muchísimo software de negocio sigue funcionando sobre él, así que el objetivo aquí es la soltura: saber qué pasa en cada petición para poder depurarla.

Dónde encaja Faces y qué es hoy

Faces es un servlet. El FacesServlet se mapea automáticamente a *.xhtml (y a /faces/*, *.faces y *.jsf para enlaces antiguos) en cuanto el contenedor detecta que se usa Faces, de modo que una petición a /shop/orders.xhtml pasa por el contenedor de servlets descrito en la guía de servlets y termina en Faces. Las páginas se escriben en Facelets, un dialecto de XHTML; los backing beans son beans CDI normales; el Expression Language (EL) conecta ambos.

El historial de versiones importa, porque la API que ves en una base de código delata su antigüedad:

VersiónAñoNovedades destacadas
JSF 1.0 / 1.22004 / 2006Vistas JSP, faces-config.xml para todo
JSF 2.02009Facelets como tecnología de vista por defecto, anotaciones, navegación implícita, Ajax integrado, view scope
JSF 2.22013Atributos pass-through pensados para HTML5, Faces Flows, subida de ficheros, vistas stateless
JSF 2.32017Integración completa con CDI (@Inject FacesContext), <f:websocket>, soporte de fechas y horas de java.time
Jakarta Faces 3.02020Cambio de namespace a jakarta.faces, sin cambios funcionales
Jakarta Faces 4.02022Eliminados @ManagedBean y los scopes de jakarta.faces.bean, eliminado el soporte de vistas JSP, namespaces XML cortos (jakarta.faces.html), mapeo automático sin extensión, @ClientWindowScoped

Existen dos implementaciones: Eclipse Mojarra (la implementación compatible, incluida en GlassFish, WildFly y Payara) y Apache MyFaces (incluida en Open Liberty y TomEE). Las librerías de componentes (con PrimeFaces a la cabeza) aportan los widgets avanzados que la mayoría de las aplicaciones usan de verdad, y OmniFaces cubre los huecos con sus utilidades. Tomcat no incluye Faces: añades tú mismo la implementación y un contenedor CDI, un motivo más para preferir un servidor Web Profile cuando trabajas con Faces.

El ciclo de vida de la petición

Entender las seis fases es lo más útil que puedes aprender sobre Faces, porque casi cualquier pregunta del tipo «no funciona» es en realidad «qué fase se detuvo y por qué».

  1. Restore View: en un postback se restaura el árbol de componentes guardado de la página (desde la sesión por defecto); en un GET inicial se construye un árbol nuevo y el ciclo de vida salta directamente a Render Response.
  2. Apply Request Values: cada componente de entrada lee de los parámetros de la petición la cadena en bruto que se envió. Los componentes cuyo atributo rendered es false se omiten, y los componentes de acción encolan sus eventos en esta fase.
  3. Process Validations: las cadenas enviadas se convierten al tipo de destino y luego se validan. Cualquier fallo añade un FacesMessage, marca el componente como inválido y hace que el ciclo de vida salte a Render Response: el modelo nunca se toca y no se ejecuta ningún método de acción.
  4. Update Model Values: los valores convertidos y válidos se escriben en las propiedades del bean referenciadas por value="#{bean.property}".
  5. Invoke Application: se ejecutan los métodos de acción y los action listeners; el valor que devuelve la acción decide la navegación.
  6. Render Response: la vista se renderiza a HTML y su estado se guarda para el siguiente postback.

Poner immediate="true" en una entrada o en un comando traslada su procesamiento a la fase Apply Request Values, que es como los botones «Cancelar» se saltan la validación del resto del formulario. Añadir <h:messages globalOnly="false" /> a una página mientras desarrollas muestra los fallos de conversión y validación que, de otro modo, serían invisibles.

Una página y su backing bean

Un formulario típico: una página de búsqueda de pedidos con un filtro, una tabla de resultados y un botón.

<!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>

El backing bean es un bean CDI. @Named lo expone a EL con el nombre orderSearch (el nombre de la clase con la inicial en minúscula) y @ViewScoped lo mantiene vivo mientras el usuario permanezca en esta vista, también a través de los 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
}

Fíjate en lo que el bean no hace: no abre transacciones, no construye SQL y no guarda el EntityManager. Mantén el backing bean como modelo de vista y delega en un servicio; la guía de session beans muestra cómo es ese servicio.

Elegir un scope

ScopeVive duranteUso típicoAdvertencia
@RequestScopedUna peticiónPáginas sin estado, formularios sencillos sin AjaxLos valores se pierden entre postbacks; el renderizado condicional basado en el bean se rompe
@ViewScopedUna vista, a través de los postbacks a la misma páginaLa mayoría de las páginas con formularios, tablas y AjaxDebe ser Serializable; el estado se guarda por vista
@FlowScoped("checkout")Un flujo de varias páginas con entrada y salida definidasAsistentes (wizards)Requiere una definición de flujo
@SessionScopedLa sesión HTTPUsuario autenticado, preferenciasHace crecer la memoria; nunca para datos de formulario propios de una página
@ApplicationScopedLa aplicaciónDatos de referencia, cachésDebe ser thread-safe
@ClientWindowScoped (4.0)Una pestaña o ventana del navegadorEstado propio de una pestañaDepende del modo client-window

Los beans con scope de vista y de sesión son passivation-capable: el contenedor puede serializarlos a disco o replicarlos en otro nodo, así que cada campo debe ser serializable o transient. Un proxy de enterprise bean inyectado no da problemas; un EntityManager inyectado, sí.

Facelets: plantillas, inclusiones y componentes compuestos

Las páginas Facelets se componen. Una plantilla declara puntos de inserción con <ui:insert name="content" />; una página usa <ui:composition template="…"> y los rellena con <ui:define>. <ui:include src="…"> incorpora fragmentos, <ui:param> les pasa valores y <ui:repeat> itera sin generar una tabla. Todo lo que quede fuera de <ui:composition> en una página que usa una plantilla se ignora, algo que sorprende a quien añade marcado al principio del fichero.

Los componentes compuestos permiten empaquetar un fragmento de marcado con su propia interfaz dentro de una librería de recursos, de modo que <shop:address value="#{customer.address}" /> se convierte en un componente reutilizable sin escribir Java. Para cualquier cosa más compleja (renderizado personalizado, comportamiento en JavaScript), las librerías de componentes como PrimeFaces normalmente ya han hecho el trabajo.

Dos detalles de EL ahorran horas. #{…} es una expresión diferida que Faces evalúa durante el ciclo de vida y en la que también puede escribir; ${…} se evalúa inmediatamente al construir la página y es de solo lectura. En Facelets, usa #{…} en todas partes. Además, los getters pueden llamarse varias veces por petición, una por cada fase que necesita el valor, así que un getter debe ser barato y no tener efectos secundarios: carga los datos en un método de acción o en un método @PostConstruct y guárdalos en un campo.

Conversión, validación y mensajes

Toda entrada pasa por la conversión (de cadena a tipo) y la validación (reglas sobre el tipo) antes de llegar al modelo. Hay converters estándar para números y fechas (<f:convertNumber>, <f:convertDateTime> con soporte de java.time desde la 2.3), y Faces resuelve automáticamente los converters para tipos primitivos, enums y BigDecimal. Para tus propios tipos, implementa Converter<T> y regístralo con @FacesConverter(forClass = Money.class); desde Faces 2.3, converters y validadores pueden ser beans CDI con @FacesConverter(managed = true), de modo que pueden inyectar servicios.

La validación tiene tres capas, que se aplican en este orden:

  1. required="true": se comprueba antes de la conversión; las cadenas vacías solo se tratan como null por defecto si está activado jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL, algo que casi cualquier aplicación debería hacer.
  2. Validadores de Faces: <f:validateLength>, <f:validateLongRange>, <f:validateRegex>, <f:validateBean> y clases @FacesValidator propias.
  3. Bean Validation: las restricciones como @NotBlank o @Size(max = 100) sobre la propiedad del bean se validan automáticamente cuando hay un proveedor de Bean Validation, que en un servidor Jakarta EE siempre lo hay. Así las reglas viven en un solo sitio, compartido con tu capa REST y tus entidades JPA.

Los fallos se convierten en objetos FacesMessage asociados a un id de componente; <h:message for="customer" /> muestra uno y <h:messages /> los muestra todos. El código de la aplicación puede añadir los suyos con FacesContext.getCurrentInstance().addMessage(...), y los mensajes sobreviven a una redirección si llamas a context.getExternalContext().getFlash().setKeepMessages(true).

Navegación y el patrón Post-Redirect-Get

Desde la 2.0 la navegación es implícita: un método de acción que devuelve "orders" renderiza orders.xhtml; devolver null o void vuelve a renderizar la vista actual. Sin embargo, tras un postback simple, la página renderizada y la URL del navegador divergen: la URL sigue mostrando la página que se envió y recargar el navegador reenvía el formulario. Añade ?faces-redirect=true al outcome (return "orders?faces-redirect=true";) para enviar una redirección y obtener un GET limpio; añade &includeViewParams=true para arrastrar los valores de <f:viewParam>. Los bloques <navigation-rule> explícitos en faces-config.xml se siguen admitiendo, y los encontrarás en código de la época de JSF 1.x, pero el código nuevo rara vez los necesita.

Para páginas que se pueden guardar en marcadores y cargan datos desde la URL, <f:metadata><f:viewParam name="id" value="#{orderView.id}" /><f:viewAction action="#{orderView.load}" /></f:metadata> enlaza un parámetro de consulta y ejecuta un método en el GET inicial: el equivalente en Faces de un método de controlador GET.

Ajax sin JavaScript

<f:ajax> anidado en un componente convierte su evento por defecto (clic en los botones, cambio en las entradas) en un postback parcial. execute indica los componentes cuyos valores se envían y procesan (@this, @form, @all o ids), y render los que se vuelven a renderizar a partir de la respuesta. El ciclo de vida completo sigue ejecutándose en el servidor para el subárbol ejecutado, así que la validación, la actualización del modelo y el método de acción se comportan exactamente igual que en un postback completo; solo cambian el transporte y la actualización del DOM. Los ids de render deben resolverse respecto al naming container (un componente dentro de h:form id="search" tiene el client id search:customer), y un id que no existe falla al renderizar con un mensaje claro, mientras que un id que existe pero no se renderiza (porque rendered="false") no actualiza nada y no avisa de ello. Envuelve las zonas que se renderizan de forma condicional en un <h:panelGroup id="wrapper"> que se renderice siempre, y apunta a ese.

Por qué Faces es legacy y qué sigue jugando a su favor

El modelo tiene costes reales. El estado por vista en la sesión limita el escalado horizontal, salvo que uses el guardado de estado en el cliente o sticky sessions. Cada interacción es un viaje de ida y vuelta al servidor; un comportamiento rico en el cliente implica pelearse con el framework o apoyarse en el JavaScript de una librería de componentes. Probar una página Faces exige un test de integración con un navegador o un contenedor. Y el mercado laboral de front-end se ha movido a frameworks TypeScript, así que contratar para Faces es más difícil cada año.

Lo que justifica mantenerlo (o, a veces, elegirlo): equipos íntegramente Java, aplicaciones internas donde una librería de componentes cubre todos los widgets necesarios, ningún contrato de API aparte que mantener y seguridad gestionada por el servidor en lugar de por un flujo de tokens. Si estás construyendo un producto nuevo de cara al público, usa un framework JavaScript contra endpoints de Jakarta REST. Si mantienes una aplicación Faces, mantenla en una implementación con soporte (Faces 4.x con PrimeFaces 13+), migra a CDI las clases @ManagedBean que queden y resiste la tentación de añadir JavaScript que se salte el árbol de componentes.

Errores habituales

  • Bean @ViewScoped que no es Serializable. Funciona en un nodo y falla con NotSerializableException al replicar la sesión o al pasivarla. Implementa Serializable y marca como transient los campos no serializables.
  • Hacer trabajo en los getters. Un getter que consulta la base de datos se ejecuta varias veces por petición, y de nuevo en cada llamada Ajax. Carga los datos en una acción o en @PostConstruct.
  • Condición rendered sobre un bean con scope de petición. Durante Apply Request Values el bean es una instancia nueva, la condición vale false y la acción del botón nunca se dispara. Usa @ViewScoped.
  • Componente de comando fuera de <h:form>. No se envía nada; la etapa Development avisa de ello.
  • Formularios anidados. HTML inválido; el navegador envía el que no es. Un formulario por envío lógico, varios formularios por página.
  • Destino render de Ajax equivocado. Renderiza un contenedor siempre presente, no el componente que se renderiza de forma condicional.
  • Guardar el estado de la página en la sesión. Provoca datos obsoletos entre pestañas y crecimiento de la memoria. Usa el scope de vista para el estado de la página y el de sesión solo para el usuario.
  • Olvidar faces-redirect=true. La URL va una página por detrás y recargar reenvía el formulario.

Puntos clave

  • Faces mantiene un árbol de componentes en el servidor y ejecuta un ciclo de vida de seis fases en cada postback; los fallos de conversión o validación saltan directamente al renderizado sin tocar el modelo.
  • Las páginas Facelets se enlazan con beans CDI mediante expresiones EL diferidas #{…}; Faces 4.0 eliminó el antiguo modelo @ManagedBean y las vistas JSP.
  • Usa @ViewScoped para el estado de la página y haz esos beans Serializable; deja las transacciones y la persistencia en la capa de servicio.
  • Las restricciones de Bean Validation sobre las propiedades del bean se validan automáticamente, y <h:messages> hace visibles los fallos.
  • Devuelve outcome?faces-redirect=true desde las acciones para conseguir el comportamiento Post-Redirect-Get; usa <f:viewParam> y <f:viewAction> para páginas que se puedan guardar en marcadores.
  • <f:ajax> ejecuta el mismo ciclo de vida sobre un subárbol y vuelve a renderizar los componentes indicados; apunta a ids que existan siempre.
  • Faces es legacy para nuevos front-ends, pero se mantiene activamente y está muy desplegado: domina el ciclo de vida y podrás darle un buen soporte.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Está muerto JSF? ¿Vale la pena aprenderlo en 2026?
Es legacy para interfaces nuevas (la mayoría de los proyectos greenfield usan un framework JavaScript que habla con endpoints REST), pero está lejos de estar muerto. Jakarta Faces 4.0 salió en 2022, PrimeFaces y MyFaces se mantienen activamente y una gran base instalada de aplicaciones de negocio internas funciona sobre él. Si trabajas con Java empresarial te lo vas a encontrar, y entender su ciclo de vida hace el mantenimiento mucho menos doloroso.
¿Qué sustituye a @ManagedBean en Faces 4.0?
CDI. Anota la clase con @Named para que sea accesible desde EL y dale un scope CDI: @RequestScoped, @ViewScoped (de jakarta.faces.view), @SessionScoped, @ApplicationScoped, @FlowScoped o @ConversationScoped. El paquete jakarta.faces.bean y sus scopes se eliminaron por completo en la 4.0, junto con el soporte de JSP como tecnología de vista.
¿Por qué mi botón de comando no llama al método de acción?
Las causas habituales, por orden de frecuencia: el botón no está dentro de un h:form; un error de conversión o validación en algún punto del formulario detuvo en silencio el ciclo de vida en Process Validations (añade un componente h:messages para verlo); el atributo rendered del botón o de un padre se evaluó a false durante Apply Request Values porque su condición depende de un bean con scope de petición; o hay formularios anidados.
¿Guardar el estado en el servidor o en el cliente?
En el servidor (la opción por defecto), el árbol de componentes se guarda en la sesión y al navegador solo se envía un token: es más seguro y más ligero para la red, pero consume memoria del servidor por cada vista. En el cliente, el árbol se serializa cifrado en un campo oculto, lo que permite escalar horizontalmente sin sticky sessions a costa de páginas más pesadas. La mayoría de las aplicaciones mantienen el valor por defecto y limitan el número de vistas por sesión.