Migrating to Jakarta Faces

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Much of the Faces code in production was written for JSF 2.x on Java EE 7 or 8, and some of it for JSF 1.x with JSP views. None of it compiles or even starts on a Jakarta EE 10 or 11 runtime without changes, and some of the changes do not fail at all: they are silently ignored and the application misbehaves. This page is the field guide for that move. It follows one small application, the Bookshelf catalogue written the way a JSF 2.3 project of 2018 would write it, through every change needed to run it on Jakarta Faces 4.1, and it records what each tool does and what it leaves for you.

The page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, Jakarta EE 11, Java 17 or later), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with PrimeFaces 16.0.0 and OmniFaces 5.5.5 where libraries are involved. Faces 4.0 (Jakarta EE 10, Java 11 or later) is named where it differs. Faces 5.0 (Jakarta EE 12) is still a milestone (5.0.0-M9 is the latest on 2026-10-08), so everything said about it is "upcoming". Every experiment was run on 2026-10-08: the application was built with Maven 3.9.16 for Java 21 (maven.compiler.release 21), deployed to an embedded Apache Tomcat 11.0.26 with Weld 6.0.4.Final on JDK 26, and queried with curl and a small script that posts Ajax requests. Every runtime test was repeated on Mojarra and on MyFaces. A claim that was not run is labelled "documented" or "not run".

Two other pages hold the pieces this page does not repeat. The Versions and Implementations page has the version matrix, the Java baselines and the per-release feature lists, and the Backing Beans and CDI page explains the CDI model that replaces managed beans in detail. This page is about the move: the order of the steps, the tools, the symptoms of each missed step, and the replacements.

The Route: Which Releases to Cross

A Faces application does not jump between "JSF" and "Jakarta Faces". It crosses up to four releases, each of which broke something different. The list below is the order in which the breaks happened, and it is also the order in which a careful migration removes them.

Table 1. What each release broke, and the minimum Java it needs
Faces release Platform Java SE What breaks when you cross it

2.3

Java EE 8

8

Nothing. It is the best place to start: Facelets, CDI and javax still work together, and @ManagedBean is deprecated but present.

3.0

Jakarta EE 9 (9.1 for Java 11)

8

Everything named javax.faces, javax.servlet, javax.el, javax.enterprise, javax.inject becomes jakarta.*. No other change: the specification says only that the namespace changed and that this is a breaking change.

4.0

Jakarta EE 10

11

JSP support, @ManagedBean and the javax.faces.bean scopes, MethodBinding, ValueBinding and the jakarta.faces.el package, the CURRENT_COMPONENT constants, the deprecated StateManager methods and ResourceResolver are removed. The XML namespaces move to the jakarta.faces.* URNs, and jsf.js becomes faces.js.

4.1

Jakarta EE 11

17

Nothing is removed that Faces 4.0 still had. More API is deprecated for removal (full state saving, ActionSource2, the custom-scope events, cc:extension) and the UIComponent.bindings field is gone. Java 17 is required.

5.0 (upcoming)

Jakarta EE 12 (planned)

21

Planned: removal of everything deprecated for removal since 4.0, of the old namespaces, and API modernisation. Not released.

The practical consequence is a staged route. Stage 1 is done while everything is still javax, because it needs no rename and can be tested on the platform you already run. Stages 2 to 4 are mechanical or nearly so, and the fewer problems you carry into each stage, the easier it is to tell a rename error from a design problem.

flowchart LR A["JSF 1.x / 2.x on Java EE 7-8
javax.faces, JSP or Facelets"] --> B["Stage 1: still javax, Faces 2.3
Facelets only, CDI beans,
no javax.faces.el code"] B --> C["Stage 2: Faces 3.0, Jakarta EE 9
javax to jakarta,
libraries with jakarta artifacts"] C --> D["Stage 3: Faces 4.0, Jakarta EE 10
URNs, removed APIs,
faces.js, descriptors"] D --> E["Stage 4: Faces 4.1, Jakarta EE 11
Java 17, deprecations,
PrimeFaces 16, OmniFaces 5"] E -.-> F["Later: Faces 5.0, Jakarta EE 12
Java 21, removals"]

The stages are advice, not a rule of the platform. The code can jump from 2.3 straight to 4.1 in one commit, and the Bookshelf below does exactly that in its later sections. The staging is about where you get feedback: stage 1 can be verified on the old server, and each later stage fails in a recognisable way (see What Happens If You Only Upgrade the Runtime).

Take an Inventory First

Before any tool runs, count the work. The script below walks a source tree and counts the files in each category that this page treats. It uses only grep, so it runs anywhere, and it is the check to repeat at the end: every line must read zero.

inventory.sh: files per category that still need a change
#!/bin/sh
# inventory.sh [project-directory]
cd "${1:-.}" || exit 1
count() { printf '%-36s %s\n' "$1" "$(grep -rEl "$2" $3 2>/dev/null | wc -l | tr -d ' ')"; }

count "javax.* imports (Java)"            '^import javax\.(faces|servlet|el|enterprise|inject|validation|persistence|ejb|websocket)\.|^import javax\.annotation\.(PostConstruct|PreDestroy|Resource)' src/main/java
count "faces.bean managed beans"          '(javax|jakarta)\.faces\.bean'               src/main/java
count "faces.el removed expression API"   '(javax|jakarta)\.faces\.el\.'               src/main/java
count "javax.faces.* names (descriptors)" 'javax\.faces\.[A-Za-z_]+'                   src/main/webapp/WEB-INF
count "managed-bean in faces-config"      '<managed-bean>'                             src/main/webapp/WEB-INF
count "variable/property resolvers"       '<(variable|property)-resolver>'             src/main/webapp/WEB-INF
count "old Facelets namespaces"           'xmlns(:[a-z]+)?="http://(java\.sun\.com|xmlns\.jcp\.org)/jsf' src/main/webapp
count "old JSTL namespaces"               'http://(java\.sun\.com|xmlns\.jcp\.org)/jsp/jstl' src/main/webapp
count "JSP views"                         '<%@'                                        src/main/webapp
count "jsf.js and jsf.ajax"               'jsf\.(js|ajax)'                             src/main/webapp
count "old descriptor namespaces"         'xmlns\.jcp\.org/xml/ns/javaee|java\.sun\.com/xml/ns/javaee' src/main/webapp/WEB-INF

Run against the legacy Bookshelf of the next section it printed 5, 2, 2, 1, 1, 1, 2, 1, 1, 1 and 3 in that order, and against the migrated application at the end of this page it printed zero in every line. The two lines about faces.bean and faces.el search for the jakarta spelling too, on purpose: a mechanical rename turns javax.faces.bean into jakarta.faces.bean, which is as dead as the original (What Happens If You Only Upgrade the Runtime).

The Legacy Bookshelf

The application to migrate is the Bookshelf catalogue as a JSF 2.3 project would have built it: a WAR for Java EE 8, javax everywhere, two library dependencies, a managed bean with an injected service, an old-style variable resolver, a phase listener, an Ajax button written with the old script name, a JSP page next to the Facelets pages, and the old namespaces. It compiles against javax:javaee-web-api:8.0.1 and builds a WAR; no Java EE 8 server was used in these experiments, because the subject is the move, not the old runtime. It is deliberately dense in legacy idioms, one of each kind this page treats, so that every step has something to change. The packages follow the conventions of the section (org.example.bookshelf.model, .service and .web, bundle variable msg).

pom.xml of the legacy application (the parts that matter)
<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>

<dependencies>
  <dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-web-api</artifactId>
    <version>8.0.1</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.primefaces</groupId>
    <artifactId>primefaces</artifactId>
    <version>11.0.0</version>
  </dependency>
  <dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>3.14.4</version>
  </dependency>
</dependencies>
src/main/java/org/example/bookshelf/service/BookService.java (legacy)
package org.example.bookshelf.service;

import java.util.List;
import javax.faces.bean.ApplicationScoped;
import javax.faces.bean.ManagedBean;
import org.example.bookshelf.model.Book;

@ManagedBean(name = "bookService", eager = true)
@ApplicationScoped
public class BookService {

    private final List<Book> books = List.of(
            new Book("Dune", "Frank Herbert"),
            new Book("Neuromancer", "William Gibson"),
            new Book("Snow Crash", "Neal Stephenson"));

    public List<Book> findAll() {
        return books;
    }
}

Source: Jakarta EE Tutorial: Managed Beans in Jakarta Faces Technology (the Jakarta EE 10 tutorial, which still describes the removed form)

src/main/java/org/example/bookshelf/web/CatalogueBean.java (legacy)
package org.example.bookshelf.web;

import java.io.Serializable;
import java.util.List;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.ViewScoped;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@ManagedBean(name = "catalogue")
@ViewScoped
public class CatalogueBean implements Serializable {

    @ManagedProperty(value = "#{bookService}")
    private BookService service;

    private String query = "";

    public void setService(BookService service) {
        this.service = service;
    }

    public String getQuery() {
        return query;
    }

    public void setQuery(String query) {
        this.query = query;
    }

    public List<Book> getBooks() {
        return service.findAll().stream()
                .filter(b -> b.getTitle().toLowerCase().contains(query.toLowerCase()))
                .toList();
    }

    public void search() {
        // the filter runs in getBooks()
    }
}

Source: Jakarta EE Tutorial: Managed Beans in Jakarta Faces Technology (the Jakarta EE 10 tutorial, which still describes the removed form)

The bean filters inside its getter, so the filter runs every time the table asks for its value. That is one of the habits of the period that the section’s best practices reject, and the migration removes it in Managed Beans to CDI. The old expression API and the old resolver live in two small classes:

QueryReader.java and LegacyVariableResolver.java (legacy)
package org.example.bookshelf.web;

import javax.faces.context.FacesContext;
import javax.faces.el.MethodBinding;
import javax.faces.el.ValueBinding;

/** Uses the Faces 1.x expression API that Faces 4.0 removed. */
public class QueryReader {

    public static String currentQuery() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        ValueBinding vb = ctx.getApplication().createValueBinding("#{catalogue.query}");
        return (String) vb.getValue(ctx);
    }

    public static void runSearch() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        MethodBinding mb = ctx.getApplication().createMethodBinding("#{catalogue.search}", new Class<?>[0]);
        mb.invoke(ctx, new Object[0]);
    }
}
package org.example.bookshelf.web;

import javax.faces.context.FacesContext;
import javax.faces.el.EvaluationException;
import javax.faces.el.VariableResolver;

/** A Faces 1.1 style resolver; Faces 1.2 deprecated it and Faces 4.0 removed it. */
public class LegacyVariableResolver extends VariableResolver {

    private final VariableResolver parent;

    public LegacyVariableResolver(VariableResolver parent) {
        this.parent = parent;
    }

    @Override
    public Object resolveVariable(FacesContext context, String name) throws EvaluationException {
        if ("appName".equals(name)) {
            return "Bookshelf";
        }
        return parent.resolveVariable(context, name);
    }
}

The remaining legacy files are the descriptors and the views:

WEB-INF/web.xml, faces-config.xml and beans.xml (legacy)
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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-app_4_0.xsd"
         version="4.0">

  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_REFRESH_PERIOD</param-name>
    <param-value>1</param-value>
  </context-param>

  <servlet>
    <servlet-name>facesServlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>facesServlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.xhtml</welcome-file>
  </welcome-file-list>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<faces-config 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_3.xsd"
              version="2.3">

  <application>
    <resource-bundle>
      <base-name>org.example.bookshelf.messages</base-name>
      <var>msg</var>
    </resource-bundle>
    <variable-resolver>org.example.bookshelf.web.LegacyVariableResolver</variable-resolver>
  </application>

  <lifecycle>
    <phase-listener>org.example.bookshelf.web.AuditPhaseListener</phase-listener>
  </lifecycle>

  <managed-bean>
    <managed-bean-name>settings</managed-bean-name>
    <managed-bean-class>org.example.bookshelf.web.CatalogueBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
  </managed-bean>

  <navigation-rule>
    <from-view-id>/index.xhtml</from-view-id>
    <navigation-case>
      <from-outcome>about</from-outcome>
      <to-view-id>/about.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>
</faces-config>
<?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="all">
</beans>
index.xhtml and legacy.xhtml (legacy)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
      xmlns:p="http://primefaces.org/ui">
<h:head>
  <title>#{msg['catalogue.title']}</title>
  <h:outputScript library="javax.faces" name="jsf.js"/>
</h:head>
<h:body>
  <h:form id="searchForm">
    <h:inputText id="query" value="#{catalogue.query}"/>
    <h:commandButton id="search" value="#{msg['catalogue.search']}" action="#{catalogue.search}">
      <f:ajax execute="@form" render=":results"/>
    </h:commandButton>
    <h:commandButton id="raw" type="button" value="Raw Ajax"
                     onclick="jsf.ajax.request(this, event, {execute: 'searchForm', render: 'results'}); return false;"/>
    <p:commandButton id="pf" value="PrimeFaces" action="#{catalogue.search}" update=":results"/>
  </h:form>
  <h:panelGroup id="results" layout="block">
    <h:dataTable value="#{catalogue.books}" var="book">
      <h:column><f:facet name="header">Title</f:facet>#{book.title}</h:column>
      <h:column><f:facet name="header">Author</f:facet>#{book.author}</h:column>
    </h:dataTable>
  </h:panelGroup>
</h:body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:jsf="http://xmlns.jcp.org/jsf"
      xmlns:pt="http://xmlns.jcp.org/jsf/passthrough">
<h:head><title>Old URIs</title></h:head>
<h:body>
  <form jsf:id="f">
    <input type="text" jsf:id="q" value="#{catalogue.query}" pt:placeholder="Title"/>
  </form>
  <h:outputText value="#{appName}"/>
