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.
| 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 |
3.0 |
Jakarta EE 9 (9.1 for Java 11) |
8 |
Everything named |
4.0 |
Jakarta EE 10 |
11 |
JSP support, |
4.1 |
Jakarta EE 11 |
17 |
Nothing is removed that Faces 4.0 still had. More API is deprecated for removal (full state saving, |
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. |
Source: Faces 4.1 specification, B.3: Changes between 3.0 and 2.3, B.2: Changes between 4.0 and 3.0 and B.1: Changes between 4.1 and 4.0
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.
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.
#!/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).
<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>
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)
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:
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);
}
}
Source: Faces 4.1 specification, B.2.1 and BalusC: What’s new in Faces 4.0 (removal of MethodBinding, ValueBinding and friends)
The remaining legacy files are the descriptors and the views:
<?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>
Source: Jakarta EE Tutorial: Configuring Jakarta Faces Applications (the Jakarta EE 10 tutorial)
<!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>
Source: Jakarta EE Tutorial: Facelets
<%@ 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.
| 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, |
Startup fails before Faces is reached. |
Library jars replaced by PrimeFaces 16.0.0 and OmniFaces 5.5.5, application classes and descriptors still |
Mojarra: |
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: |
The sources after the OpenRewrite recipe (see Tools: Eclipse Transformer and OpenRewrite), with the new libraries |
Does not compile: |
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:
| Leftover | Observed result | Message in the log |
|---|---|---|
|
|
None. |
|
|
None. |
|
|
None. |
|
The first request for |
|
|
Mojarra renders |
MyFaces: |
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.
| Kind of failure | Moment | What it looks like |
|---|---|---|
A removed API called from Java ( |
Build time |
A compile error, once the dependency is the Jakarta EE 11 API |
A |
Container start-up, before Faces is reached |
|
A descriptor entry or a parameter that 4.x no longer reads ( |
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 ( |
Render Response, when the head renderer asks for the resource |
Mojarra: |
An expression that no longer resolves ( |
Wherever the expression is read, usually Render Response |
An empty string |
A field that is no longer injected ( |
At the first use of the field: Render Response here, because the table reads |
HTTP 500 and a |
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:
| Place | Old | New |
|---|---|---|
Java packages |
|
The same names under |
Annotations in |
|
|
Context parameters in |
|
|
Component, converter, validator and renderer identifiers |
|
|
Messages |
The bundle |
|
Resources |
Library |
Library |
Request parameters |
|
|
Service files |
|
|
JNDI names read by Mojarra (4.0) |
|
|
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.
#!/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:
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.xmlbecameweb-app_5_0.xsd,version="5.0", thejakarta.facesparameter names and thejakarta.faces.webapp.FacesServletclass. -
faces-config.xmlbecameweb-facesconfig_3_0.xsd,version="3.0", and still namedLegacyVariableResolverand the<managed-bean>entry. -
beans.xmlbecamebeans_3_0.xsd. -
The bytecode of the application and of the two libraries now refers to
jakarta.faces.bean.ManagedBeanandjakarta.faces.bean.ViewScoped. -
The XHTML files were not changed: the
xmlns.jcp.orgnamespaces andlibrary="javax.faces"withjsf.jswere still there. -
The two library jars were transformed too (
primefaces-11.0.0.jarnow refers tojakarta/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:
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:
| File | Changed | Left untouched |
|---|---|---|
|
PrimeFaces 11.0.0 became 14.0.18 with the |
|
|
|
|
|
The types |
|
|
It now extends |
It does not implement the abstract methods of |
|
|
Nothing left to do. |
|
Namespace and |
|
|
Namespace and |
The |
|
Not touched: it kept the |
|
|
The namespaces became the URNs ( |
|
|
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.
| 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 |
|
|
|
Pass-through elements |
not available (the Facelets 1.x |
|
|
JSTL in XHTML |
|
|
|
Descriptors |
|
|
|
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 namespacehttp://xmlns.jcp.org/jsfand the JSTL URI rendered correctly withh:form,f:ajax,p:commandButton,jsf:idon a plain<form>,pt:placeholderandc:if; so did the same page with thejava.sun.comURIs. 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.xmlwith thexmlns.jcp.orgnamespace andversion="4.0"and afaces-config.xmlwithversion="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).
<!-- 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:
| In a JSP view | In a Facelets view |
|---|---|
|
|
|
Not needed. Use |
|
Delete it. Template text is part of the tree in the right order. The tag is gone. |
|
|
JSP comments |
|
Scriptlets, JSP declarations, |
Delete them. Use |
|
Usually |
A custom component as a JSP tag handler ( |
A composite component, a tag file, or |
The |
The mapping |
The legacy search page converts to this Facelets page, which is the search.xhtml of the migrated Bookshelf:
<!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.jspwhen a view/legacy.xhtmlexisted rendered that view on Mojarra and on MyFaces (PrimeFaces.settingsin the page showedviewId:'/legacy.xhtml'). The JSP file itself, with its<%@ taglib %>lines, was never read. -
The same request without a
legacy.xhtmlanswered 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.
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;
}
}
Source: Jakarta EE Tutorial: Managed Beans in Jakarta Faces Technology and OpenRewrite: Substitute removed Faces Managed Beans
Three details of this class are migration decisions, not typing:
-
@Namedwithout a name isbookService, the same rule@ManagedBeanused, so the expressions of the views need no change. The recipe wrote the name out (@Named("bookService")); both forms resolve. -
eager = truehas 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
Serializableand does not need to be. An application-scoped bean is never passivated. The view-scoped bean below is a different matter.
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();
}
}
Source: Jakarta Faces 4.1 API: ManagedProperty and ViewScoped
The bean differs from the recipe’s output in three ways that matter:
-
@Injectreplaces the field annotation and the setter. The recipe kept@ManagedProperty(value = "#{bookService}")with only its import changed. The annotation still exists in 4.1, injakarta.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 aNullPointerException(What Happens If You Only Upgrade the Runtime). Grep forManagedPropertyafter 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 intosearch(), 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 forDUNE(case-insensitive),snowandneuro, for thef:ajaxbutton 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.xmlfrom the Java EE 8 era was accepted. The unchanged file (xmlns.jcp.orgnamespace,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 modeallevery 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 unresolvedsettingsmanaged bean. CDI’s implicit bean archive rule lets a full application server discover annotated beans in an archive withoutbeans.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).
| Removed | Use instead |
|---|---|
|
CDI |
|
|
|
|
|
|
|
Facelets tag libraries, composite components and |
|
A |
|
|
|
|
|
A |
The standalone Facelets context parameters |
|
Source: BalusC: What’s new in Faces 4.0 (removal list with issue numbers), Faces 4.1 specification, B.2.1 and Jakarta Faces 4.1 API
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:
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.
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>
Source: Faces 4.1 specification, B.2.1 and the ELResolver section of Extending Faces
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:
| API | Since | Replacement |
|---|---|---|
|
4.0 |
Nothing: the element has had no effect since 4.0. Write |
|
4.0 |
|
|
4.0 |
|
|
4.0 |
Nothing: the API documentation says these attributes are not specified in HTML for these elements |
|
4.1 |
Partial state saving, the default (State Saving) |
|
4.1 |
The methods moved to |
|
4.1 |
CDI scope lifecycle events |
|
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.
| Faces 2.x (javax) | Faces 4.x |
|---|---|
|
|
|
|
|
|
|
|
Request parameters |
|
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.
<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.
<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.
<?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).
Source: Faces 4.1 specification, B.1 and Jakarta Servlet 6.1
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:
| Version | primefaces-X.jar (no classifier) |
primefaces-X-jakarta.jar |
Breaking changes in the migration guide (selection) |
|---|---|---|---|
12.0.0, 13.0.0 |
|
|
12: PrimeFlex 3 becomes the standard, Lightbox, Ring and ContentFlow deprecated. 13: Internet Explorer support removed; jqPlot |
14.0.0 |
|
|
Support for JSF 2.0, 2.1 and 2.2 removed; |
15.0.0 |
|
|
ThemeRoller themes unsupported; the extra themes moved to |
16.0.0 |
not published; a single artifact, |
not published |
JSF and |
Source: PrimeFaces migration guide 15.0.0 to 16.0.0, 14.0.0 to 15.0.0, 13.0.0 to 14.0.0, 12.0.0 to 13.0.0, 11.0.0 to 12.0.0, 10.0.0 to 11.0.0 and 8.0 to 10.0.0
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:commandButtonof 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.
<!-- 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 tojavax/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.
| Faces | Mojarra | MyFaces |
|---|---|---|
2.3 |
|
|
3.0 |
|
|
4.0 |
|
4.0.x (4.0.4) |
4.1 |
|
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:
-
Replacing Them maps RichFaces (including
a4j:), ICEfaces, Tomahawk and Trinidad components to standard Faces 4.1 and PrimeFaces 16, and the discontinued libraries section gives the evidence for each (last release, project status). -
What Moved into PrimeFaces Core or Was Removed maps the components of the PrimeFaces Extensions book (CKEditor, exporter, timeline, spotlight and others) to what replaced them.
What those pages do not cover are the parts of the book-era stack that were not component libraries:
| Piece named in the books | Use now |
|---|---|
Seam 2 (conversations, bijection, security), and the Seam-based recipes |
CDI scopes ( |
PrettyFaces and other URL-rewriting add-ons |
The automatic extensionless mapping of Faces 4.0 (Configuration), |
Weblets, ajax4jsf, Dynamic Faces (component resources and 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 |
|
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):
<!-- 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>
Source: Spring Framework API: SpringBeanFacesELResolver and the Spring Boot web UI frameworks page
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.
| Struts 1 | Jakarta Faces 4.1 |
|---|---|
|
|
An |
An action method of a backing bean, returning an outcome (Navigation) |
|
Outcomes, implicit navigation, |
|
Properties of a |
|
|
The Commons Validator rules in |
Bean Validation constraints and the standard validators |
Tiles definitions |
Templates with |
The Struts tag libraries ( |
|
Message resources ( |
Resource bundles declared with |
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.
#!/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:
-
Compile with
-Xlint:removalagainst 4.1. Every[removal]warning is something 5.0 plans to delete. A class using@FacesConfig(version = ...), theJSF_SCRIPT_*constants andViewHandler.DEFAULT_SUFFIXgave five warnings against 4.1.2. -
Compile against the milestone in a separate step. The same class gave five errors against
5.0.0-M9(cannot find symbolversion(),Version,JSF_SCRIPT_LIBRARY_NAME,JSF_SCRIPT_RESOURCE_NAME,DEFAULT_SUFFIX). The migrated Bookshelf compiled against it with no warning and no error. -
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.
-
Check the libraries. Whether PrimeFaces 16, OmniFaces 5 and your own extensions run on a 5.0 milestone was not run here.
# 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()andFacesConfig.Version; -
ResourceHandler.JSF_SCRIPT_LIBRARY_NAMEandJSF_SCRIPT_RESOURCE_NAME; -
ViewHandler.DEFAULT_SUFFIXandDEFAULT_SUFFIX_PARAM_NAME; -
StateManager.PARTIAL_STATE_SAVING_PARAM_NAMEandFULL_STATE_SAVING_VIEW_IDS_PARAM_NAME; -
the
alt,autocomplete,maxlength,readonlyandsizeaccessors ofHtmlInputFile, andaltofHtmlInputSecret; -
ActionSource2,ActionSource2AttachedObjectHandler,ActionSource2AttachedObjectTarget,PostConstructCustomScopeEventandPreDestroyCustomScopeEvent.
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 usedjavax(Mojarra withNoClassDefFoundErrorfor the class it was loading, MyFaces withClass javax.faces.webapp.FacesServlet not foundand a failedStartupServletContextListener). -
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 (
*.jspwith no.xhtmlsibling): Mojarra answered 404, MyFaces 500 withInvalidFileException. -
Startup noise: on an embedded Tomcat without Jasper, MyFaces 4.1.4 logs
SEVERE: Class org.apache.jasper.servlet.JspServlet not foundwhile 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=1field in a request that you assemble by hand, as the smoke test does; Mojarra ignores it. -
Generated markup: the Ajax button’s
onclickismojarra.ab(...)on Mojarra andmyfaces.ab(...)insidefaces.util.chainon MyFaces. Tests must not depend on either.
The general comparison is in Versions and Implementations.
What Changed, Release by 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 |
2.2 |
CDI |
2.3 |
|
3.0 |
The |
4.0 |
JSP, |
4.1 |
Full state saving, |
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-quarkus4.1.4 andquarkus-primefaces4.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
currentaddress 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.
| Area | What the books teach | Today | Since | Where |
|---|---|---|---|---|
View technology |
JSP views, JSP tag handlers ( |
Facelets XHTML. JSP support was deprecated in 2.0 and removed in 4.0, with the tag classes. |
2.0, 4.0 |
|
View technology |
Standalone Facelets 1.x: a library JAR, |
Facelets is part of Faces and the default VDL; parameters are |
2.0, 2.2 |
|
Namespaces and packages |
|
|
3.0 |
|
Namespaces and packages |
|
The URNs |
4.0, 5.0 planned |
XML Namespaces: java.sun.com and xmlns.jcp.org to the URNs (Faces 4.0) |
Beans |
|
CDI |
2.3 (deprecated), 4.0 (removed) |
|
Core APIs |
|
Value and method expressions, |
4.0 |
|
Core APIs |
Full state saving as the normal model. |
Partial state saving; full state saving is deprecated for removal. |
2.0, 4.1 |
|
Core APIs |
|
|
4.0 |
|
Ajax and resources |
Hand-written |
|
2.0 |
|
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 |
|
Integration |
Struts 1 integration and migration (Mann, Dudney et al.). |
Struts 1 is end of life; convert to Faces, screen by screen. |
n/a |
|
Integration |
JSR 301 and 329 portlet bridges (Burns and Schalk, appendix A). |
No portlet bridge in Jakarta EE 10 or 11. |
n/a |
|
Integration |
Spring 2.5 |
|
Spring 3.2 on, Faces 4.0 |
|
PrimeFaces books |
|
|
n/a |
|
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; |
11, 15 |
|
PrimeFaces books |
Legacy jqPlot charts; |
Chart.js models ( |
10, 13, 7 |
|
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 |
|
Render kits |
XUL and HTC render kits, and switching render kits at run time to target them (Jacobi and Fallows). |
Dead browser technologies. The |
n/a |
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
| Book | Weight | What is outdated, in short |
|---|---|---|
Ed Burns and Chris Schalk, JavaServer Faces 2.0: The Complete Reference (McGraw-Hill, 2010) |
High |
|
Mert Çalışkan and Oleg Varaksin, PrimeFaces Cookbook, Second Edition (Packt, 2015) |
Medium |
PrimeFaces 5.1 and 5.2 on JSF 2.2: |
Bruno Aranda and Zubin Wadia, Facelets Essentials (Apress, 2008) |
Low to medium |
Facelets 1.x as a separate library: |
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 |
Kito D. Mann, JavaServer Faces in Action (Manning, 2004) |
Low |
JSF 1.0 and 1.1 with JSP and Struts 1; |
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, |
Source: McGraw-Hill: JavaServer Faces 2.0: The Complete Reference, Packt: PrimeFaces Cookbook, Second Edition, Springer: Facelets Essentials, Packt: Learning PrimeFaces Extensions Development, Packt: JSF 2.0 Cookbook, Manning: JavaServer Faces in Action, Wiley: Mastering JavaServer Faces and Springer: Pro JSF and Ajax
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, thejava.sun.comandxmlns.jcp.orgnamespaces 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.faceseverywhere, 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
.jspwith<f:view>and*.jsfmapping, beans infaces-config.xml,javaxnames; today Facelets, CDI, and a mandatory CDI container (Weld on Tomcat). -
Project Setup: JSP wiring,
javax.facesparameters, mandatoryfaces-config.xmlwith<managed-bean>, DTD descriptors, standalone Facelets, hand-copiedjsf-api.jarandjsf-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:
javaxnames, JSP views building the tree while rendering, hand-written Ajax,immediateread as "skip validation", no system events,@ManagedBeanandValueBindingin examples. -
Facelets and XHTML Views: Facelets 1.x set-up and parameters, the
java.sun.comnamespaces, JSP views as an alternative, recipes that rely on RichFaces or Tomahawk tags. -
Templating: the
FaceletViewHandlerandfacelets.*parameters, the namespace changed twice, thejavaxrename,jsfc; silent on composite components, contracts and Ajax. -
HTML5-Friendly Markup: the
jsfcattribute 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),
@ManagedBeanwith<managed-bean>,ValueBindingandVariableResolver, JSP${},javax.el. -
HTML Components: the component reference is the part that aged best; missing
typeonh:inputText,layout="list",f:selectItemGroups,h:commandScript; Tomahawk and RichFaces tags;p:components standing in for standard ones. -
Core Tags:
javaxand the 1.x namespace,f:view,f:verbatimandf:subviewof JSP, managed beans withf:setPropertyActionListener,SelectItemlists,MethodBinding; nof:event,f:metadata,f:selectItemGroupsorf:resetValues. -
Data Tables and Iteration: JSP and
javax,@ManagedBeantables,ArrayDataModeland friends withoutCollectionDataModel, sorting and paging through RichFaces, Tomahawk or Trinidad,ui:repeatwithjsfc,bindingwithgetRowData(), 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.Converterand ids, registration in XML,java.util.Dateinstead ofjava.time, converters with parameters via managed beans, entity converters that serialise or call a static DAO, hand-written refresh; noUUIDConverter. -
Validators:
javaxnames 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.validationversus 3.1, nof:validateWholeBean,@ManagedBeanand<validator>registrations, recipes relying on ended libraries, PrimeFaces client-side validation as an addition. -
Messages and Internationalisation:
javax.faces.Messagesand its keys,f:loadBundleand managed beans for text, session-scoped locale switchers,native2asciiescapes,RequestContext. -
Events and Listeners: no system events in the 1.x books,
immediateandrenderResponse()tricks for dependent drop-downs,MethodBindinglisteners,@ManagedBeanlistener beans,javax.faces.event. -
Navigation: rules for everything,
.jspview ids, every link a postback,<view-param>,NavigationHandlersubclasses andMethodBinding, no Post/Redirect/Get by default,javaxnames, Struts-style navigation. -
View Parameters and View Actions: no
f:viewAction(apreRenderViewlistener 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 usejavaxand@ManagedBean. -
Backing Beans and CDI: beans in
faces-config.xml(1.x),@ManagedBeanand@ManagedPropertywith setters (2.x), Spring and EJB integration through resolvers or JNDI, staticFacesContext.getCurrentInstance()everywhere, session beans as the default. -
Scopes:
@ManagedBeanand thejavax.faces.beanscopes, 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.xmlnavigation rules and@ManagedBeanfor wizards. -
Multi-Window Support: nothing knows that tabs exist; state in session beans; hand-made tokens and
window.namescripts;javaxnames for the 2.2 client window; session-scoped wizard beans. -
Ajax: hand-written Ajax, Weblets and ajax4jsf;
jsf.js,jsf.ajax.requestandjavax.faces.partial.*; older thandelay,resetValues,h:commandScriptand search expressions; the libraries ended. -
Search Expressions: the PrimeFaces Cookbook treats
processandupdateas 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; usef:websocketoro:socket. -
Composite Components:
javaxpackages, thejava.sun.com/jsf/compositenamespace,@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.faceletshandlers, JSP tag handlers with TLDs, "composition components", the Tutorial’svalidator-classelement. -
Custom Components and Renderers: JSP tag handlers and TLDs,
javax, hand-writtensaveStateandrestoreState, 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"withjsf.js; the oldResourceHandlerWrapperconstructor 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:
javaxregistration and namespace,VariableResolver,PropertyResolver,MethodBinding,ValueBindingandResourceResolver, standalone Facelets view handlers, implementing whole interfaces instead of*Wrapperclasses, Weblets, beans from the managed-bean facility. -
Exception Handling:
javaxnames, 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
StateManagerAPI, 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_METHODas 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.faceson 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:watermarkand the mobile components, namespaces. -
PrimeFaces: Data Components: selection attributes, the old lazy
loadsignature,sortByandSortOrder,java.util.Dateschedule models, iText 2 and POI 3.7 exporters,ui-iconclasses, the raw tree-node constructors. -
PrimeFaces: Panels, Menus and Dialogs:
RequestContextfor 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:
fileUploadListenerand the upload limits on the component, Commons FileUpload,DefaultStreamedContentconstructors, 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,@ManagedPropertyparameters, hand-written exception and resource handlers, pretty URLs, PrimeFaces Push, cross-field validators, ended libraries,javaximports. -
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.xmlandjboss-web.xml, implementation JARs copied into the WAR, managed beans declared infaces-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.jarand Commons JARs to copy intoWEB-INF/libor 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
DelegatingVariableResolverintegration of Leonard’s cookbook, WAR-and-web.xmlassumptions in place of properties, Struts 1 integration, hand-editedweb.xmland manual JARs in the PrimeFaces books, andjavax.facesimports in old Spring-and-Faces tutorials. -
Quarkus: every book predates Quarkus; WAR deployment and
*.jsfmappings,managed-beanentries, Spring, Seam and Hibernate recipes that assume a run-time scanning container,Developmentinweb.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 andhbm.xml,@ManagedPropertyinjection, 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
javaxfield 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_STAGEcopied 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,@ManagedBeanand 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.
| Habit common in the era’s code | Correct form, and the page |
|---|---|
Page state in session-scoped beans (memory growth, cross-tab bugs) |
|
Business logic or queries in getters that run several times per request |
Load in |
|
|
JSTL |
|
|
Understand the phases first (Request Processing Lifecycle) |
Secrets, passwords or database URLs written in |
The environment or a secret store (Security) |
|
Escaped output; sanitise if markup must be kept (Security) |
State-changing |
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.facesstring 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 breaksjavax.naming,javax.sql,javax.cryptoandjavax.annotation.processing, which Java SE still owns, and it producesjakarta.faces.beanandjakarta.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
@ManagedPropertywithout@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 thejavax.faces.*parameters are ignored without a message; the stage falls back to Production. -
Changing
jsf.jsin 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
jakartaclassifier on PrimeFaces 16. The artifact has none. -
Assuming the artifact name tells the namespace.
org.glassfish:jakarta.faces:2.3.21isjavax(Faces Implementations and Servers). -
Mixing
javaxandjakartajars 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
Developmentstage because the migratedweb.xmlcame from a development copy. SetProductionfor deployments (Project Stages). -
Letting the temporary
*.jspmapping 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.
Related Pages
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.
References
-
Jakarta Faces 4.1 specification, Appendix B: Change Log (4.1 and 4.0, 3.0)
-
Jakarta EE Platform 9, 10 and 11
-
PrimeFaces migration guides (one per major, 10.0.0 to 16.0.0)
-
McGraw-Hill: JavaServer Faces 2.0: The Complete Reference (publisher page)
-
Packt: PrimeFaces Cookbook, Second Edition (publisher page)
-
Packt: JSF 2.0 Cookbook (publisher page)
-
Packt: Learning PrimeFaces Extensions Development (publisher page)
-
Springer: Facelets Essentials (publisher page)
-
Manning: JavaServer Faces in Action (publisher page)
-
Wiley: Mastering JavaServer Faces (publisher page)
-
Springer: Pro JSF and Ajax (publisher page)