</h:body>
</html>
legacy.jsp: the JSP view of the oldest generation
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
<f:view>
  <head><title>JSP view</title></head>
  <body>
    <h:form>
      <h:inputText value="#{catalogue.query}"/>
      <h:commandButton value="Search" action="#{catalogue.search}"/>
    </h:form>
  </body>
</f:view>
</html>

The bundle org.example.bookshelf.messages holds the two keys catalogue.title=Catalogue and catalogue.search=Search, and AuditPhaseListener is a three-method PhaseListener that logs each phase at FINE. Neither needs a change except the import line.

What Happens If You Only Upgrade the Runtime

The honest way to learn what a migration step does is to leave it out. The legacy application was put on the Faces 4.1 runtime in four increasingly well-prepared states, each on Mojarra 4.1.17 and on MyFaces 4.1.4 (Tomcat 11.0.26, Weld 6.0.4.Final). The first column says what was in the WAR.

Table 2. Four attempts to run the Bookshelf on a Faces 4.1 runtime
State of the WAR What happened (identical on both implementations unless stated)

The legacy WAR as built (PrimeFaces 11.0.0, OmniFaces 3.14.4, javax classes and descriptors)

Startup fails before Faces is reached. SEVERE: Error configuring application listener of class [org.primefaces.webapp.UploadedFileCleanerListener], NoClassDefFoundError: javax/servlet/ServletRequestListener, then Context [/bookshelf] startup failed due to previous errors. Every request answers 404. MyFaces adds Class javax.faces.webapp.FacesServlet not found.

Library jars replaced by PrimeFaces 16.0.0 and OmniFaces 5.5.5, application classes and descriptors still javax

Mojarra: Caused by: java.lang.NoClassDefFoundError: javax/faces/event/PhaseListener while loading AuditPhaseListener, then Servlet [facesServlet] ... threw load() exception; every request 404. MyFaces: Class javax.faces.webapp.FacesServlet not found, Exception sending context initialized event to listener ... StartupServletContextListener, the same ClassNotFoundException: javax.faces.event.PhaseListener, startup failed.

The WAR after Eclipse Transformer 1.0.0 (names rewritten to the Jakarta EE 9 form, libraries included)

Startup fails in the OmniFaces 3.14.4 listener: NoClassDefFoundError: jakarta/faces/bean/ApplicationScoped. The transformer produced the EE 9 names, and jakarta.faces.bean does not exist in Faces 4.x. Every request answers 404.

The sources after the OpenRewrite recipe (see Tools: Eclipse Transformer and OpenRewrite), with the new libraries

Does not compile: javax:javaee-web-api is still the compile dependency and the rewritten code calls methods that no longer exist (Tools: Eclipse Transformer and OpenRewrite).

None of these failures is subtle, and that is the good news: a missing rename stops the application at startup. The dangerous states are the ones in which the application starts. After the compile errors were fixed by hand but five legacy leftovers were kept (@ManagedProperty without @Inject, javax.faces.PROJECT_STAGE in web.xml, a <managed-bean> entry, a <variable-resolver> entry and jsf.js), the application deployed on both implementations and answered like this:

Table 3. A deployed application that is not migrated (Mojarra 4.1.17 and MyFaces 4.1.4)
Leftover Observed result Message in the log

javax.faces.PROJECT_STAGE context parameter

#{facesContext.application.projectStage} printed Production

None.

<managed-bean> entry settings in faces-config.xml

#{settings.query} printed an empty string

None.

<variable-resolver> entry for appName

#{appName} printed an empty string

None.

@ManagedProperty (now jakarta.faces.annotation) on a field of a CDI bean, without @Inject

The first request for index.xhtml answered 500: Cannot invoke "org.example.bookshelf.service.BookService.findAll()" because "this.service" is null

NullPointerException at first use. MyFaces logs it as occurred while processing RENDER_RESPONSE.

<h:outputScript library="jakarta.faces" name="jsf.js"/>

Mojarra renders <script src="RES_NOT_FOUND">; MyFaces renders nothing. A request for the resource answers 404 on both.

MyFaces: WARNING: Resource referenced by resourceName jsf.js and libraryName jakarta.faces not found ... It will be silenty ignored. (spelling as logged). Mojarra: none.

The first three rows are the reason the inventory of Take an Inventory First searches the descriptors as well as the Java: Faces never complains about an unknown parameter name, an unknown element it no longer reads, or a resolver it no longer calls. The @ManagedProperty row is the costliest of the five, because the annotation still exists in the new API with a different meaning (Managed Beans to CDI).

Source: Jakarta EE Tutorial: Configuring Jakarta Faces Applications, Faces 4.1 specification, B.2 and the MyFaces and Mojarra logs of the runs described above

Where in the Lifecycle Each Failure Shows Up

A migration problem is cheapest when it shows up early. The failures of the previous section appear at four different moments, and the later the moment, the easier it is to ship the problem. The phases named below are those of the request processing lifecycle; the first moments happen before any request.

Table 4. When each kind of failure appears
Kind of failure Moment What it looks like

A removed API called from Java (createValueBinding, VariableResolver)

Build time

A compile error, once the dependency is the Jakarta EE 11 API

A javax library, or a javax or jakarta.faces.bean class that a listener or the servlet loads

Container start-up, before Faces is reached

SEVERE in the log, Context [/bookshelf] startup failed, every request 404

A descriptor entry or a parameter that 4.x no longer reads (<managed-bean>, <variable-resolver>, javax.faces.PROJECT_STAGE)

Faces start-up

Nothing: no error and no warning on either implementation

An old tag library URI

View build, on the first request for a view (Restore View builds the tree from the page)

Nothing in 4.1: the tag works. Removal is planned for 5.0.

A script resource that no longer exists (jsf.js)

Render Response, when the head renderer asks for the resource

Mojarra: RES_NOT_FOUND in the markup. MyFaces: a log warning. The browser then has no faces object to call.

An expression that no longer resolves (#{appName}, #{settings.query})

Wherever the expression is read, usually Render Response

An empty string

A field that is no longer injected (@ManagedProperty without @Inject)

At the first use of the field: Render Response here, because the table reads getBooks(); Invoke Application for an action method

HTTP 500 and a NullPointerException; MyFaces names RENDER_RESPONSE in the log

javax to jakarta (Faces 3.0, Jakarta EE 9)

The first and widest change is the one the specification states in a single sentence: the namespace changed from javax.faces to jakarta.faces, and that is a breaking change. Jakarta EE 9 applied the same rename to every Jakarta EE API, so the change reaches far beyond Faces: Servlet, Expression Language, CDI, Bean Validation, Persistence and the rest of the platform moved with it. Jakarta EE 9 was only the rename, with no new behaviour (the BalusC article on Faces 4.0 describes Faces 3.0 as the same specification and implementation as 2.3 with javax replaced by jakarta), which is why a Faces 3.0 application is the safest intermediate stop.

The string javax.faces appears in more places than the imports. A migration that renames only the Java sources leaves the rest working until a request touches it. Search the whole tree for each of these:

Table 5. Where the rename has to reach
Place Old New

Java packages

javax.faces.*, javax.servlet.*, javax.el.*, javax.enterprise.*, javax.inject.*, javax.validation.*, javax.persistence.*, javax.ejb.*, javax.websocket.*

The same names under jakarta.

Annotations in javax.annotation

javax.annotation.PostConstruct, PreDestroy, Resource, Priority, javax.annotation.security.*

jakarta.annotation.*. But javax.annotation.processing is part of Java SE (module java.compiler) and stays.

Context parameters in web.xml

javax.faces.PROJECT_STAGE, javax.faces.STATE_SAVING_METHOD, javax.faces.FACELETS_*

jakarta.faces.*. An unrenamed name is ignored without a message (What Happens If You Only Upgrade the Runtime).

Component, converter, validator and renderer identifiers

javax.faces.Integer, javax.faces.Input, javax.faces.Text

jakarta.faces.*, in faces-config.xml, in f:converter converterId and in @FacesConverter values

Messages

The bundle javax.faces.Messages and keys such as javax.faces.component.UIInput.REQUIRED

jakarta.faces.Messages and jakarta.faces.component.UIInput.REQUIRED. A bundle with the old keys is ignored, as the messages page shows.

Resources

Library javax.faces with jsf.js; URL prefix /javax.faces.resource/

Library jakarta.faces with faces.js; URL prefix /jakarta.faces.resource/ (jsf.js to faces.js)

Request parameters

javax.faces.ViewState, javax.faces.partial.ajax, javax.faces.source

jakarta.faces.ViewState and the same under jakarta.faces.partial.*. Hand-written Ajax posts and Selenium or curl scripts break here.

Service files

META-INF/services/javax.faces.context.FacesContextFactory and every other factory file

META-INF/services/jakarta.faces.context.FacesContextFactory; the specification names this form for a factory resource

JNDI names read by Mojarra (4.0)

java:comp/env/jsf/ClientSideSecretKey

java:comp/env/faces/ClientSideSecretKey. This rename came with 4.0, not 3.0; the state saving page explains the silent fallback to a random key.

Two Java rules decide whether a global search-and-replace is safe. The first is that not every javax package moved: the packages that Java SE itself provides (javax.naming, javax.sql, javax.crypto, javax.net, javax.security.auth, javax.xml.parsers, javax.annotation.processing) keep their names. A blanket s/javax/jakarta/ breaks the build in exactly those places. The second is that the rename does not tell you whether the target exists: javax.faces.bean and javax.faces.el become jakarta.faces.bean and jakarta.faces.el, names that Faces 4.0 removed (Removed and Deprecated APIs).

The script below renames only the packages and names that moved. It was run on a copy of the legacy sources to which a class with javax.naming, javax.sql, javax.annotation.PostConstruct and javax.annotation.processing imports had been added.

javax2jakarta.sh: rename what moved, keep what Java SE owns
#!/bin/sh
# javax2jakarta.sh [project-directory]
cd "${1:-.}" || exit 1
find src -type f \( -name '*.java' -o -name '*.xhtml' -o -name '*.xml' -o -name '*.properties' -o -name '*.js' \) -print0 |
  xargs -0 sed -i.bak -E \
    -e 's/javax\.(faces|servlet|el|enterprise|inject|validation|persistence|ejb|websocket|interceptor|transaction|xml\.bind)([."])/jakarta.\1\2/g' \
    -e 's/javax\.security\.enterprise/jakarta.security.enterprise/g' \
    -e 's/javax\.annotation\.(PostConstruct|PreDestroy|Resource|Resources|Priority|Generated|security)/jakarta.annotation.\1/g'
find src -name '*.bak' -delete

After it ran, 19 lines contained jakarta. and three javax lines were left: the javax.annotation.processing, javax.naming and javax.sql imports, which is correct. The inventory of Take an Inventory First then read 0, 2, 2, 0, 1, 1, 2, 1, 1, 1, 3: the packages and the parameters were done, and the work that a rename cannot do (managed beans, the old expression API, the resolver, the namespaces, the JSP view, jsf.js, the descriptor namespaces) was still listed.

Tools: Eclipse Transformer and OpenRewrite

Two tools do part of this work, and they do different parts. Both were run on the legacy Bookshelf. The results matter more than the descriptions, because each tool stops at a different boundary.

Eclipse Transformer: Names in Binaries, up to Jakarta EE 9

The Eclipse Transformer rewrites javax names to jakarta names in already built archives (WAR, JAR, EAR), in bytecode and in text resources, using the rule sets of the Jakarta project. It needs no source and no build. Version 1.0.0 was run from the command line on the legacy WAR:

Transforming a WAR with the Eclipse Transformer command line
java -jar org.eclipse.transformer.cli-1.0.0.jar bookshelf-1.0.0.war bookshelf-jakarta.war

It finished with Transformer Return Code [ 0 ] [ Success ], accepted 2648 resources, changed 1245 and renamed two. Its output differed from the original in these ways, all read from the transformed WAR:

  • web.xml became web-app_5_0.xsd, version="5.0", the jakarta.faces parameter names and the jakarta.faces.webapp.FacesServlet class.

  • faces-config.xml became web-facesconfig_3_0.xsd, version="3.0", and still named LegacyVariableResolver and the <managed-bean> entry.

  • beans.xml became beans_3_0.xsd.

  • The bytecode of the application and of the two libraries now refers to jakarta.faces.bean.ManagedBean and jakarta.faces.bean.ViewScoped.

  • The XHTML files were not changed: the xmlns.jcp.org namespaces and library="javax.faces" with jsf.js were still there.

  • The two library jars were transformed too (primefaces-11.0.0.jar now refers to jakarta/faces/component/UIComponent).

That is exactly the Faces 3.0 and Jakarta EE 9 boundary. It is the right tool to run a binary that you cannot rebuild, or a library that has no jakarta release, on an EE 9 server. It is the wrong tool to reach Faces 4: the transformed WAR failed at startup on Faces 4.1 on both implementations, because jakarta.faces.bean does not exist there (What Happens If You Only Upgrade the Runtime). The transformer migrates names, not APIs.

OpenRewrite: Source Recipes for Faces 4.1

OpenRewrite rewrites the source of a project with recipes. The recipe set rewrite-migrate-java contains Faces recipes for every step: Faces2xMigrationToJakartaFaces3x, Faces3xMigrationToFaces4x and, covering all of them, Faces4xMigrationToFaces41x. That last recipe runs the 2.x to 3.x and 3.x to 4.x recipes, then updates the Faces API dependency to 4.1.x, replaces the OmniFaces namespaces and upgrades the open source libraries. It was run with the Maven plugin on a copy of the legacy sources:

Running the Faces 4.1 recipe with Maven, with nothing added to the pom
mvn org.openrewrite.maven:rewrite-maven-plugin:6.46.1:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:3.42.1 \
  -Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.Faces4xMigrationToFaces41x

The plugin 6.46.1 and the recipes 3.42.1 are the newest releases on Maven Central on 2026-10-08. The recipe page of the OpenRewrite documentation now sends users to a different distribution point (version 3.45.0 of the recipes, from a repository that requires an account and a token), and says the recipes are published under the Moderne Source Available License. Check the current terms of the artifacts you use; the run described here needed no credentials and used none.

The run changed these files, and did not change the others:

Table 6. What the Faces4xMigrationToFaces41x recipe did to the legacy Bookshelf
File Changed Left untouched

pom.xml

PrimeFaces 11.0.0 became 14.0.18 with the jakarta classifier; OmniFaces 3.14.4 became 5.5.5.

javax:javaee-web-api:8.0.1 stayed the compile dependency, so nothing compiles against jakarta.*. The recipe pins PrimeFaces to 14.0.x, not 16. No Faces implementation is added.

BookService, CatalogueBean

@ManagedBean(name = "bookService", eager = true) became @Named("bookService"); the javax.faces.bean scopes became CDI scopes and jakarta.faces.view.ViewScoped; @ManagedProperty was imported from jakarta.faces.annotation.

eager = true disappeared without a replacement. @ManagedProperty(value = "#{bookService}") and its setter stayed, without @Inject (the null service of What Happens If You Only Upgrade the Runtime). The blank line after package was removed.

QueryReader

The types ValueBinding and MethodBinding were replaced by ValueExpression and MethodExpression.

createValueBinding and createMethodBinding are still called on Application, so the class does not compile.

LegacyVariableResolver

It now extends jakarta.el.ELResolver.

It does not implement the abstract methods of ELResolver and still overrides resolveVariable: it does not compile.

AuditPhaseListener

javax.faces.event became jakarta.faces.event.

Nothing left to do.

web.xml

Namespace and version="6.0" (not 6.1), the context parameter names and the servlet class.

faces-config.xml

Namespace and version="4.0" (not 4.1).

The <variable-resolver> and <managed-bean> elements are still there, and still ignored at run time.

beans.xml

Not touched: it kept the xmlns.jcp.org namespace, version="2.0" and bean-discovery-mode="all".

index.xhtml, legacy.xhtml

The namespaces became the URNs (jakarta.faces.html, jakarta.faces.core, jakarta.faces.facelets, jakarta.tags.core, primefaces, jakarta.faces, jakarta.faces.passthrough). library="javax.faces" became library="jakarta.faces".

name="jsf.js" and the inline jsf.ajax.request(...) in an onclick stayed. The jsf: prefix stayed (it only has to map to the right namespace).

legacy.jsp

Not touched. Nothing converts JSP to Facelets.

The recipe has a separate step that rewrites jsf.ajax to faces.ajax and window.jsf to window.faces, but only in .js and .ts files. A JavaScript call written in an XHTML attribute is outside it.

A last, practical observation. On JDK 26 the same recipe run reported BUILD SUCCESS but printed CatalogueBean.java is not print idempotent under "problems parsing some source files", and then did not change CatalogueBean.java at all: the managed bean, the file that most needs the recipe, was skipped. The run on JDK 21 changed it. A warning in the middle of a green build is easy to miss; run the recipe on the JDK it was tested with (21 here) and check that the number of changed files matches the files you expect.

The recipe made the mechanical part of the work in seconds and left a project that does not compile. Everything it left is on the list of the next sections.

XML Namespaces: java.sun.com and xmlns.jcp.org to the URNs (Faces 4.0)

Faces 4.0 replaced the HTTP URLs of its tag libraries with URNs (jakarta.faces.html, jakarta.faces.core, jakarta.faces.facelets, jakarta.faces.composite, jakarta.faces.passthrough, jakarta.faces for pass-through elements, and jakarta.tags.core and jakarta.tags.functions for the Tags library). The full table with the prefixes is in Facelets and XHTML Views; the old-namespaces section shows the sed script that rewrites them. This section adds what the migration experiments showed.

The descriptors have a second, separate family of namespaces, and a migration touches both. The recipe wrote https://jakarta.ee/xml/ns/jakartaee into web.xml and faces-config.xml, and left beans.xml on http://xmlns.jcp.org/xml/ns/javaee.

Table 7. Three generations of namespaces, and the three places that use them
Where JSF 1.x to 2.1 (Java EE 5, 6) JSF 2.2, 2.3 and Faces 3.0 as written (Java EE 7, 8) Faces 4.0 and later (Jakarta EE 9 onwards)

Tag libraries in XHTML

http://java.sun.com/jsf/html

http://xmlns.jcp.org/jsf/html

jakarta.faces.html

Pass-through elements

not available (the Facelets 1.x jsfc attribute)

http://xmlns.jcp.org/jsf (prefix jsf:)

jakarta.faces (prefix faces: by convention)

JSTL in XHTML

http://java.sun.com/jsp/jstl/core

http://xmlns.jcp.org/jsp/jstl/core

jakarta.tags.core

Descriptors

http://java.sun.com/xml/ns/javaee

http://xmlns.jcp.org/xml/ns/javaee

https://jakarta.ee/xml/ns/jakartaee

Two facts were observed on Mojarra 4.1.17 and on MyFaces 4.1.4 (Tomcat 11.0.26):

  • The old tag library URIs still work. A page with http://xmlns.jcp.org/jsf/html, /core, /facelets, /passthrough, the pass-through element namespace http://xmlns.jcp.org/jsf and the JSTL URI rendered correctly with h:form, f:ajax, p:commandButton, jsf:id on a plain <form>, pt:placeholder and c:if; so did the same page with the java.sun.com URIs. The specification plans to remove the old namespaces in 5.0, so treat this as a grace period.

  • The old descriptors still load. A web.xml with the xmlns.jcp.org namespace and version="4.0" and a faces-config.xml with version="2.3" (without the removed elements) deployed on Tomcat 11.0.26 with both implementations, and the application answered. This was run only on Tomcat; application servers may validate descriptors more strictly. Update them anyway: editors validate against the schema named in the document, and the schema decides which elements you see.

A new page that declares the pass-through element namespace as xmlns:jsf="jakarta.faces" worked with the old prefix jsf:; the prefix is only a local alias. The convention of the 4.x documentation is faces: (see pass-through elements).

index.xhtml header before and after the namespaces step
<!-- before -->
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
      xmlns:p="http://primefaces.org/ui">

<!-- after -->
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets"
      xmlns:c="jakarta.tags.core"
      xmlns:p="primefaces">

The PrimeFaces URN primefaces is the short form of http://primefaces.org/ui; PrimeFaces 16.0.0 accepts both (see the PrimeFaces namespace section). The OmniFaces URI http://omnifaces.org/ui is omnifaces in OmniFaces 5, and the recipe rewrites it (Libraries Across Majors).

JSP to Facelets

JSP was deprecated in Faces 2.0, when Facelets became the standard view declaration language, and removed in Faces 4.0, together with the JSP tag classes of the jakarta.faces.webapp package. The BalusC article states the reason: JSP writes template text to the response as soon as it meets it, while Faces needs it postponed to Render Response, which forced the f:verbatim workaround and an "everything must be a component" habit. The tools of Tools: Eclipse Transformer and OpenRewrite do not convert a JSP view; the OpenRewrite run left legacy.jsp byte for byte as it was.

The conversion is a short list of mechanical rules plus one design decision per page:

Table 8. JSP constructs and their Facelets equivalents
In a JSP view In a Facelets view

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

xmlns:h="jakarta.faces.html" on the root element

<f:view> around the page

Not needed. Use <f:view locale="..."> only when you need its attributes.

<f:verbatim> around template text

Delete it. Template text is part of the tree in the right order. The tag is gone.

<jsp:include>, <%@ include %>

ui:include, or better a template with ui:composition and ui:define (Templating)

JSP comments <%-- --%>

<!-- --> (sent to the browser) or ui:remove (not sent)

Scriptlets, JSP declarations, ${...} read as JSP EL

Delete them. Use #{...} expressions (Expression Language).

c:forEach and c:if mixed with component tags

Usually ui:repeat and rendered; the two sets of tags run at different times (JSTL and build time versus render time)

A custom component as a JSP tag handler (UIComponentELTag) plus a .tld

A composite component, a tag file, or @FacesComponent(createTag = true) (Composite Components, Custom Components and Renderers)

The FacesServlet mapped to *.jsp, *.jsf or /faces/*

The mapping *.xhtml, or none at all with the automatic extensionless mapping (Project Setup)

The legacy search page converts to this Facelets page, which is the search.xhtml of the migrated Bookshelf:

search.xhtml: legacy.jsp as a Facelets view
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html">
<h:head>
  <title>Search</title>
</h:head>
<h:body>
  <h:form>
    <h:inputText id="query" value="#{catalogue.query}"/>
    <h:commandButton id="go" value="Search" action="#{catalogue.search}"/>
  </h:form>
</h:body>
</html>

It rendered on both implementations (a form with the jakarta.faces.ViewState field). The head of the page also shows that the HTML moved from "a JSP that happens to contain Faces tags" to "a view that is a component tree": there is a h:head and a h:body instead of head and body inside f:view, so Faces can add resources to the page (jsf.js to faces.js).

Old URLs are the other half of the move. Users and search engines still hold /search.jsp or /faces/search.jsp. Two observations from the run, with the FacesServlet mapped to *.jsp as well as *.xhtml:

  • A request for /legacy.jsp when a view /legacy.xhtml existed rendered that view on Mojarra and on MyFaces (PrimeFaces.settings in the page showed viewId:'/legacy.xhtml'). The JSP file itself, with its <%@ taglib %> lines, was never read.

  • The same request without a legacy.xhtml answered 404 on Mojarra and 500 on MyFaces (InvalidFileException: Invalid path provided: /legacy.jsp).

So a temporary extra mapping lets old bookmarks keep working while the pages are converted one by one, but delete the .jsp files at the same time as you convert them: leaving the old file next to the new view only hides which of the two a user got. For a clean break, answer the old URLs with a 301 redirect from a servlet filter or from the web server in front, and remove the mapping.

Managed Beans to CDI

Faces 4.0 removed the native managed bean facility: @ManagedBean, the scopes in javax.faces.bean, @ManagedProperty in that package and the <managed-bean> element. The mapping of every piece, with the CDI form of each, is in the Backing Beans and CDI page and its worked migration. What follows is what the Bookshelf run adds: what the recipe gets wrong, and what a managed bean silently did that a CDI bean does not.

The result for BookService: @Named, an application scope, and an explicit start-up observer
package org.example.bookshelf.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Named;
import java.util.List;
import java.util.logging.Logger;
import org.example.bookshelf.model.Book;

@Named
@ApplicationScoped
public class BookService {

    private static final Logger LOG = Logger.getLogger(BookService.class.getName());

    private final List<Book> books = List.of(
            new Book("Dune", "Frank Herbert"),
            new Book("Neuromancer", "William Gibson"),
            new Book("Snow Crash", "Neal Stephenson"));

    /** Replaces eager = true of the old managed bean: runs when the application scope starts. */
    void start(@Observes @Initialized(ApplicationScoped.class) Object event) {
        LOG.info("catalogue holds " + books.size() + " books");
    }

    public List<Book> findAll() {
        return books;
    }
}

Three details of this class are migration decisions, not typing:

  • @Named without a name is bookService, the same rule @ManagedBean used, so the expressions of the views need no change. The recipe wrote the name out (@Named("bookService")); both forms resolve.

  • eager = true has no automatic equivalent. The recipe dropped the attribute without a trace. A CDI application-scoped bean is created on its first use. The observer above runs when the application scope is initialised: on Mojarra 4.1.17 with Weld 6.0.4.Final on Tomcat 11 the log line appeared at start-up, before the first request. Do this only for a bean whose start-up has a side effect you need (loading a cache, validating configuration); most services do not need it.

  • The class is not Serializable and does not need to be. An application-scoped bean is never passivated. The view-scoped bean below is a different matter.

CatalogueBean: @Inject instead of @ManagedProperty, and the filter out of the getter
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import java.util.Locale;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named("catalogue")
@ViewScoped
public class CatalogueBean implements Serializable {

    @Inject
    private BookService service;

    private String query = "";
    private List<Book> books = List.of();

    @PostConstruct
    void init() {
        books = service.findAll();
    }

    public String getQuery() {
        return query;
    }

    public void setQuery(String query) {
        this.query = query;
    }

    public List<Book> getBooks() {
        return books;
    }

    public void search() {
        String needle = query.toLowerCase(Locale.ROOT);
        books = service.findAll().stream()
                .filter(b -> b.getTitle().toLowerCase(Locale.ROOT).contains(needle))
                .toList();
    }
}

The bean differs from the recipe’s output in three ways that matter:

  • @Inject replaces the field annotation and the setter. The recipe kept @ManagedProperty(value = "#{bookService}") with only its import changed. The annotation still exists in 4.1, in jakarta.faces.annotation, with a different meaning: it is a CDI qualifier that evaluates an EL expression, and it injects nothing unless it is combined with @Inject. A bean that kept it compiled, started, and answered the first request with a NullPointerException (What Happens If You Only Upgrade the Runtime). Grep for ManagedProperty after every automated run; the only legitimate uses are @Inject @ManagedProperty("#{param.id}") for an EL value, as shown on the backing beans page.

  • The filter runs once per action, not once per render. The legacy getBooks() filtered the service’s list inside the getter, so the work ran every time the table asked for its value, and in a real application that call would have been a database query. The migration is a good moment to remove the habit: the filter moved into search(), and the getter returns a field. The view-scoped bean keeps the result between the postbacks of the same view. The change is behaviour-preserving for the page: Mojarra and MyFaces returned the same rows for DUNE (case-insensitive), snow and neuro, for the f:ajax button and for the PrimeFaces button.

  • toLowerCase(Locale.ROOT). Case-insensitive comparison with the default locale gives wrong answers in a few locales (the Turkish dotted and dotless i). It is not a Faces change; it is the kind of defect an old code base carries and a review during a migration finds.

Two descriptor facts complete the bean story:

  • beans.xml from the Java EE 8 era was accepted. The unchanged file (xmlns.jcp.org namespace, version="2.0", bean-discovery-mode="all") deployed and worked with Weld 6.0.4.Final on Tomcat 11. It is still worth replacing: in discovery mode all every class of the archive is a bean candidate, which is how a converter or a helper ends up injectable by accident. The Jakarta EE 11 form is shown in Descriptors, Parameters and the Build.

  • Without any beans.xml, Weld on Tomcat discovered nothing. The page rendered, and the table had no books (#{catalogue.books} resolved to nothing) and the log had no error, which is the same silent failure as the unresolved settings managed bean. CDI’s implicit bean archive rule lets a full application server discover annotated beans in an archive without beans.xml (not run on a server here); on Tomcat with Weld the descriptor is required (see Project Setup).

The <managed-bean> entry named settings in faces-config.xml has no replacement worth writing: it declared a session-scoped copy of CatalogueBean, nothing in the views used it, and it was removed. If yours is used, the replacement is a class with the right scope annotation and a @Named name. Session scope for page state is the first habit to leave behind, with @ViewScoped as the usual successor (Scopes).

Removed and Deprecated APIs

Faces 4.0 removed a defined list of APIs, Faces 4.1 deprecated more, and Faces 5.0 is planned to remove what 4.0 and 4.1 deprecated. The list below was built by comparing the class lists of the API jars (javax.faces-api 2.3, jakarta.faces-api 3.0.0, 4.0.1, 4.1.2 and 5.0.0-M9) and then reading the replacement in the 4.1 API. The comparison is of classes; removed fields and methods were found separately (see Preparing for Faces 5.0).

Table 9. Removed from the API in Faces 4.0 (present in jakarta.faces-api 3.0.0, absent in 4.0.1)
Removed Use instead

jakarta.faces.bean.*: ManagedBean, ManagedProperty, ApplicationScoped, SessionScoped, RequestScoped, ViewScoped, NoneScoped, CustomScoped, ReferencedBean

CDI @Named, @Inject and the scopes (Managed Beans to CDI)

jakarta.faces.el.*: ValueBinding, MethodBinding, VariableResolver, PropertyResolver, EvaluationException, MethodNotFoundException, PropertyNotFoundException, ReferenceSyntaxException

jakarta.el.ValueExpression, MethodExpression, ELResolver, ELException, MethodNotFoundException, PropertyNotFoundException

Application.createValueBinding, createMethodBinding, getVariableResolver, getPropertyResolver (present in 3.0.0, absent in 4.0.1)

Application.evaluateExpressionGet, getExpressionFactory and getELResolver

MethodBindingValidator, MethodBindingValueChangeListener, MethodBindingAdapterBase in jakarta.faces.component

MethodExpressionValidator and MethodExpressionValueChangeListener, or the validator and valueChangeListener attributes

jakarta.faces.webapp JSP classes: UIComponentELTag, UIComponentTag, UIComponentBodyTag, UIComponentClassicTagBase, UIComponentTagBase, ConverterELTag, ConverterTag, ValidatorELTag, ValidatorTag, AttributeTag, FacetTag, PreJsf2ExceptionHandlerFactory

Facelets tag libraries, composite components and @FacesComponent(createTag = true) (JSP to Facelets)

ResourceResolver and FaceletsResourceResolver in jakarta.faces.view.facelets

A ResourceHandler that overrides createViewResource(FacesContext, String) (Extending Faces)

StateManager.SerializedView and the deprecated StateManager and ResponseStateManager methods (saveSerializedView, getTreeStructureToSave and others)

StateManagementStrategy (State Saving)

UIComponent.CURRENT_COMPONENT and CURRENT_COMPOSITE_COMPONENT (fields)

UIComponent.getCurrentComponent(FacesContext) and getCurrentCompositeComponent(FacesContext)

jakarta.faces.model.ResultDataModel

A ListDataModel or CollectionDataModel, or a plain List; the class wrapped a JSP Tags SQL Result

The standalone Facelets context parameters facelets.*

jakarta.faces.FACELETS_* (Configuration)

Between 2.3 and 3.0 the class lists differ only by the four binding adapter classes (MethodBindingMethodExpressionAdapter, MethodExpressionMethodBindingAdapter, ValueBindingValueExpressionAdapter, ValueExpressionValueBindingAdapter) and an internal cache class: the rename really was only the rename. Between 4.0.1 and 4.1.2 the only class that disappeared is an internal iterator.

The Expression API and the Resolver, Rewritten

Two classes of the legacy Bookshelf used the removed expression API. The recipe changed their imports and left the calls. The migrated QueryReader evaluates a value expression through the Application and builds a method expression through the ExpressionFactory:

QueryReader.java after the migration
package org.example.bookshelf.web;

import jakarta.el.ExpressionFactory;
import jakarta.el.MethodExpression;
import jakarta.faces.context.FacesContext;

/** Reads a value and calls a method through the Expression Language API of Jakarta Faces 4. */
public class QueryReader {

    public static String currentQuery() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        return ctx.getApplication().evaluateExpressionGet(ctx, "#{catalogue.query}", String.class);
    }

    public static void runSearch() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        ExpressionFactory factory = ctx.getApplication().getExpressionFactory();
        MethodExpression search = factory.createMethodExpression(
                ctx.getELContext(), "#{catalogue.search}", void.class, new Class<?>[0]);
        search.invoke(ctx.getELContext(), new Object[0]);
    }
}

The Faces 1.1 VariableResolver becomes an ELResolver, which is a different contract: three methods that read, type and write a property of a base object, and one that tells whether a name is read-only. A resolver for a top-level name such as appName answers only when base is null and marks the property resolved.

AppNameResolver.java replaces LegacyVariableResolver
package org.example.bookshelf.web;

import jakarta.el.ELContext;
import jakarta.el.ELResolver;

/** The EL resolver that replaces the Faces 1.1 variable resolver: it supplies the name appName. */
public class AppNameResolver extends ELResolver {

    private static final String NAME = "appName";

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (base == null && NAME.equals(property)) {
            context.setPropertyResolved(true);
            return "Bookshelf";
        }
        return null;
    }

    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (base == null && NAME.equals(property)) {
            context.setPropertyResolved(true);
            return String.class;
        }
        return null;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        // read-only: isReadOnly() answers true, so Faces never calls this for appName
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (base == null && NAME.equals(property)) {
            context.setPropertyResolved(true);
            return true;
        }
        return false;
    }

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return base == null ? String.class : null;
    }
}

In faces-config.xml the <variable-resolver> entry becomes <el-resolver>. The old entry did not fail, and #{appName} printed an empty string (What Happens If You Only Upgrade the Runtime); with the new one legacy.xhtml printed Bookshelf on both implementations.

<application>
  <resource-bundle>
    <base-name>org.example.bookshelf.messages</base-name>
    <var>msg</var>
  </resource-bundle>
  <el-resolver>org.example.bookshelf.web.AppNameResolver</el-resolver>
</application>

A resolver registered with <el-resolver> is one for the whole application. If the only need is "a value reachable by name", a CDI bean with @Named and a @Produces method does the same without a resolver (Producers); the resolver is the right tool when the answer depends on the base object or on the name itself.

Deprecated for Removal in 4.0 and 4.1

The 4.1 API marks a further set as deprecated for removal, which the compiler reports with a [removal] warning and which Faces 5.0 plans to delete. A scan of the 4.1.2 API jar for @Deprecated(forRemoval = true) gives:

Table 10. Marked for removal in jakarta.faces-api 4.1.2
API Since Replacement

FacesConfig.version() and FacesConfig.Version

4.0

Nothing: the element has had no effect since 4.0. Write @FacesConfig without it.

ResourceHandler.JSF_SCRIPT_LIBRARY_NAME, JSF_SCRIPT_RESOURCE_NAME

4.0

FACES_SCRIPT_LIBRARY_NAME and FACES_SCRIPT_RESOURCE_NAME

ViewHandler.DEFAULT_SUFFIX and DEFAULT_SUFFIX_PARAM_NAME

4.0

DEFAULT_FACELETS_SUFFIX and FACELETS_SUFFIX_PARAM_NAME (Configuration)

HtmlInputFile (alt, autocomplete, maxlength, readonly, size) and HtmlInputSecret.alt

4.0

Nothing: the API documentation says these attributes are not specified in HTML for these elements

StateManager.PARTIAL_STATE_SAVING_PARAM_NAME and FULL_STATE_SAVING_VIEW_IDS_PARAM_NAME (full state saving)

4.1

Partial state saving, the default (State Saving)

ActionSource2, ActionSource2AttachedObjectHandler, ActionSource2AttachedObjectTarget

4.1

The methods moved to ActionSource as default methods

PostConstructCustomScopeEvent, PreDestroyCustomScopeEvent

4.1

CDI scope lifecycle events

cc:extension (a tag, not in the jar)

4.1

Nothing; it was a design-time placeholder

The deprecation warnings are the cheapest migration test there is: compile with -Xlint:removal against the 4.1 API and every hit is something Faces 5.0 is planned to delete. A class with all five legacy idioms compiled against 4.1.2 with five [removal] warnings (Preparing for Faces 5.0 shows the same file against the 5.0 milestone).

jsf.js to faces.js

The JavaScript API of Faces was renamed with the package: the resource jsf.js of the library javax.faces became faces.js of the library jakarta.faces, the global jsf became faces, and the Java constants followed. Faces 4.0 introduced the new names and deprecated the old Java constants for removal.

Table 11. The Ajax JavaScript names, old and new
Faces 2.x (javax) Faces 4.x

<h:outputScript library="javax.faces" name="jsf.js"/>

<h:outputScript library="jakarta.faces" name="faces.js"/>

jsf.ajax.request(...), jsf.ajax.addOnEvent(...), jsf.getViewState(...)

faces.ajax.request(...), faces.ajax.addOnEvent(...), faces.getViewState(...)

window.jsf

window.faces

ResourceHandler.JSF_SCRIPT_LIBRARY_NAME, JSF_SCRIPT_RESOURCE_NAME

ResourceHandler.FACES_SCRIPT_LIBRARY_NAME, FACES_SCRIPT_RESOURCE_NAME

Request parameters javax.faces.partial.ajax, javax.faces.source

jakarta.faces.partial.ajax, jakarta.faces.source

The failure is in the table of What Happens If You Only Upgrade the Runtime: a view that asks for jsf.js gets no script, the log says so (MyFaces) or not (Mojarra), and the first jsf.ajax.request call has no script to call (the global jsf does not exist any more; not run in a browser here). The recipe rewrote the library name in the XHTML (javax.faces to jakarta.faces) but not jsf.js, and it rewrote jsf.ajax only in .js and .ts files, not in an onclick attribute of a page. Search the XHTML too.

index.xhtml after the change: faces.js, the standard Ajax button, the raw API call and the PrimeFaces button
<h:head>
  <title>#{msg['catalogue.title']}</title>
  <h:outputScript library="jakarta.faces" name="faces.js"/>
</h:head>
<h:body>
  <h:form id="searchForm">
    <h:inputText id="query" value="#{catalogue.query}"/>
    <h:commandButton id="search" value="#{msg['catalogue.search']}" action="#{catalogue.search}">
      <f:ajax execute="@form" render=":results"/>
    </h:commandButton>
    <h:commandButton id="raw" type="button" value="Raw Ajax"
                     onclick="faces.ajax.request(this, event, {execute: 'searchForm', render: 'results'}); return false;"/>
    <p:commandButton id="pf" value="PrimeFaces" action="#{catalogue.search}" update=":results"/>
  </h:form>
  <h:panelGroup id="results" layout="block">
    <h:dataTable value="#{catalogue.books}" var="book">
      <h:column><f:facet name="header">Title</f:facet>#{book.title}</h:column>
      <h:column><f:facet name="header">Author</f:facet>#{book.author}</h:column>
    </h:dataTable>
  </h:panelGroup>
</h:body>

The explicit h:outputScript is only needed because the page calls faces.ajax.request itself; f:ajax adds the script by itself. The faces.js resource was served (/jakarta.faces.resource/faces.js.xhtml?ln=jakarta.faces answered 200) and jsf.js in either library was not (404 for ln=jakarta.faces and for ln=javax.faces). The Ajax page explains the details, including why the script has to come first in the head when your own scripts use faces while loading (Getting faces.js onto the Page).

Descriptors, Parameters and the Build

The last mechanical step is the files that name versions. They keep working in older forms (the earlier experiments), so nothing forces the change except the editor, the validator and the next person who reads them. The target for Jakarta EE 11 is web-app_6_1.xsd, web-facesconfig_4_1.xsd and beans_4_1.xsd; the section on faces-config.xml has the full table of versions.

The migrated pom.xml: APIs from the Jakarta EE 11 Web Profile, libraries on their Jakarta lines
<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>

<dependencies>
  <dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-web-api</artifactId>
    <version>11.0.0</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.primefaces</groupId>
    <artifactId>primefaces</artifactId>
    <version>16.0.0</version>
  </dependency>
  <dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>5.5.5</version>
  </dependency>
</dependencies>

The first dependency is the one the recipe never changed. On a Jakarta EE 11 server nothing else is needed; on Tomcat the Faces implementation and Weld are added to the WAR (Tomcat and Weld covers it), for example org.glassfish:jakarta.faces:4.1.17 or org.apache.myfaces.core:myfaces-impl:4.1.4 together with org.jboss.weld.servlet:weld-servlet-shaded:6.0.4.Final.

web.xml, faces-config.xml and beans.xml after the migration
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
         version="6.1">

  <context-param>
    <param-name>jakarta.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
  <context-param>
    <param-name>jakarta.faces.FACELETS_REFRESH_PERIOD</param-name>
    <param-value>1</param-value>
  </context-param>

  <servlet>
    <servlet-name>facesServlet</servlet-name>
    <servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>facesServlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.xhtml</welcome-file>
  </welcome-file-list>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
              version="4.1">

  <application>
    <resource-bundle>
      <base-name>org.example.bookshelf.messages</base-name>
      <var>msg</var>
    </resource-bundle>
    <el-resolver>org.example.bookshelf.web.AppNameResolver</el-resolver>
  </application>

  <lifecycle>
    <phase-listener>org.example.bookshelf.web.AuditPhaseListener</phase-listener>
  </lifecycle>

  <navigation-rule>
    <from-view-id>/index.xhtml</from-view-id>
    <navigation-case>
      <from-outcome>about</from-outcome>
      <to-view-id>/about.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>
</faces-config>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_1.xsd"
       version="4.1" bean-discovery-mode="annotated">
</beans>

The PROJECT_STAGE value Development is for the developer’s machine: a deployment sets Production (Project Stages), and nothing sensitive belongs in these files; the migration changed no secret because the legacy application had none, but an application that kept a credential or a client-state key here should move it out in the same step (Security). @FacesConfig, which Faces 2.3 needed on a CDI bean to enable the CDI features, is not required any more, and its version element is deprecated for removal (Project Setup).

Libraries Across Majors

A Faces application is rarely only Faces. The component library, the utility library and the server each have their own line of releases, and each has a release at which it crossed the javax boundary. Crossing it is part of the same migration, and the library often needs more than one step.

PrimeFaces

PrimeFaces did not cross the boundary at once. Its plain artifact stayed on javax for years, and a second artifact with the Maven classifier jakarta carried the same version for the new namespace, until 16.0.0. This was checked by reading the class CommandButton in each jar on Maven Central:

Table 12. The PrimeFaces artifacts and the namespace of their classes
Version primefaces-X.jar (no classifier) primefaces-X-jakarta.jar Breaking changes in the migration guide (selection)

12.0.0, 13.0.0

javax.faces

jakarta.faces

12: PrimeFlex 3 becomes the standard, Lightbox, Ring and ContentFlow deprecated. 13: Internet Explorer support removed; jqPlot Chart, Lightbox, Ring, ContentFlow and p:repeat deleted; tabular layouts of DataGrid, PanelGrid, SelectOneRadio and SelectManyCheckbox deprecated.

14.0.0

javax.faces

jakarta.faces

Support for JSF 2.0, 2.1 and 2.2 removed; Watermark deleted (use pt:placeholder); SearchExpressionFacade replaced by SearchExpressionUtils; Apache Commons FileUpload deprecated; p:xxxChart deprecated for p:chart; primefaces.CLIENT_SIDE_LOCALISATION on by default.

15.0.0

javax.faces

jakarta.faces

ThemeRoller themes unsupported; the extra themes moved to primefaces-themes; legacy Chart.js components, InputSwitch and Commons FileUpload support deleted; UIData renamed PrimeUIData; primefaces.HTML5_COMPLIANCE defaults to auto.

16.0.0

not published; a single artifact, jakarta.faces

not published

JSF and javax.* support removed (baseline Faces 4.0); PrimeClientWindowLifecycleFactory replaced by a client-window-factory entry; p:separator removed for p:divider; dialog markup changed; widgets are JavaScript classes; PrimeFlex 4.

Three consequences for the plan:

  • The classifier is a transition tool, not a design. A project on Java EE 8 uses the plain artifact up to 15.x; a project on Jakarta EE uses the same version with <classifier>jakarta</classifier>; at 16.0.0 the classifier disappears, and a build that still asks for it cannot resolve the artifact. The OpenRewrite recipe added the classifier and pinned 14.0.x, which is a correct step on the way and a wrong end.

  • Go one major at a time. Each guide lists what was deleted and what was deprecated "for the next release". Deleting is where code breaks, so read the guide of every major you cross, grep your XHTML and Java for the names it lists, and build and test between steps. A jump from 11 to 16 skips the deprecation notices that tell you what to change first. The PrimeFaces pages cover the current components; the migration guides cover the old names.

  • Not every major breaks your pages. The legacy p:commandButton of the Bookshelf, written for PrimeFaces 11, worked unchanged on 16.0.0 (an Ajax click filtered the table on both implementations). What broke it was the namespace, not the library API.

The PrimeFaces 16 documentation page on dependencies still lists the Faces runtimes "2.3, 3.0, 4.0" while the 16.0.0 migration guide says Faces 2.x and javax.* support were removed and the baseline is Faces 4.0; the jar agrees with the guide (all its classes use jakarta.faces). Trust the jar and the guide. The version matrix already carries a warning about the dependencies page.

pom.xml: the PrimeFaces dependency along the route
<!-- Java EE 8, Faces 2.3 (stage 1) -->
<dependency>
  <groupId>org.primefaces</groupId>
  <artifactId>primefaces</artifactId>
  <version>11.0.0</version>
</dependency>

<!-- Jakarta EE 9 or 10, Faces 3.0 or 4.0 (stages 2 and 3): same major line, classifier added -->
<dependency>
  <groupId>org.primefaces</groupId>
  <artifactId>primefaces</artifactId>
  <version>14.0.18</version>
  <classifier>jakarta</classifier>
</dependency>

<!-- Jakarta EE 11, Faces 4.1 (stage 4): one artifact -->
<dependency>
  <groupId>org.primefaces</groupId>
  <artifactId>primefaces</artifactId>
  <version>16.0.0</version>
</dependency>

The PrimeFaces Extensions library followed the same split: Maven Central has a plain and a jakarta-classifier jar for 13.0.0 through 15.0.18, and one jar for 16.0.0. A large part of the book-era components moved or were removed; the table is in What Moved into PrimeFaces Core or Was Removed.

OmniFaces

OmniFaces follows the platform with three lines, and the first of them is javax:

  • 3.x targets Faces 2.3 on Java 8. omnifaces-3.14.4.jar, the version of the legacy Bookshelf, refers to javax/faces/context/PartialViewContext. It is the line that failed in the first row of the table in What Happens If You Only Upgrade the Runtime.

  • 4.x targets Faces 3.0 and 4.0 on Java 11 (last release 4.7.15).

  • 5.x targets Faces 4.1 on Java 17 (5.5.5 is the current release; 5.0 is its upper Faces version).

The OpenRewrite recipe set contains OmniFacesNamespaceMigration, which rewrites http://omnifaces.org/ui and http://omnifaces.org/functions to the single URN omnifaces of OmniFaces 5. It was not exercised on the Bookshelf, which has no OmniFaces tags; the namespace and the o: functions are described in the OmniFaces page. On a Jakarta EE 10 server such as TomEE 10.2.0 (Faces 4.0) use the 4.x line, because 5.x needs Faces 4.1.

Faces Implementations and Servers

The implementation and the server choose which Faces you run; the library choice has to agree with them. The artifact name does not tell you the namespace. Mojarra’s artifact org.glassfish:jakarta.faces carries versions 2.3.9 to 2.3.21 as well, and the class ApplicationImpl of 2.3.21 refers to javax/faces/application/Application; only the version number says javax or jakarta.

Table 13. Faces implementations by line (Maven Central, 2026-10-08)
Faces Mojarra MyFaces

2.3

org.glassfish:javax.faces 2.3.x, and org.glassfish:jakarta.faces 2.3.9 to 2.3.21 (still javax)

org.apache.myfaces.core:myfaces-api and myfaces-impl 2.3.x

3.0

org.glassfish:jakarta.faces 3.0.0 to 3.0.5

myfaces-api and myfaces-impl 3.0.0 to 3.0.4

4.0

org.glassfish:jakarta.faces 4.0.0 to 4.0.26

4.0.x (4.0.4)

4.1

org.glassfish:jakarta.faces 4.1.0 to 4.1.17

4.1.x (4.1.4)

The server rule is that the platform release fixes the Faces line: Jakarta EE 9 servers run Faces 3.0, EE 10 servers 4.0 and EE 11 servers 4.1. The Jakarta EE servers page lists the Faces implementation bundled by each product and the version it carries (for example, GlassFish 8.0.5 bundles Mojarra 4.1.17, while TomEE 10.2.0 bundles MyFaces 4.0.3, a Faces 4.0 line), and the version matrix joins them with the Java baselines. Moving from a server to Tomcat or Spring Boot is a different migration with its own pages in this section; the Tomcat and Weld page and the Spring Boot with JoinFaces page cover them.

Replacing Libraries That Ended

The books of the section recommend several libraries that no longer exist as maintained projects. Two pages of this section already hold the replacement tables, and this page does not repeat them:

What those pages do not cover are the parts of the book-era stack that were not component libraries:

Table 14. Other pieces of the era, and what to use now
Piece named in the books Use now

Seam 2 (conversations, bijection, security), and the Seam-based recipes

CDI scopes (@ViewScoped, @ConversationScoped, @ClientWindowScoped), Faces Flows for wizards (Scopes, Faces Flows)

PrettyFaces and other URL-rewriting add-ons

The automatic extensionless mapping of Faces 4.0 (Configuration), f:viewParam and f:viewAction (View Parameters and View Actions) and OmniFaces FacesViews (OmniFaces)

Weblets, ajax4jsf, Dynamic Faces (component resources and Ajax)

ResourceHandler and f:ajax (Resource Handling, Ajax)

Apache Shale (dialogs, remoting), the JSF Security project, Acegi

Faces Flows; Jakarta Security or, in a Spring application, Spring Security (Security)

JSFUnit and in-container JSF test harnesses

Browser tests with Selenium or Playwright, and Arquillian for in-container tests (the testing page)

Spring 2.5 DelegatingVariableResolver and the Spring beans in managed bean expressions

SpringBeanFacesELResolver in a Spring application, or JoinFaces (see below and Spring Boot with JoinFaces)

Portlet bridges (JSR 286 and 329) for portal deployments

None in Jakarta EE 10 or 11; the technology ended with the portal era, so plan a rewrite as a web application

The two Spring classes were checked against the library jars: spring-web-2.5.6.jar contains org.springframework.web.jsf.DelegatingVariableResolver, and spring-web 5.3.39, 6.0.0 and 7.0.0 contain org.springframework.web.jsf.el.SpringBeanFacesELResolver and no variable resolver. The configuration change in a Spring application is one element (documented; not run here, because the Bookshelf is not a Spring application):

faces-config.xml: from the Spring 2.5 variable resolver to the EL resolver
<!-- before: JSF 1.1 variable resolver -->
<application>
  <variable-resolver>org.springframework.web.jsf.DelegatingVariableResolver</variable-resolver>
</application>

<!-- after: EL resolver -->
<application>
  <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
</application>

The decision for any other ended library is the one the best-practices page turns into a rule: find the feature in Faces itself first (Ajax, file upload, selection, templating, push, flows and scopes cover most of the old recipes), then in PrimeFaces and OmniFaces, and only then look for a maintained library, with the adoption questions of Plugging In a Library.

Struts 1 to Faces

The older books treat Struts 1 integration and conversion to Faces as a main subject: Mann’s book integrates the two frameworks, and the book of Dudney and colleagues has a chapter on converting a Struts application. That material is historical. The Apache Struts project has declared Struts 1 end of life (its announcement counts the Struts 2 releases as of April 2013); the last Struts 1 release was 1.3.10 in December 2008, and the announcement says that there is no drop-in replacement for it. (Struts 1, its migration and JSP in general are covered in the Struts 1 (Classic) & Migration page of the JSP & Struts section.)

One technical fact makes the old integration advice unusable on a current platform: struts-core 1.3.10, the newest release on Maven Central, refers to javax/servlet/http/HttpServlet in its ActionServlet, so it does not run on a Servlet 5 or newer container such as Tomcat 10 or later. Struts 1 and Faces 4 cannot share a WAR on a Jakarta EE 9 or newer server unless the Struts jars are transformed (the Eclipse Transformer of Eclipse Transformer: Names in Binaries, up to Jakarta EE 9 could do it; not run). The practical plan for a Struts 1 application is therefore to convert it screen by screen while it still runs on its old platform, as stage 1 of the route, or to run old and new applications as two deployments behind a reverse proxy that routes by URL.

The concepts map one to one in most places, and this is the table to use when converting. It is the author’s mapping from the two frameworks' concepts; no Struts 1 application was converted for this page.

Table 15. Struts 1 concepts and their Faces counterparts
Struts 1 Jakarta Faces 4.1

ActionServlet and struts-config.xml

FacesServlet, faces-config.xml and annotations

An Action class per URL (execute returns an ActionForward)

An action method of a backing bean, returning an outcome (Navigation)

ActionForward and <forward> entries

Outcomes, implicit navigation, faces-redirect=true and navigation rules

ActionForm with reset() and validate()

Properties of a @ViewScoped or @RequestScoped bean, with validators and Bean Validation (Validators, Bean Validation)

ActionErrors and ActionMessages

FacesMessage and h:messages (Messages)

The Commons Validator rules in validation.xml

Bean Validation constraints and the standard validators

Tiles definitions

Templates with ui:composition, ui:define, ui:insert (Templating)

The Struts tag libraries (html:text, bean:write, logic:iterate)

h:inputText, expressions in template text, ui:repeat and h:dataTable

Message resources (ApplicationResources.properties)

Resource bundles declared with <resource-bundle>, read as #{msg['key']}

One request, one action, no component state

A component tree restored on every postback, with view state (Architecture and Component Model)

The last row is the one that costs time. Struts is action-based: a URL names an action, and the page is a template. Faces is component-based: a URL names a view, a form posts back to the same view, and the server keeps state for it. A Struts screen with several buttons becomes one view with several action methods, not several URLs. Plan the conversion by screens, not by files.

Proving the Migration: A Smoke Test

The most dangerous failures of this page are the silent ones, so the end of the migration needs a test that fails on them. The script below makes one check for each: books listed (the bean was injected), the project stage (the parameter was read), no missing script resource, no jsf. left in the page, faces.js served, the EL resolver working, and an Ajax search that filters the table. It needs only curl and a running application, and it works on any server and on both implementations.

smoke.sh: one check per silent failure
#!/bin/sh
# smoke.sh http://localhost:8080/bookshelf
BASE=${1:?base url}
JAR=$(mktemp)
fail=0
check() { if [ "$2" = "ok" ]; then echo "ok    $1"; else echo "FAIL  $1"; fail=1; fi; }

page=$(curl -s -c "$JAR" -b "$JAR" "$BASE/index.xhtml")
echo "$page" | grep -q 'Dune' && check "bean injected, books listed" ok || check "bean injected, books listed" no
echo "$page" | grep -q 'stage=Development' && check "jakarta.faces.PROJECT_STAGE read" ok || check "jakarta.faces.PROJECT_STAGE read" no
echo "$page" | grep -q 'RES_NOT_FOUND' && check "no missing script resource" no || check "no missing script resource" ok
echo "$page" | grep -q 'jsf\.' && check "no jsf.* left in the page" no || check "no jsf.* left in the page" ok
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/jakarta.faces.resource/faces.js.xhtml?ln=jakarta.faces")" = 200 ] \
  && check "faces.js served" ok || check "faces.js served" no
curl -s "$BASE/legacy.xhtml" | grep -q 'Bookshelf' && check "EL resolver supplies appName" ok || check "EL resolver supplies appName" no

state=$(echo "$page" | sed -n 's/.*name="jakarta.faces.ViewState"[^>]*value="\([^"]*\)".*/\1/p' | head -1)
answer=$(curl -s -c "$JAR" -b "$JAR" -H 'Faces-Request: partial/ajax' \
  --data-urlencode 'searchForm=searchForm' --data-urlencode 'searchForm_SUBMIT=1' \
  --data-urlencode 'searchForm:query=snow' --data-urlencode "jakarta.faces.ViewState=$state" \
  --data-urlencode 'jakarta.faces.source=searchForm:search' --data-urlencode 'jakarta.faces.partial.ajax=true' \
  --data-urlencode 'jakarta.faces.partial.execute=searchForm' --data-urlencode 'jakarta.faces.partial.render=results' \
  --data-urlencode 'jakarta.faces.behavior.event=action' --data-urlencode 'searchForm:search=' \
  "$BASE/index.xhtml")
echo "$answer" | grep -q 'Snow Crash' && ! echo "$answer" | grep -q 'Dune' \
  && check "Ajax search filters the table" ok || check "Ajax search filters the table" no
rm -f "$JAR"
exit $fail

Against the migrated Bookshelf the script printed ok seven times and exited with 0 on Mojarra 4.1.17 and on MyFaces 4.1.4. Against the state with the five leftovers of What Happens If You Only Upgrade the Runtime it exited with 1: four of the seven checks failed (the page itself answered 500, so the first two checks failed together). The line searchForm_SUBMIT=1 is needed by MyFaces only; Mojarra ignores it. Add the script to the build pipeline; it is the cheapest regression test the migration can leave behind. The Security and testing pages hold the browser-level tests.

The finished application is the Bookshelf as listed in this page: BookService, CatalogueBean, QueryReader, AppNameResolver, AuditPhaseListener (unchanged but for the import), the three views, the three descriptors and the pom. It builds with mvn package (maven.compiler.release 21), deploys to Tomcat 11.0.26 with Weld 6.0.4.Final, runs on Mojarra 4.1.17 and MyFaces 4.1.4, and compiles without warnings against the Faces 5.0 milestone (Preparing for Faces 5.0).

Preparing for Faces 5.0

Faces 5.0 is not released. The specification page marks it as under development with Java SE 21 as the minimum, and the latest milestone of the API is 5.0.0-M9 (2026-09-18). What the final release contains can still change, so this section describes a method and the milestone’s data, not a promise.

The direction is stated on the versions page: the removal of everything deprecated for removal since 4.0, of the old XML namespaces, of composite:extension and of the custom scope events, plus a modernisation of the API with generics and enums. That makes the preparation concrete, because the things to avoid are already marked:

  1. Compile with -Xlint:removal against 4.1. Every [removal] warning is something 5.0 plans to delete. A class using @FacesConfig(version = ...), the JSF_SCRIPT_* constants and ViewHandler.DEFAULT_SUFFIX gave five warnings against 4.1.2.

  2. Compile against the milestone in a separate step. The same class gave five errors against 5.0.0-M9 (cannot find symbol version(), Version, JSF_SCRIPT_LIBRARY_NAME, JSF_SCRIPT_RESOURCE_NAME, DEFAULT_SUFFIX). The migrated Bookshelf compiled against it with no warning and no error.

  3. Replace the old namespaces now. They still work in 4.1 (XML Namespaces: java.sun.com and xmlns.jcp.org to the URNs (Faces 4.0)) and are planned to go in 5.0.

  4. Check the libraries. Whether PrimeFaces 16, OmniFaces 5 and your own extensions run on a 5.0 milestone was not run here.

Compiling the application against the 5.0 milestone, apart from the normal build
# once: get the milestone API and the compile classpath of the project
mkdir -p target/faces5
curl -s -o target/faces5/jakarta.faces-api-5.0.0-M9.jar \
  https://repo1.maven.org/maven2/jakarta/faces/jakarta.faces-api/5.0.0-M9/jakarta.faces-api-5.0.0-M9.jar
mvn -q dependency:build-classpath -DincludeScope=compile -Dmdep.outputFile=target/cp.txt

# the milestone first on the class path, so it wins over the 4.1 classes inside jakartaee-web-api
mkdir -p target/faces5-classes
javac -Xlint:removal -d target/faces5-classes \
  -cp "target/faces5/jakarta.faces-api-5.0.0-M9.jar:$(cat target/cp.txt)" \
  $(find src/main/java -name '*.java')

Reading the milestone API jar against the 4.1.2 jar shows the direction in detail. In 5.0.0-M9 these public API elements are absent (present and deprecated for removal in 4.1.2):

  • FacesConfig.version() and FacesConfig.Version;

  • ResourceHandler.JSF_SCRIPT_LIBRARY_NAME and JSF_SCRIPT_RESOURCE_NAME;

  • ViewHandler.DEFAULT_SUFFIX and DEFAULT_SUFFIX_PARAM_NAME;

  • StateManager.PARTIAL_STATE_SAVING_PARAM_NAME and FULL_STATE_SAVING_VIEW_IDS_PARAM_NAME;

  • the alt, autocomplete, maxlength, readonly and size accessors of HtmlInputFile, and alt of HtmlInputSecret;

  • ActionSource2, ActionSource2AttachedObjectHandler, ActionSource2AttachedObjectTarget, PostConstructCustomScopeEvent and PreDestroyCustomScopeEvent.

And the milestone newly marks as deprecated for removal (since = "5.0"): ConfigurableNavigationHandler and its wrapper, ScopeContext, two constructors of NavigationCase, the FacesMessage.SEVERITY_* constants, StateManager.STATE_SAVING_METHOD_CLIENT and STATE_SAVING_METHOD_SERVER, ViewHandler.getBookmarkableURL and getRedirectURL (and their wrapper methods), the processSaveState and processRestoreState family of UIComponent, UIForm.prependId, and the managed element of @FacesConverter, @FacesValidator and @FacesBehavior. These come from scanning the milestone jar and may change before the release; prependId and the navigation handler matter to many applications, so read them as a reason to look at your own use, not as a rule. Note also that the class files of 5.0.0-M9 are Java 17 class files (version 61) although the specification page asks for Java SE 21.

Mojarra vs MyFaces During a Migration

The two implementations agreed on almost everything above, and the differences are mostly in what they say, not in what they do:

  • Both ignored the old context parameters, the <managed-bean> and <variable-resolver> entries and an unannotated @ManagedProperty; both accepted the old namespaces and the old descriptor versions; both failed the same way when a library or a class still used javax (Mojarra with NoClassDefFoundError for the class it was loading, MyFaces with Class javax.faces.webapp.FacesServlet not found and a failed StartupServletContextListener).

  • Missing script resource: Mojarra renders <script src="RES_NOT_FOUND"> and logs nothing; MyFaces renders nothing and logs a warning with the resource and library names.

  • A request mapped to a missing view (*.jsp with no .xhtml sibling): Mojarra answered 404, MyFaces 500 with InvalidFileException.

  • Startup noise: on an embedded Tomcat without Jasper, MyFaces 4.1.4 logs SEVERE: Class org.apache.jasper.servlet.JspServlet not found while starting, after looking up the JSP servlet class. It is harmless here (JSP support is gone) and is not the reason the application fails when it fails; the actual cause is further down the log.

  • Hand-built Ajax posts: MyFaces needs the hidden form_SUBMIT=1 field in a request that you assemble by hand, as the smoke test does; Mojarra ignores it.

  • Generated markup: the Ajax button’s onclick is mojarra.ab(...) on Mojarra and myfaces.ab(...) inside faces.util.chain on MyFaces. Tests must not depend on either.

The general comparison is in Versions and Implementations.

What Changed, Release by Release

Table 16. The migration-relevant changes of every release
Release Change that a migration has to handle

2.0

Facelets becomes the standard view declaration language and JSP is deprecated. Annotations replace most registration in faces-config.xml. The native @ManagedBean facility is introduced. f:ajax and jsf.js appear.

2.2

CDI @ViewScoped (javax.faces.view) and @FlowScoped. HTML5 pass-through attributes and elements (http://xmlns.jcp.org/jsf/passthrough, http://xmlns.jcp.org/jsf). The XML namespaces move from java.sun.com to xmlns.jcp.org. The specification announces that the javax.faces.bean package will be deprecated.

2.3

@ManagedBean and the javax.faces.bean scopes are deprecated; @FacesConfig activates the CDI features; @ManagedProperty returns as a CDI qualifier; Faces artifacts become injectable. Java 8.

3.0

The javax to jakarta rename, nothing else. Jakarta EE 9.

4.0

JSP, @ManagedBean and its scopes, MethodBinding, ValueBinding, ResourceResolver, the CURRENT_COMPONENT constants and the deprecated StateManager methods are removed. The URNs replace the namespace URLs. faces.js replaces jsf.js. Extensionless mapping, @ClientWindowScoped, programmatic Facelets. Java 11. Jakarta EE 10.

4.1

Full state saving, ActionSource2, the custom-scope events and cc:extension are deprecated for removal; the UIComponent.bindings field is removed; @Inject Flow, UUIDConverter, rowStatePreserved on ui:repeat. Java 17. Jakarta EE 11.

5.0 (upcoming)

Planned removal of what is deprecated for removal; removal of the old XML namespaces; API modernisation. Java 21. Jakarta EE 12. Milestones only.

Where the Books Are Outdated: Every Statement in One Place

The eight books of the section’s bibliography date from 2004 to 2015 and describe JSF 1.0 to 2.2 and PrimeFaces 2 to 5. Their ideas are mostly sound, which is why they are listed; their code is not, and the section’s pages each say where. This part gathers all of those statements in four views, so that a team migrating from a book-era code base can look up anything it meets. The first view is the baseline, the second is organised by area (the statements that apply to every book), the third by book, and the fourth by page: it names every page that has such a section and links it, with what that section says in short. The statements are paraphrased from the section’s own pages and checked on the pages they come from; no text of any book is reproduced. On any difference between a book and the official documentation, the documentation wins.

The Baseline the Statements Are Measured Against

  • Faces: Jakarta Faces 4.1 (Jakarta EE 11, Java SE 17 or later, API 4.1.2). Faces 4.0 (Jakarta EE 10, Java 11) is still widely deployed. Faces 5.0 (Jakarta EE 12, Java 21) is not released; its latest milestone is 5.0.0-M9.

  • Implementations: Eclipse Mojarra 4.1.17 (4.0.26 on the 4.0 line) and Apache MyFaces Core 4.1.4 (4.0.4).

  • Ecosystem: PrimeFaces and PrimeFaces Extensions 16.0.0, OmniFaces 5.5.5, JoinFaces 6.1.1 on Spring Boot 4.1.1, myfaces-quarkus 4.1.4 and quarkus-primefaces 4.16.0.

  • Servers: GlassFish 8.0.5, WildFly 41.0.1, Payara 7.2026.9, Open Liberty 26.0.0.10 (feature faces-4.1), TomEE 10.2.0, and Tomcat 11 with Weld.

  • The Jakarta EE Tutorial at its current address documents Jakarta EE 10 and therefore Faces 4.0. Its pages on configuring managed beans still describe what Faces 4.0 removed, so they must not be followed.

  • The MyFaces "getting started" page is outdated (Faces 2.3 and javax); link and read the Faces 4.1 pages of the MyFaces documentation (https://myfaces.apache.org/#/core41) instead.

By Area

Each row says what the books teach (paraphrased), what holds today, and the release in which it changed. The last column names the page of this section that shows the current form.

Table 17. Outdated statements by area
Area What the books teach Today Since Where

View technology

JSP views, JSP tag handlers (UIComponentELTag) and tag library descriptors (Mann, Dudney et al., Jacobi and Fallows, parts of Burns and Schalk and of Leonard).

Facelets XHTML. JSP support was deprecated in 2.0 and removed in 4.0, with the tag classes.

2.0, 4.0

JSP to Facelets; Facelets and XHTML Views

View technology

Standalone Facelets 1.x: a library JAR, com.sun.facelets classes, a custom ViewHandler, the facelets.* parameters, the jsfc attribute (Aranda and Wadia).

Facelets is part of Faces and the default VDL; parameters are jakarta.faces.FACELETS_*; jsfc is replaced by pass-through elements.

2.0, 2.2

Templating; HTML5-Friendly Markup

Namespaces and packages

javax.faces.* in every book.

jakarta.faces.*.

3.0

javax to jakarta (Faces 3.0, Jakarta EE 9)

Namespaces and packages

http://java.sun.com/jsf/* and http://xmlns.jcp.org/jsf/*.

The URNs jakarta.faces.html, jakarta.faces.core, jakarta.faces.facelets, jakarta.faces.composite, jakarta.faces.passthrough, jakarta.faces and jakarta.tags.core. The old ones still work in 4.1 and are planned to go in 5.0.

4.0, 5.0 planned

XML Namespaces: java.sun.com and xmlns.jcp.org to the URNs (Faces 4.0)

Beans

@ManagedBean, javax.faces.bean.*Scoped, <managed-bean>, @ManagedProperty from javax.faces.bean.

CDI @Named with the scopes of jakarta.enterprise.context and jakarta.faces.view.ViewScoped; jakarta.faces.annotation.ManagedProperty only together with @Inject.

2.3 (deprecated), 4.0 (removed)

Managed Beans to CDI

Core APIs

MethodBinding, ValueBinding, VariableResolver, PropertyResolver, ResourceResolver, the deprecated StateManager methods.

Value and method expressions, ELResolver, ResourceHandler, StateManagementStrategy.

4.0

Removed and Deprecated APIs

Core APIs

Full state saving as the normal model.

Partial state saving; full state saving is deprecated for removal.

2.0, 4.1

State Saving and Stateless Views

Core APIs

jsf.js and jsf.ajax; @FacesConfig needed to turn on CDI features.

faces.js and faces.ajax; @FacesConfig is no longer needed to activate CDI.

4.0

jsf.js to faces.js

Ajax and resources

Hand-written XMLHttpRequest, Weblets, ajax4jsf and Dynamic Faces (Jacobi and Fallows, Leonard).

f:ajax, the standard partial response, ResourceHandler, h:outputScript and h:outputStylesheet.

2.0

Ajax; Resource Handling

Libraries

RichFaces, ICEfaces 1.x, Tomahawk, Trinidad, Mojarra Scales, Seam 2, Apache Shale, JSFUnit, the JSF Security project, Acegi, PrettyFaces (Leonard, Burns and Schalk).

All ended. PrimeFaces, OmniFaces, the platform features, Jakarta Security, and Arquillian, Selenium or Playwright.

n/a

Replacing Libraries That Ended; Replacing Them

Integration

Struts 1 integration and migration (Mann, Dudney et al.).

Struts 1 is end of life; convert to Faces, screen by screen.

n/a

Struts 1 to Faces

Integration

JSR 301 and 329 portlet bridges (Burns and Schalk, appendix A).

No portlet bridge in Jakarta EE 10 or 11.

n/a

Replacing Libraries That Ended

Integration

Spring 2.5 DelegatingVariableResolver (Leonard).

SpringBeanFacesELResolver, or JoinFaces in Spring Boot.

Spring 3.2 on, Faces 4.0

Replacing Libraries That Ended

PrimeFaces books

RequestContext for callback parameters, scripts and the dialog framework.

PrimeFaces.current().

n/a

PrimeFaces Ajax, Forms and Validation

PrimeFaces books

ThemeRoller themes and the theme switcher; jQuery UI icon classes.

The PrimeFaces themes (Saga and the others) and PrimeIcons; ThemeRoller themes do not render since 15; p:themeSwitcher was removed in 11.

11, 15

PrimeFaces: Getting Started and Themes

PrimeFaces books

Legacy jqPlot charts; p:push on Atmosphere; p:calendar.

Chart.js models (p:chart); f:websocket or o:socket (push was removed in 7.0); p:datePicker.

10, 13, 7

Files, Media, Charts and Drag and Drop; WebSocket Push

PrimeFaces Extensions book

Extensions 1.2.1 components (CKEditor, exporter, timeline, spotlight and others).

Several moved into PrimeFaces core or were removed.

10 to 12

What Moved into PrimeFaces Core or Was Removed

Render kits

XUL and HTC render kits, and switching render kits at run time to target them (Jacobi and Fallows).

Dead browser technologies. The RenderKit mechanism itself is current.

n/a

Render Kits

Source: Faces 4.1 specification, Appendix B and the pages named in the last column

What No Book Covers

The books are older than every feature below, so a team that follows them will not find these and will often rebuild them by hand. Each is detailed on its own page and listed per release on the versions page: Faces Flows, CDI @ViewScoped, HTML5 pass-through markup, resource library contracts, stateless views, h:inputFile, f:viewAction, protected views and the ClientWindow (2.2); f:websocket and @Push, CDI injection of Faces artifacts, managed converters and validators, f:validateWholeBean, search expressions, f:importConstants and java.time support (2.3); the jakarta namespace (3.0); programmatic Facelets, the extensionless mapping, @ClientWindowScoped, SameSite cookies and f:selectItemGroups (4.0); UUIDConverter, rowStatePreserved on ui:repeat and the injectable Flow (4.1); and Jakarta Security, JoinFaces and Quarkus.

By Book

Table 18. The weight of each book today, and where it is outdated
Book Weight What is outdated, in short

Ed Burns and Chris Schalk, JavaServer Faces 2.0: The Complete Reference (McGraw-Hill, 2010)

High

javax names, the java.sun.com namespaces, @ManagedBean, JSP as an alternative view technology, MethodBinding and ValueBinding, a hand-written StateManager, jsf.js, Bean Validation 1.0, portlet bridges; silent on flows, f:viewAction, f:websocket, search expressions and ClientWindow. The concepts (lifecycle, component model, extension points) hold.

Mert Çalışkan and Oleg Varaksin, PrimeFaces Cookbook, Second Edition (Packt, 2015)

Medium

PrimeFaces 5.1 and 5.2 on JSF 2.2: javax, RequestContext, ThemeRoller and the theme switcher, Font Awesome in the jar, Grid CSS, jqPlot charts, p:calendar, p:push, Commons FileUpload, load(first, pageSize, ...) lazy models, SortOrder sorting, selection attributes, iText 2 and POI 3.7 exporters.

Bruno Aranda and Zubin Wadia, Facelets Essentials (Apress, 2008)

Low to medium

Facelets 1.x as a separate library: FaceletViewHandler, facelets.* parameters, jsfc, the java.sun.com namespaces, facelet-taglib with a DTD. The ui:* tags are as described.

Sudheer Jonna, Learning PrimeFaces Extensions Development (Packt, 2014)

Low to medium

Extensions 1.2.1 on PrimeFaces 4.0: namespaces, setup, and many components that moved to core or were removed (CKEditor, exporter, timeline, spotlight, Ajax error handling).

Anghel Leonard, JSF 2.0 Cookbook (Packt, 2010)

Low

About half of the recipes rely on RichFaces 3.3, Tomahawk, Trinidad, Seam 2 and Spring 2.5; the file upload, tables, Ajax, security, testing (JSFUnit) and persistence recipes are replaced by h:inputFile, f:ajax, Jakarta Security, Arquillian and CDI.

Kito D. Mann, JavaServer Faces in Action (Manning, 2004)

Low

JSF 1.0 and 1.1 with JSP and Struts 1; faces-config.xml beans and navigation; JSP-based custom components; client-state advice; development on Tomcat 5. The component and renderer split and the lifecycle are as described.

Bill Dudney, Jonathan Lehr, Bill Willis and LeRoy Mattingly, Mastering JavaServer Faces (Wiley, 2004)

Very low

JSF 1.0 and JSP 2.0 with Struts; the Ant build; the design-patterns view (MVC, Composite, Strategy, Observer, Decorator) remains useful.

Jonas Jacobi and John R. Fallows, Pro JSF and Ajax (Apress, 2006)

Low

JSF 1.1 and 1.2 rich components: hand-written Ajax, Weblets, XUL and HTC render kits, JSP tag handlers, f:view calling writeState, client state as a clustering answer. Render-kit theory holds.

By Page

Every page of this section that draws on a book has its own "Where the Books Are Outdated" part. The list below links each of them with a summary; follow the link for the current form, the version in which it changed and the example. A page that is not listed has no such part.

  • Introduction and History: the books use JSP views and tag handlers, javax.faces, the java.sun.com and xmlns.jcp.org namespaces and @ManagedBean; treat Struts 1 integration as a main topic; describe RichFaces, Tomahawk, Trinidad and Seam 2 as the ecosystem; and present JSR 127 to 314 as the state of the art.

  • Versions and Implementations: javax.faces everywhere, JSP and standalone Facelets, @ManagedBean, Sun’s reference implementation and MyFaces 1.x, jsf.js, MethodBinding, ResourceResolver; silent on flows, CDI @ViewScoped, pass-through markup, contracts, stateless views and the Jakarta EE profiles.

  • Getting Started: a first application as a .jsp with <f:view> and *.jsf mapping, beans in faces-config.xml, javax names; today Facelets, CDI, and a mandatory CDI container (Weld on Tomcat).

  • Project Setup: JSP wiring, javax.faces parameters, mandatory faces-config.xml with <managed-bean>, DTD descriptors, standalone Facelets, hand-copied jsf-api.jar and jsf-impl.jar, Tomcat 5 and Ant; the Tutorial’s managed-bean pages must not be followed.

  • Architecture and Component Model: JSP tag handlers and TLDs, registration in XML, hand-written saveState, ValueBinding, XUL and HTC render kits, @ManagedBean, subclassing implementation classes, Weblets.

  • Request Processing Lifecycle: javax names, JSP views building the tree while rendering, hand-written Ajax, immediate read as "skip validation", no system events, @ManagedBean and ValueBinding in examples.

  • Facelets and XHTML Views: Facelets 1.x set-up and parameters, the java.sun.com namespaces, JSP views as an alternative, recipes that rely on RichFaces or Tomahawk tags.

  • Templating: the FaceletViewHandler and facelets.* parameters, the namespace changed twice, the javax rename, jsfc; silent on composite components, contracts and Ajax.

  • HTML5-Friendly Markup: the jsfc attribute of Facelets 1.x is the precursor of pass-through elements and was superseded by them.

  • JSTL and Build Time versus Render Time: advice from the JSP era that relied on JSTL and Faces tags running in one pass no longer applies; Leonard’s JSTL material is the same distinction in other words.

  • Expression Language: EL 1.2 and 2.1 (no method parameters, no lambdas), @ManagedBean with <managed-bean>, ValueBinding and VariableResolver, JSP ${}, javax.el.

  • HTML Components: the component reference is the part that aged best; missing type on h:inputText, layout="list", f:selectItemGroups, h:commandScript; Tomahawk and RichFaces tags; p: components standing in for standard ones.

  • Core Tags: javax and the 1.x namespace, f:view, f:verbatim and f:subview of JSP, managed beans with f:setPropertyActionListener, SelectItem lists, MethodBinding; no f:event, f:metadata, f:selectItemGroups or f:resetValues.

  • Data Tables and Iteration: JSP and javax, @ManagedBean tables, ArrayDataModel and friends without CollectionDataModel, sorting and paging through RichFaces, Tomahawk or Trinidad, ui:repeat with jsfc, binding with getRowData(), no Ajax.

  • File Upload and Download: upload recipes on Tomahawk and RichFaces components or a Commons FileUpload filter; their newest releases are from 2012 and 2016 and predate jakarta.faces; the era’s recipes lack content-type sniffing, generated file names and CSV formula escaping.

  • Converters: javax.faces.convert.Converter and ids, registration in XML, java.util.Date instead of java.time, converters with parameters via managed beans, entity converters that serialise or call a static DAO, hand-written refresh; no UUIDConverter.

  • Validators: javax names and ids, static lookups instead of injection, JSP tag handlers, MethodBinding, hand-written regular-expression and required validators, cross-field validation through other-component lookup or Tomahawk, hand-written client-side checks.

  • Bean Validation: Bean Validation 1.0 and javax.validation versus 3.1, no f:validateWholeBean, @ManagedBean and <validator> registrations, recipes relying on ended libraries, PrimeFaces client-side validation as an addition.

  • Messages and Internationalisation: javax.faces.Messages and its keys, f:loadBundle and managed beans for text, session-scoped locale switchers, native2ascii escapes, RequestContext.

  • Events and Listeners: no system events in the 1.x books, immediate and renderResponse() tricks for dependent drop-downs, MethodBinding listeners, @ManagedBean listener beans, javax.faces.event.

  • Navigation: rules for everything, .jsp view ids, every link a postback, <view-param>, NavigationHandler subclasses and MethodBinding, no Post/Redirect/Get by default, javax names, Struts-style navigation.

  • View Parameters and View Actions: no f:viewAction (a preRenderView listener instead), legacy beans and namespaces, pretty URLs from PrettyFaces, errors answered as 200, hand-built redirects.

  • Flash and Post/Redirect/Get: JSF 1.x books predate the flash and used a <redirect/> case, parameters or a session bean; the 2.0 books use javax and @ManagedBean.

  • Backing Beans and CDI: beans in faces-config.xml (1.x), @ManagedBean and @ManagedProperty with setters (2.x), Spring and EJB integration through resolvers or JNDI, static FacesContext.getCurrentInstance() everywhere, session beans as the default.

  • Scopes: @ManagedBean and the javax.faces.bean scopes, only three scopes, Seam 2 conversations, "session scope is fine for a form", javax.faces.bean.ViewScoped, the Tutorial’s "Allowable Connections" table.

  • Faces Flows: no flows at all; session beans, Seam 2 conversations, faces-config.xml navigation rules and @ManagedBean for wizards.

  • Multi-Window Support: nothing knows that tabs exist; state in session beans; hand-made tokens and window.name scripts; javax names for the 2.2 client window; session-scoped wizard beans.

  • Ajax: hand-written Ajax, Weblets and ajax4jsf; jsf.js, jsf.ajax.request and javax.faces.partial.*; older than delay, resetValues, h:commandScript and search expressions; the libraries ended.

  • Search Expressions: the PrimeFaces Cookbook treats process and update as a library feature; the core specification owns the facility since 2.3 and the older books show only four keywords.

  • WebSocket Push: no framework push before 2.3 (polling, XMLHttpRequest, libraries); PrimeFaces Push on Atmosphere was removed in 7.0; use f:websocket or o:socket.

  • Composite Components: javax packages, the java.sun.com/jsf/composite namespace, @ManagedBean; Facelets Essentials predates composites; the Composite design pattern of Dudney is a different thing; JSP tag handler components are gone.

  • Tag Files and Tag Libraries: Facelets 1.x taglib DTD and registration parameters, com.sun.facelets handlers, JSP tag handlers with TLDs, "composition components", the Tutorial’s validator-class element.

  • Custom Components and Renderers: JSP tag handlers and TLDs, javax, hand-written saveState and restoreState, XML registration, Weblets and Ajax by hand, XUL and HTC render kits; the design-patterns chapters stay accurate.

  • Client Behaviors: nothing before 2.0 knew behaviors (renderers, scripts and libraries did the job); javax; the XUL and HTC kits; PrimeFaces books describe libraries that moved on.

  • Resource Handling: Weblets as the answer for JAR resources (configuration file, weblet:// URLs, a view handler and a phase listener) and its standard replacements; library="javax.faces" with jsf.js; the old ResourceHandlerWrapper constructor rule.

  • Resource Library Contracts: contracts did not exist (2.2); templates chosen by literal path; skinning through render kits and library skins; PrimeFaces theme switchers are a separate mechanism.

  • Configuration and Project Stages: javax.faces.* parameter names (ignored silently), <managed-bean>, the JSP-era mapping and suffix, DTDs and old schemas, facelets.* parameters, Mojarra 1.x and 2.x parameters, standalone Facelets development mode, secrets in configuration.

  • Extending Faces: javax registration and namespace, VariableResolver, PropertyResolver, MethodBinding, ValueBinding and ResourceResolver, standalone Facelets view handlers, implementing whole interfaces instead of *Wrapper classes, Weblets, beans from the managed-bean facility.

  • Exception Handling: javax names, the no-argument wrapper constructor, navigating from a handler during Render Response (invalid since 2.3), no mention of per-implementation behaviour, library mechanisms of PrimeFaces 3 to 5, error pages that explain the failure.

  • State Saving and Stateless Views: the removed StateManager API, a readable client state shown as normal, "everything is saved", client state as a casual switch, JSP-era advice, hand-written component state, Seam-era workarounds, no stateless views, session memory as free.

  • Security: container-managed authentication per server (realms, j_security_check), the JSF Security project, Acegi and Seam 2; silent on protected views, SameSite, CSP, Sec-Fetch-* headers, modern password storage and OpenID Connect; listings that store clear-text passwords are historical.

  • Performance: harsher state-size warnings from full tree saving, STATE_SAVING_METHOD as the lever, ajax4jsf and Weblets, JSFUnit-era load testing, PrimeFaces 5 lazy models, partial submit and cache; none covers CDI @ViewScoped, OmniFaces 5, MyFaces view pooling or Production-stage defaults.

  • PrimeFaces: Getting Started and Themes: javax.faces on JSF 2.2, primefaces.js, ThemeRoller and the theme switcher, Font Awesome in the jar, Grid CSS, the head-resource ordering, client-side localisation.

  • PrimeFaces: Ajax, Forms and Validation: RequestContext, p:calendar, the client-side validation framework of its day, p:push, autoUpdate, p:watermark and the mobile components, namespaces.

  • PrimeFaces: Data Components: selection attributes, the old lazy load signature, sortBy and SortOrder, java.util.Date schedule models, iText 2 and POI 3.7 exporters, ui-icon classes, the raw tree-node constructors.

  • PrimeFaces: Panels, Menus and Dialogs: RequestContext for the dialog framework, ThemeRoller themes and jQuery UI icons, removed or renamed components and attributes, the old JSF base, p:push.

  • PrimeFaces: Files, Media, Charts and Drag and Drop: fileUploadListener and the upload limits on the component, Commons FileUpload, DefaultStreamedContent constructors, JCrop and the Flash camera, jqPlot charts, map set-up, drag and drop.

  • PrimeFaces Extensions: Extensions 1.2.1 and the old namespace, components that are gone or renamed, reworked components, the utility functions, security of rich-text input, a single downloadable release.

  • OmniFaces: none of the books covers OmniFaces 4 or 5; helper code around FacesContext, per-entity converters, @ManagedProperty parameters, hand-written exception and resource handlers, pretty URLs, PrimeFaces Push, cross-field validators, ended libraries, javax imports.

  • Other Component Libraries: the Cookbook’s recipes target RichFaces 3.3, PrimeFaces 2.0, Tomahawk, Trinidad, Seam 2 and Spring 2.5; render-kit chapters cover the rich-component frameworks of the JSF 1.x era; none mentions BootsFaces, Tobago or AdminFaces.

  • Jakarta EE Servers: Sun Java System Application Server, JBoss AS 4 to 7, WebLogic and Tomcat with sun-web.xml and jboss-web.xml, implementation JARs copied into the WAR, managed beans declared in faces-config.xml, Ant and hot deploy by copying files, no containers.

  • Tomcat and Weld: the 2004 and 2010 books list the JSF API and implementation JARs, jstl.jar, standard.jar and Commons JARs to copy into WEB-INF/lib or Tomcat’s shared directory, on Tomcat 5 or 6 and Servlet 2.5; no step for a CDI container (the managed-bean facility did the work), JSP as the view technology, deployment by copying files.

  • Spring Boot with JoinFaces: the Spring 2.5 DelegatingVariableResolver integration of Leonard’s cookbook, WAR-and-web.xml assumptions in place of properties, Struts 1 integration, hand-edited web.xml and manual JARs in the PrimeFaces books, and javax.faces imports in old Spring-and-Faces tutorials.

  • Quarkus: every book predates Quarkus; WAR deployment and *.jsf mappings, managed-bean entries, Spring, Seam and Hibernate recipes that assume a run-time scanning container, Development in web.xml, redeploy and JRebel hot deployment, no native or container-first packaging, PrimeFaces 4 and 5 theme jars.

  • Persistence and Services: Seam 2 conversations and entity homes, Spring 2.5 DelegatingVariableResolver (deprecated in Spring 3.2), Hibernate 3-era session filters and hbm.xml, @ManagedProperty injection, EJB 2.x entity beans, extended persistence contexts as view state.

  • Testing: JSFUnit and Faces Console, the container as the only place to test a bean, JMeter view-state extraction with the javax field name; still valid is the advice to keep converter and validator logic free of runtime Faces classes. None of the books covers Selenium, Arquillian or browser tests of PrimeFaces components.

  • Deployment and Clustering: Tomcat 5 and Servlet 2.5 containers, Ant builds, client state as the answer to clusters (true only with fixed keys, encryption and a home for view-scoped beans), the non-serialisable saved view of JSF 1.1, javax.faces.PROJECT_STAGE copied from samples, and no containers, proxies or probes.

  • Best Practices: the books teach much that holds (separate view from model, keep beans small, validate on the server, redirect after a post), but their examples use JSP, Facelets 1.x, javax.faces, @ManagedBean and session beans; they rely on hand-written Ajax and libraries without a Jakarta Faces release; they show the removed state-saving API; their security material rests on the container and on ended projects; and none covers flows, the CDI view scope, stateless views, WebSocket push, JoinFaces or Quarkus.

Bad Practices the Era Normalised

Separately from outdated APIs, the sources of the period show habits that this section does not reproduce. They are listed because a migrated code base usually still has them, and the migration is the moment to remove them; the right-hand column names the page that shows the correct form.

Table 19. Habits to remove while migrating
Habit common in the era’s code Correct form, and the page

Page state in session-scoped beans (memory growth, cross-tab bugs)

@ViewScoped, @FlowScoped or @ClientWindowScoped (Scopes, Multi-Window Support)

Business logic or queries in getters that run several times per request

Load in @PostConstruct, a view action or the action method (Managed Beans to CDI, Backing Beans and CDI)

binding to session-scoped beans

binding only to a request-scoped bean or none (Architecture and Component Model)

JSTL c:if and c:forEach used as if they ran at render time

rendered and ui:repeat (JSTL and Build Time versus Render Time)

immediate="true" used to "skip validation" without knowing the lifecycle consequences

Understand the phases first (Request Processing Lifecycle)

Secrets, passwords or database URLs written in faces-config.xml, web.xml or beans

The environment or a secret store (Security)

escape="false" on user-supplied text

Escaped output; sanitise if markup must be kept (Security)

State-changing h:link or GET requests, or switching off view state or CSRF protection

POST with view state; protected views (Security)

Client state saving without encryption and a MAC

Keep the defaults, fix the keys (State Saving)

Common Mistakes

  • Renaming only the imports. The javax.faces string also lives in parameters, XML, messages, ids, request parameters and resources, and a missed one fails without an error (javax to jakarta (Faces 3.0, Jakarta EE 9), What Happens If You Only Upgrade the Runtime).

  • Blind s/javax/jakarta/. It breaks javax.naming, javax.sql, javax.crypto and javax.annotation.processing, which Java SE still owns, and it produces jakarta.faces.bean and jakarta.faces.el, which do not exist in 4.x.

  • Trusting a green build from an automated tool. OpenRewrite reported success on a project that did not compile, and on JDK 26 skipped the most important class with a warning. Count the changed files and compile.

  • Keeping @ManagedProperty without @Inject. It compiles, it deploys and it fails on the first request with a null service.

  • Leaving the old descriptor entries. <managed-bean>, <variable-resolver> and the javax.faces.* parameters are ignored without a message; the stage falls back to Production.

  • Changing jsf.js in Java and XHTML but not in the JavaScript files, or the other way round. Search all three.

  • Jumping several PrimeFaces majors at once. The deprecations that told you what to change were in the releases you skipped (PrimeFaces).

  • Using the jakarta classifier on PrimeFaces 16. The artifact has none.

  • Assuming the artifact name tells the namespace. org.glassfish:jakarta.faces:2.3.21 is javax (Faces Implementations and Servers).

  • Mixing javax and jakarta jars in one WAR. A library that was not migrated fails at startup, in its own listener, before Faces is reached (What Happens If You Only Upgrade the Runtime).

  • Shipping with the Development stage because the migrated web.xml came from a development copy. Set Production for deployments (Project Stages).

  • Letting the temporary *.jsp mapping stay. Old URLs keep working and hide which page is served (JSP to Facelets).

  • Migrating without a test that fails on the silent errors. The smoke test of Proving the Migration: A Smoke Test is a start.

  • Copying a book’s example and "fixing" it by trial. Start from the current page of this section, not the book.

The pages that carry the detail this page links to: Versions and Implementations, Backing Beans and CDI, Facelets and XHTML Views, Project Setup, Configuration and Project Stages, Ajax, State Saving, Other Component Libraries, PrimeFaces Extensions, OmniFaces and Jakarta EE Servers. The Tomcat and Weld page, the Spring Boot with JoinFaces page and the Quarkus page cover moving the migrated application to those runtimes; Persistence and Services covers the data layer that old Seam, Spring and Hibernate recipes leave behind; the Testing page covers Arquillian, Selenium and Playwright for the regression tests; the Deployment and Clustering page covers the Production stage and what changes in a cluster; and the Best Practices page turns the habits of Bad Practices the Era Normalised into a checklist. The PrimeFaces side of an upgrade is in PrimeFaces: Getting Started and Themes and PrimeFaces: Data Components.

Outside the section: Spring Boot web UI frameworks for the legacy JoinFaces route, ASP.NET Web Forms for a comparable migration problem in another stack, and Starting a New Project for when a new application should not choose Faces at all. Struts and JSP are documented in the JSP & Struts section; the Playwright, Cypress and Selenium comparison is the subject of the planned issue #258, mentioned here by number only; GWT has its own GWT (Google Web Toolkit) section.