Jakarta Faces on Quarkus: myfaces-quarkus, quarkus-primefaces and quarkus-omnifaces
|
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. |
This page is written against Jakarta Faces 4.1 as implemented by Apache MyFaces 4.1.4, packaged for Quarkus by the
myfaces-quarkus extension 4.1.4, on the Quarkus platform 3.33.1 (the main runs) and 3.40.1 (the PrimeFaces run and the
project generated by the Maven create goal), with Java 21 as the compiler release. The component-library extensions are
quarkus-primefaces 4.16.0 (which bundles PrimeFaces 16.0.0; quarkus-primefaces-extensions 4.16.0 adds PrimeFaces
Extensions 16.0.0) and quarkus-omnifaces 5.5.4 (OmniFaces 5.5.4; the current OmniFaces release is 5.5.5). Everything was
built and run on 2026-10-08 on a Mac with JDK 26, compiling with --release 21. Quarkus 4.0.0.Beta1 is published; the
MyFaces extension started on it and served a page, but nothing below describes Quarkus 4. Faces 5.0 is not covered: it is
still a milestone.
Quarkus is not a servlet container into which you drop a WAR. It is a build tool that analyses your application, runs extensions at build time and produces a runnable jar. Faces runs on top of the Undertow servlet layer, with CDI provided by ArC, Quarkus' build-time CDI container. That changes where files live, what a backing bean may be, what survives a restart and what the build can and cannot do for you. Only the MyFaces-based extensions are covered here; the Mojarra and Tomcat routes are the subject of Jakarta EE Servers and Tomcat and Weld.
How Faces Fits Into Quarkus
The Faces extension has build steps that run while Quarkus builds your application and a small runtime. At build time it
indexes your classes, registers the FacesServlet for *.xhtml when web.xml does not declare one, produces the context
parameters that wire MyFaces to Quarkus (an injection provider and a Faces initializer), registers the Faces scopes as ArC
contexts and records every class carrying a Faces or CDI annotation so MyFaces does not have to scan the classpath at startup.
The result is a jar that starts in well under two seconds.
Source: Apache MyFaces Core: Getting Started, Quarkus section and Quarkus: Contexts and Dependency Injection reference
The Faces lifecycle itself (see the lifecycle page) is unchanged: the same six
phases, the same FacesContext. What differs is everything around it, and that is what the rest of this page covers. The
general Quarkus topics (CDI, configuration, packaging, dev mode, tests) have their own pages in the Quarkus section of this
site: CDI and the Programming Model,
Configuration, Developer Experience,
Packaging and JVM Modes, Native
Executables and Testing Quarkus Applications. This page adds only the Faces-specific parts.
The Three Extensions
| Artifact | Maintained by, version | What it adds |
|---|---|---|
|
Apache MyFaces project, 4.1.4 (tracks MyFaces Core; first release 2020-06-30; declares Quarkus core 3.27 or later as its minimum) |
MyFaces itself, the servlet registration, the CDI integration (view, flow, client window and Faces scopes), the Undertow integration and the build-time annotation index. Installed feature name: |
|
Quarkiverse, 4.16.0 (bundles PrimeFaces 16.0.0; depends on |
PrimeFaces, plus the Quarkus extensions its exporters and barcode components need (Apache POI, OpenPDF, Okapi Barcode, AWT). Feature name: |
|
Quarkiverse, 5.5.4 (bundles OmniFaces 5.5.4, one patch behind the current OmniFaces release) |
OmniFaces, with its |
The Quarkiverse page for PrimeFaces describes the version number as the Quarkus major version followed by the PrimeFaces
major version. That does not match what was observed: 4.16.0 runs on Quarkus 3.33.1 and 3.40.1 and bundles PrimeFaces 16,
so the first number behaves like the Faces major version. Read the bundled library version from the startup log
(Running on PrimeFaces 16.0.0, Using OmniFaces version 5.5.4) rather than from the extension version.
Project Setup
The Bookshelf project of this page is a plain Maven project: the Quarkus BOM, one extension dependency, and the Quarkus Maven
plugin. There is no WAR packaging, no jakarta.jakartaee-web-api and no maven-war-plugin.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>bookshelf-quarkus</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<quarkus.version>3.33.1</quarkus.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
<artifactId>myfaces-quarkus</artifactId>
<version>4.1.4</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
A project with the quarkus-junit test dependency was generated and built with exactly this POM. The quarkus-junit5
artifact name used by older guides is a relocation to quarkus-junit since Quarkus 3.31, and Maven prints a warning when you use
the old name. The extension pulls in quarkus-undertow, quarkus-arc, quarkus-websockets and quarkus-jsonp, so you do
not add them yourself.
Where Files Go
A Quarkus application is packaged as a jar, so there is no src/main/webapp and no WEB-INF. The layout that was run:
bookshelf-quarkus/
pom.xml
src/main/java/org/example/bookshelf/
model/Book.java
service/BookService.java
web/CatalogueBean.java
src/main/resources/
application.properties Quarkus configuration
META-INF/web.xml replaces WEB-INF/web.xml
META-INF/faces-config.xml replaces WEB-INF/faces-config.xml
org/example/bookshelf/messages.properties resource bundle, var msg
org/example/bookshelf/messages_en.properties
org/example/bookshelf/messages_es.properties
META-INF/resources/ the web root
catalogue.xhtml views
expired.xhtml
resources/css/site.css Faces resource library "css"
WEB-INF/templates/layout.xhtml template, not reachable by URL
src/test/java/org/example/bookshelf/web/CatalogueTest.java
Source: Apache MyFaces Core: Getting Started, "Differences to a normal servlet container while developing" and Jakarta EE Tutorial: Configuring Jakarta Faces Applications
Behaviour observed on this layout:
-
Views, resource libraries and static files under
src/main/resources/META-INF/resourcesare the web root. A file placed insrc/main/webappis ignored: a view put there answered HTTP 404. -
web.xmlandfaces-config.xmlare read fromMETA-INFof the classpath, the way a library jar supplies its own. -
Templates and other include files can live in
META-INF/resources/WEB-INF/templates/. A request for/WEB-INF/templates/layout.xhtmlanswers 404 (the directory is hidden), whiletemplate="/WEB-INF/templates/layout.xhtml"inside a view resolves. That keeps the section’s template path (/WEB-INF/templates/layout.xhtml) working unchanged. A template inMETA-INF/resources/templates/also works but is a normally addressable view. -
Everything else under
META-INF/resourcesis public. A resource library file such asresources/css/site.cssis served twice: through Faces at/jakarta.faces.resource/site.css.xhtml?ln=cssand as a static file at/resources/css/site.css. Nothing under that tree should be secret. -
Files outside
META-INF/resources(for instancemessages.propertiesandMETA-INF/web.xml) answered 404 when requested.
The Catalogue
The code is the usual Bookshelf: a Book record, an application-scoped BookService and a view-scoped CatalogueBean
with a search field. Packages and the bundle variable msg follow the conventions of the section.
package org.example.bookshelf.model;
import java.io.Serializable;
public record Book(long id, String title, String author, int year) implements Serializable {
}
package org.example.bookshelf.service;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import org.example.bookshelf.model.Book;
@ApplicationScoped
public class BookService {
private final List<Book> books = List.of(
new Book(1, "Dune", "Frank Herbert", 1965),
new Book(2, "Neuromancer", "William Gibson", 1984),
new Book(3, "The Left Hand of Darkness", "Ursula K. Le Guin", 1969));
public List<Book> findAll() {
return books;
}
}
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
@Named
@ViewScoped
public class CatalogueBean implements Serializable {
@Inject
BookService service;
private String query = "";
public List<Book> getBooks() {
return service.findAll().stream()
.filter(b -> b.title().toLowerCase().contains(query.toLowerCase()))
.toList();
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query == null ? "" : query;
}
}
Source: Jakarta EE Tutorial: Developing a Simple Facelets Application and Quarkus: Contexts and Dependency Injection reference
The injected field is package-private on purpose: the Quarkus guide recommends avoiding private injection members because native
image needs reflection to reach them (a private field also works on the JVM, as shown later). The bean stays Serializable
so that the same class also runs unchanged on the servers of the other pages; ArC does not require it (see ArC, Scopes and Injection).
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<title><ui:insert name="title">Bookshelf</ui:insert></title>
<h:outputStylesheet library="css" name="site.css"/>
</h:head>
<h:body>
<ui:insert name="content"/>
</h:body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
<ui:define name="title">#{msg['catalogue.title']}</ui:define>
<ui:define name="content">
<h1>#{msg['catalogue.title']}</h1>
<h:form id="search">
<h:inputText id="query" value="#{catalogueBean.query}"/>
<h:commandButton id="go" value="#{msg['catalogue.search']}">
<f:ajax execute="@form" render=":books"/>
</h:commandButton>
</h:form>
<h:dataTable id="books" value="#{catalogueBean.books}" var="b">
<h:column><f:facet name="header">Title</f:facet>#{b.title}</h:column>
<h:column><f:facet name="header">Author</f:facet>#{b.author}</h:column>
<h:column><f:facet name="header">Year</f:facet>#{b.year}</h:column>
</h:dataTable>
</ui:define>
</ui:composition>
</html>
Source: Jakarta EE Tutorial: Facelets
The Ajax search was exercised with a raw partial request (execute set to the input, render set to the table): the response
carried only the matching row. The @form keyword is resolved by the client script before the request is sent; a hand-built
request that sends the literal @form as jakarta.faces.partial.execute did not apply the input in the runs, so a
curl test has to send the form id. See Search Expressions for the keywords.
<?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>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>es</supported-locale>
</locale-config>
</application>
</faces-config>
# messages.properties and messages_en.properties
catalogue.title=Catalogue
catalogue.search=Search
# messages_es.properties
catalogue.title=Catálogo
catalogue.search=Buscar
The messages_en.properties file is not decoration. With only messages.properties and messages_es.properties, a request
with Accept-Language: en on a machine whose JVM default locale is Spanish rendered the Spanish title: the JDK’s
ResourceBundle fallback tries the default locale’s bundle before the base bundle. The same happens on every Faces runtime,
but on Quarkus the JVM default locale is that of the build or container host, which makes it easy to meet in a container. A
bundle for every supported locale, English included, removes the dependency on the host. See
Messages and Internationalisation.
web.xml, application.properties and the Project Stage
META-INF/web.xml is read at build time. These entries were used and worked on the run:
<?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">
<welcome-file-list>
<welcome-file>catalogue.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<exception-type>jakarta.faces.application.ViewExpiredException</exception-type>
<location>/expired.xhtml</location>
</error-page>
</web-app>
Observed: with no <servlet> element the extension registers FacesServlet for *.xhtml; / served the welcome view;
the error-page for ViewExpiredException rendered expired.xhtml (the HTTP status was 500, as with any container); and
the jakarta.faces.AUTOMATIC_EXTENSIONLESS_MAPPING parameter is handled by the extension (see Pitfalls for a caveat).
The project stage is where Quarkus differs from every server on the previous pages:
quarkus.http.port=8080
# Build-time property: the Faces project stage. Do not set it in web.xml (see the table).
# unset dev mode -> Development, tests -> SystemTest, everything else -> Production
# explicit one value for every launch mode, for example:
# jakarta.faces.PROJECT_STAGE=Production
# Locales the JVM-side libraries (Hibernate Validator) must initialise
quarkus.default-locale=en
quarkus.locales=en,es
Source: Quarkus: Configuration reference and Jakarta EE Tutorial: Configuring Jakarta Faces Applications
| Configuration of the build | Stage observed |
|---|---|
|
|
the same context parameter and |
|
no context parameter, no property |
|
no context parameter, |
|
|
|
|
|
|
|
The extension reads jakarta.faces.PROJECT_STAGE through MicroProfile Config while building and writes the result into the
servlet initialisation parameters, so a web.xml entry for the stage has no effect, and a deployment cannot change it with an
environment variable afterwards: it is fixed at build time, like the context path. Use the property (Quarkus profile prefixes
such as %dev. are standard configuration syntax; they were not run for this property) and let the defaults do the rest. See
the project stage section for what each stage changes.
The context root follows the same rule. quarkus.servlet.context-path=/bookshelf given when building made the application
answer under /bookshelf/ and 404 at /, and every generated URL (resource links, form actions) carried the prefix.
Setting QUARKUS_SERVLET_CONTEXT_PATH only when launching the jar changed nothing, because the value had already been baked in
by the build.
ArC, Scopes and Injection
Quarkus' CDI container, ArC, implements CDI Lite plus selected extras (Decorators, parts of BeanManager, @SessionScoped
with Undertow), not CDI Full. For Faces this means several things that Weld users take for granted need attention. Each
statement below was run against myfaces-quarkus 4.1.4.
| Pattern | Result |
|---|---|
A |
works: a click counter incremented across postbacks with state kept in the view scope |
A |
both work (ArC is more permissive than Weld here) |
An |
the field is |
The same converter with |
the field is injected |
|
not resolvable: the bean was removed as unused; resolvable once annotated |
A |
kept: the |
A |
builds, then fails at render time with |
package org.example.bookshelf.web;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import jakarta.inject.Inject;
import org.example.bookshelf.service.BookService;
@FacesConverter(value = "bookshelf.title", managed = true)
public class TitleConverter implements Converter<String> {
@Inject
BookService service;
@Override
public String getAsObject(FacesContext context, UIComponent component, String value) {
return value == null ? null : value.trim();
}
@Override
public String getAsString(FacesContext context, UIComponent component, String value) {
return value;
}
}
package org.example.bookshelf.service;
import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
@Unremovable
@ApplicationScoped
public class AuditTrail {
public String hello() {
return "audit-ok";
}
}
Source: Quarkus: Removing unused beans
What this means in practice:
-
The extension registers ArC contexts for
@ViewScoped,@FlowScoped,@ClientWindowScopedand Faces' own scope annotation, so those scopes work the way they do on a server. It does not provide@ConversationScoped: the MyFaces extension documentation lists that scope as unsupported and the run above shows the failure mode. Replace conversations with@ViewScoped, a flow (see Faces Flows) or an explicit entity id in a view parameter. See the conversation section of the scopes page. -
Because ArC removes unused beans at build time, anything that is looked up by name at run time (
CDI.current(),Instance<T>with a qualifier,BeanManager) needs@Unremovableor a reference in the dependency tree. Beans reached through Expression Language need no annotation, because@Namedmarks them as in use. -
Injection into Faces artefacts that are not CDI beans (a custom
NavigationHandler,ViewHandlerand the like) is not supported by the extension, according to its documentation. Use a managed converter or validator, a CDI bean, orCDI.current(). See Extending Faces for the artefacts involved and Backing Beans and CDI for the standard CDI rules. -
@Inject FacesContextworks in request-scoped beans; the bean that printed the project stage for the table above did exactly that.
State Saving, Sessions and Restarts
Quarkus keeps HTTP sessions in memory, in the single process. The MyFaces extension documentation states that Quarkus does not support session replication, passivation or clustering, and that Quarkus beans are not serialisable for that purpose anyway. On this page that has three consequences.
-
With the default server state saving, the view state lives in the session and a restart loses it. Every form open in a browser then fails with
ViewExpiredExceptionon the next postback: theerror-pageof the web.xml above rendersexpired.xhtmlinProduction, and inDevelopment(dev mode) MyFaces writes its own development error page. -
With client state saving the view state travels in the page, so a restart does not lose it, but only if the encryption key survives the restart. Without configured keys MyFaces generates random ones at startup, and the old state could not be decrypted afterwards: the postback answered HTTP 500 and the log showed
ViewExpiredException. -
A syntactically wrong
jakarta.faces.ViewStatevalue (not Base64) answered HTTP 500 with anIllegalArgumentException("Last unit does not have enough valid bits") instead of an expired-view page. The page redirect fromerror-pageis only applied forViewExpiredException.
The keys belong in the environment, not in the source. Quarkus has no ${...} substitution inside web.xml, so a listener
copies them from MicroProfile Config into the servlet context before MyFaces reads them:
<context-param>
<param-name>jakarta.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
package org.example.bookshelf.web;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import org.eclipse.microprofile.config.ConfigProvider;
/** Copies the client-state keys from runtime configuration (environment) into the servlet context. */
@WebListener
public class StateKeys implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
var config = ConfigProvider.getConfig();
config.getOptionalValue("myfaces.secret", String.class)
.ifPresent(v -> event.getServletContext().setInitParameter("org.apache.myfaces.SECRET", v));
config.getOptionalValue("myfaces.mac-secret", String.class)
.ifPresent(v -> event.getServletContext().setInitParameter("org.apache.myfaces.MAC_SECRET", v));
}
}
# Generated once per environment and stored in a secret store, never in the repository.
export MYFACES_SECRET="$(openssl rand -base64 16)" # AES key, 16 bytes
export MYFACES_MAC_SECRET="$(openssl rand -base64 32)" # HMAC key, 32 bytes
java -jar target/quarkus-app/quarkus-run.jar
Quarkus maps the environment variable MYFACES_SECRET to the property myfaces.secret. The sequence that was run: start with
both variables, open the catalogue, stop the process, start it again with the same variables and post the old view state back
(the click was applied, HTTP 200); then start it without variables and post the same state (HTTP 500, ViewExpiredException).
The keys were 16 and 32 random bytes in Base64. All nodes of a
deployment need the same two values, which is the multi-instance rule of
the client-state keys section. For the cost and the audit of client
state see the audit section. Client state also puts the
encrypted view into every page, which matters for the size of the response; measure it with
the state saving part of the performance page.
Dev Mode and Live Reload
mvn quarkus:dev starts the application with live coding. The log of a dev-mode session on the Bookshelf showed the
project stage Development, the MyFaces development warning and a start of about 1.5 seconds; Quarkus then restarts in a
fraction of a second when the code changes.
mvn -Dquarkus.analytics.disabled=true -Dquarkus.console.enabled=false quarkus:dev
# Dev UI at http://localhost:8080/q/dev/ (dev mode only)
| Edit | Observed |
|---|---|
An existing |
"Files changed but restart not needed"; the next request showed the change (a heading edit in the run) |
A new |
served at once, HTTP 200 |
A new resource under |
served at once; a reference made before the file existed logged "Resource referenced … not found … will be silently ignored" and worked after the file was created |
A Java class (backing bean, service, converter) |
"Restarting quarkus due to changes in `CatalogueBean.class`", live reload in 0.2 to 0.4 s |
|
no restart and no change in the page; the JDK keeps the bundle it had. Touching a Java class restarted Quarkus and the new text appeared. |
A form opened before a Java restart, then posted |
|
Two practical rules follow. After changing a bundle, touch a Java class (which forces a restart) and reload. And keep the browser tab you are testing in sync with the server: an open form survives edits to .xhtml files but not
a Java reload. xhtml hot reload is the main reason to keep the project stage at Development in dev mode; it also enables
the Faces development error page, a ProjectStage feature described in
the exception handling page.
Tests
@QuarkusTest boots the application once and gives the test the real HTTP port. The Faces project stage in tests is
SystemTest, which is why the log of a test run shows the system-test banner. The Arquillian approach of the testing page is not needed on Quarkus; a Quarkus test already runs against a started application.
package org.example.bookshelf.web;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.junit.jupiter.api.Test;
@QuarkusTest
class CatalogueTest {
@TestHTTPResource("/catalogue.xhtml")
URL catalogue;
@Test
void catalogueListsTheBooks() throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(catalogue.toString()))
.header("Accept-Language", "en")
.build();
HttpResponse<String> response = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build()
.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("<td>Dune</td>"));
assertTrue(response.body().contains("<h1>Catalogue</h1>"));
}
}
mvn test ran this test green. Two details cost time. First, the default HttpClient of the JDK asks for an HTTP/2 upgrade,
and the test failed with Received RST_STREAM: Not an error while the log reported the request being handled after the
shutdown; pinning HttpClient.Version.HTTP_1_1 fixed it. Second, the English assertion needed the Accept-Language header
and the messages_en.properties file described earlier. For browser-level tests of Ajax and view state see
the Faces testing page and
End-to-end testing with real browsers; the Quarkus side is in
Testing Quarkus Applications.
Bean Validation
Add quarkus-hibernate-validator and MyFaces finds Bean Validation: the startup log changes from "MyFaces Core Bean Validation
support disabled" to "enabled", and the constraints on the bound property run in the Process Validations phase.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Named
@RequestScoped
public class ReviewNoteBean {
@NotBlank
@Size(max = 20)
private String comment;
@Inject
FacesContext facesContext;
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public void save() {
facesContext.addMessage(null, new FacesMessage("Saved: " + comment));
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html">
<h:head><title>Note</title></h:head>
<h:body>
<h:form id="note">
<h:inputText id="comment" value="#{reviewNoteBean.comment}"/>
<h:commandButton id="save" value="Save" action="#{reviewNoteBean.save}"/>
<h:messages id="msgs" globalOnly="false"/>
</h:form>
</h:body>
</html>
Source: Quarkus: Validation with Hibernate Validator and Jakarta EE Tutorial: Configuring Jakarta Faces Applications
A request with an empty comment showed "must not be blank" with Accept-Language: en, a 25-character comment showed "size
must be between 0 and 20", and a short comment saved. The locale trap: Hibernate Validator on Quarkus initialises only the
locales it was told about. With application.properties free of quarkus.locales, the same request with
Accept-Language: en on a Spanish-default JVM answered HTTP 500 with HV000250: Uninitialized locale: en. Declaring
quarkus.default-locale=en and quarkus.locales=en,es (the Bookshelf properties above) fixed it, and an unknown language
such as German then fell back to English. See Bean Validation in Faces for the integration and
Quarkus validation for the Quarkus side.
PrimeFaces and PrimeFaces Extensions
quarkus-primefaces replaces the myfaces-quarkus dependency (it depends on it). The Bookshelf catalogue as a PrimeFaces
page needs only the dependency and a view:
<dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces</artifactId>
<version>4.16.0</version>
</dependency>
<!-- optional: PrimeFaces Extensions 16.0.0 (pe: namespace) -->
<dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces-extensions</artifactId>
<version>4.16.0</version>
</dependency>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:p="primefaces">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
<ui:define name="title">#{msg['catalogue.title']}</ui:define>
<ui:define name="content">
<h1>#{msg['catalogue.title']}</h1>
<h:form id="search">
<p:inputText id="query" value="#{catalogueBean.query}"/>
<p:commandButton id="go" value="#{msg['catalogue.search']}" process="@form" update=":books"/>
</h:form>
<p:dataTable id="books" value="#{catalogueBean.books}" var="b" paginator="true" rows="2">
<p:column headerText="Title" sortBy="#{b.title}">#{b.title}</p:column>
<p:column headerText="Author">#{b.author}</p:column>
<p:column headerText="Year" sortBy="#{b.year}">#{b.year}</p:column>
</p:dataTable>
</ui:define>
</ui:composition>
</html>
The same CatalogueBean served this page. The theme, icon and component stylesheets, the PrimeFaces.ab client call
(s:"search:go", p:"search", u:"books") and the locale script were written; a PrimeFaces Ajax request that sent
query=dune returned a table with the one matching row; pe:tooltip and pe:qrCode of PrimeFaces Extensions rendered
(Running on PrimeFaces Extensions 16.0.0 in the log). The installed features of the PrimeFaces application were
awt, cdi, myfaces, okapi, openpdf, poi, primefaces (plus primefaces-extensions when added): the extension brings the
document and barcode libraries that the exporters need, which is why the application grows from about 22 MB (100 library jars)
to about 83 MB (147 jars) in fast-jar form once PrimeFaces, OmniFaces and Hibernate Validator are all present. The deployment-time consequences are discussed in Packaging: Fast-Jar, Uber-Jar and Containers.
A few observations about the extension page itself:
-
The Quarkiverse documentation describes a Codestart that scaffolds
web.xml,faces-config.xml, a layout,index.xhtmland aGreetingBean. With the Mavencreategoal of plugin 3.40.1 and-Dextensions=io.quarkiverse.primefaces:quarkus-primefaces:4.16.0the generated project contained only thepom.xml, theREADME.md, the Docker files, the wrapper and an emptyapplication.properties: the releasedquarkus-primefaces-4.16.0.jarhas no Codestart. Write the descriptors yourself, as on this page, or copy the web.xml of the extension’s integration test, which sets the theme, the mime mappings for fonts and theerror-pageentries. See the PrimeFaces parameters for theprimefaces.*context parameters. -
The same page lists limitations for native image: PrimeFaces'
FeedReaderis unsupported, and EL expressions that reach reflection-sensitive getters (size()on a list,name()on an enum) can fail in a native executable because they are not bean-compliant names (see Native Image). -
PrimeFaces' own pages: data components, Ajax forms and validation and PrimeFaces Extensions apply unchanged, because they are MyFaces-compatible.
OmniFaces
quarkus-omnifaces adds OmniFaces to the classpath and registers its push endpoint. The o: namespace is
omnifaces and the functions are called as o:abbreviate(...) and o:formatDate(...); both worked on the page that was run
(#{o:abbreviate('The Left Hand of Darkness', 12)} rendered The Left Han...). The tags, validators and
utilities behave as described in the OmniFaces page; o:highlight rendered in the run.
<dependency>
<groupId>io.quarkiverse.omnifaces</groupId>
<artifactId>quarkus-omnifaces</artifactId>
<version>5.5.4</version>
</dependency>
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.omnifaces.cdi.Push;
import org.omnifaces.cdi.PushContext;
@Named
@RequestScoped
public class ReviewAnnouncer {
@Inject
@Push(channel = "reviews")
PushContext reviews;
public void announce() {
reviews.send("new review");
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:o="omnifaces">
<h:head><title>Live reviews</title></h:head>
<h:body>
<o:socket channel="reviews" scope="session" onmessage="function (message) { console.log(message); }"/>
<h:form id="live">
<h:commandButton id="announce" value="Announce" action="#{reviewAnnouncer.announce}"/>
</h:form>
</h:body>
</html>
A raw WebSocket client connected to the endpoint (/omnifaces.socket/reviews?<token>, sent with the session cookie), a postback
pressed the button and the client received "new review". The startup log lists the endpoint
org.omnifaces.cdi.push.SocketEndpoint for /omnifaces.socket/{channel}. Compare this with the standard
f:websocket, which needed a different answer on Quarkus:
f:websocket on Quarkus
The standard push of Faces (f:websocket with @Push from jakarta.faces.push, see the push page) is
enabled in MyFaces with jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT:
<context-param>
<param-name>jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT</param-name>
<param-value>true</param-value>
</context-param>
<f:websocket channel="reviews" scope="session" onmessage="function (m) { console.log(m); }"/>
The endpoint was registered (org.apache.myfaces.push.EndpointImpl for /jakarta.faces.push/{channel}) and the page
rendered, but a client that opened the socket with the session cookie and the channel token was closed at once with code 1011 and
the reason "Websocket connection not registered in current session". The same happened with scope="session",
"application" and "view", and nothing was delivered after a postback sent a message. The cause was not investigated; the
extension documentation does not list WebSocket as supported or unsupported. Until that is understood, use o:socket on Quarkus, or
test f:websocket on your exact versions before relying on it. The Bookshelf live feed on the other pages of the section
uses f:websocket; on Quarkus use the OmniFaces variant above.
Packaging: Fast-Jar, Uber-Jar and Containers
The default package type is the fast-jar: target/quarkus-app/quarkus-run.jar with its libraries beside it. The MyFaces
extension documentation says that Quarkus uber-jars are not supported, referring to MYFACES-4427. That statement is out of date for
the versions on this page: the issue is fixed in 4.0.3, 4.1.0-RC3 and 5.0.0, and the extension has a build step that merges the
service files and faces-config.xml for uber-jars.
# fast-jar (default): target/quarkus-app/quarkus-run.jar plus lib/
mvn package
java -jar target/quarkus-app/quarkus-run.jar
# one uber-jar: target/bookshelf-quarkus-1.0.0-SNAPSHOT-runner.jar
mvn package -Dquarkus.package.jar.type=uber-jar
java -jar target/bookshelf-quarkus-1.0.0-SNAPSHOT-runner.jar
Source: Quarkus: Creating a project with Maven, uber-jar and MYFACES-4427: Cannot open any pages in Quarkus with uber-jar
Both were built and served the catalogue: the MyFaces-only uber-jar was about 20 MB and the PrimeFaces uber-jar about 80 MB, and the
PrimeFaces uber-jar rendered a page with its theme and stylesheet links and served the PrimeFaces jQuery resource. The fast-jar is the better default for containers
because the libraries form a layer that changes less often than your classes; see
Packaging and JVM Modes and
Container Images. The Faces-specific deployment topics (the Production stage, state
saving across several instances, the clustered picture) are on the deployment and clustering page: this page
only adds that Quarkus has no session replication, so several instances need sticky sessions with server state or the shared
keys of State Saving, Sessions and Restarts with client state.
Native Image
A native executable removes the JVM and starts in milliseconds, at the price of closed-world analysis. The MyFaces extension
helps: its build steps register renderers, system events, StateHolder implementations and the factory classes for reflection,
and the CDI side uses ArC, which is built for native. None of that was run here, because no GraalVM or Mandrel installation and
no running container daemon were available on the test machine. This section therefore lists only what the extensions document:
-
The Quarkus CDI guide recommends avoiding
privatemembers in beans, since the build must register reflective access for them; use package-private fields, asCatalogueBeandoes. -
PrimeFaces on native:
FeedReaderis not supported, and reflection-sensitive EL (size(),name()) may fail. -
Any EL expression, converter or validator that reaches a class through reflection that the extension did not register needs a
@RegisterForReflectionannotation. The Quarkus native guide describes the mechanism and how to find the missing registration. -
The generated native build command (from the project the Maven plugin created) is
./mvnw package -Dnative, or, without GraalVM installed,./mvnw package -Dnative -Dquarkus.native.container-build=true.
Treat all of this as untested for Faces on 4.1.4 until you build it. See Native Executables for the general trade-offs.
Quarkus Compared With the Other Runtimes
| Aspect | Jakarta EE server (WAR) | Tomcat plus Weld (WAR) | Quarkus (jar) |
|---|---|---|---|
Packaging |
WAR with |
WAR with the implementation and Weld in |
jar built by the Quarkus plugin; no WAR |
Faces implementation |
chosen by the server (Mojarra or MyFaces) |
your choice |
MyFaces, through the extension |
CDI container |
Weld (or OpenWebBeans) |
Weld, started by a listener |
ArC (CDI Lite), build time |
Views, descriptors |
|
|
|
Project stage |
|
|
|
Hot reload |
redeploy or a plugin |
redeploy |
|
Session replication |
available (server specific) |
Tomcat clustering |
none; use client state with shared keys or sticky sessions |
Conversation scope |
yes |
yes |
no |
The servers and the Tomcat route are described in Jakarta EE Servers and Tomcat and Weld; the
choice between them and Quarkus is mostly about the surrounding platform (the rest of the services, containers, native image)
and not about Faces. Faces-heavy applications with deep reliance on @ConversationScoped, CDI portable extensions or server
clustering are the ones where Quarkus needs rework. See Quarkus vs Spring Boot
for the Quarkus comparison.
What Changed
-
Faces 2.3 era. The MyFaces documentation says it has had a Quarkus extension "since 2.3-next", and the extension catalogue lists the first release of
myfaces-quarkuson 2020-06-30. The Quarkiverse extensions for PrimeFaces and OmniFaces exist alongside it; their first release dates were not checked. -
Quarkus 3 and Jakarta EE. The extensions moved to
jakarta.*with Quarkus 3. The current MyFaces extension (4.1.4) requires Quarkus core 3.27 or later;quarkus-primefaces4.16.0 requires 3.33 or later and declares Java 21 as its minimum. -
Faces 4.0 and 4.1. Version 4.0.x of the extension provides Faces 4.0; 4.1.x provides Faces 4.1. The uber-jar problem MYFACES-4427 was fixed in 4.0.3 and 4.1.0-RC3.
jakarta.faces.AUTOMATIC_EXTENSIONLESS_MAPPINGis handled by the extension build step. -
Quarkus 3.31.
quarkus-junit5became a relocation toquarkus-junit. -
Faces 5.0 (upcoming) and Quarkus 4. Both exist as milestones or betas (Faces 5.0.0-M9, Quarkus 4.0.0.Beta1). Nothing here describes them.
Where the Books Are Outdated
All eight books in the bibliography predate Quarkus (first released in 2019) and the MyFaces extension (2020), so none describes this runtime. What they do describe differs from what you will do here in several ways.
-
The deployment chapters of the Faces 1.x and 2.x books assume a WAR copied to a servlet container or application server, with
WEB-INF/web.xmlmappingFacesServletto*.jsf,/faces/*or*.faces. On Quarkus there is no WAR, the servlet is registered for*.xhtmlby the extension, and the descriptors live inMETA-INF. -
The books register beans in
faces-config.xml(managed-bean), use#{...}managed properties and rely on@ManagedBean. These were removed in Faces 4.0; Quarkus has only CDI beans, resolved by ArC at build time. -
The Spring, Seam and Hibernate recipes of the cookbook-style books assume a dynamic container that scans at run time. A build-time container removes unused beans and cannot run portable extensions, which is the reverse of those recipes.
-
The JSF 2.x books call for
Developmentinweb.xmland the stage switch as a deployment decision. On Quarkus it is a build-time property with launch-mode defaults. -
The books' hot-deployment advice (redeploy, JRebel, exploded directory) is replaced by
quarkus:devlive coding. -
Nothing in the books covers native image or container-first packaging; their sizing and startup assumptions (seconds to minutes of start, a gigabyte of heap) do not apply to a Quarkus jar.
-
The PrimeFaces books target PrimeFaces 4 and 5 and a
primefaces-themessetup by jar; the Quarkus extension bundles PrimeFaces 16 and its libraries, and themes other than the default one come from the separate themes jar (see the themes section).
Pitfalls
-
Putting views in
src/main/webapp. They are ignored (HTTP 404). Put them undersrc/main/resources/META-INF/resources. -
Setting
jakarta.faces.PROJECT_STAGEinweb.xml. It is overridden by the extension: the stage isProductionwhatever the file says. Use the property, in the build. -
Expecting
QUARKUS_SERVLET_CONTEXT_PATHor the stage to change at launch. Both are build-time values. -
Extensionless mapping and the library folders. With
jakarta.faces.AUTOMATIC_EXTENSIONLESS_MAPPINGset totrue, my reading of the extension’s build step (not documented) is that the mapping is built from the views found underMETA-INF/resourcesbefore the step meets a folder namedWEB-INForresources. Observed once, on one machine and file system (directory order of the build output), a project that hadWEB-INF/templatesandresources/cssmapped only one of its three views extensionless (the others answered 404 without the extension), and a project with neither folder mapped all three. Test this parameter on your exact layout before relying on it, or keep the.xhtmlsuffix. -
Plain converters and validators with
@Inject. The field staysnull; usemanaged = true. -
Looking beans up with
CDI.current()orInstance. Unused beans are removed; annotate with@Unremovable. -
@ConversationScoped. It compiles and then fails at render time withContextNotActiveException. -
Server state in production. Every restart or deploy expires every open form. Use client state with keys from the environment, or accept the expiry and design the
expired.xhtmlpage for it. -
Client state without keys. Random keys per start: old pages fail after a restart and different instances cannot read each other’s state.
-
Hibernate Validator locales. Register the locales you serve with
quarkus.locales; otherwise an unregisteredAccept-Languageproduces HTTP 500 withHV000250. -
A missing English bundle. On a non-English JVM default locale, the base bundle is skipped for an English request. Ship a bundle for every supported locale.
-
Using
f:websocketwithout testing it. The socket was closed with 1011 on Quarkus in the runs; useo:socket. -
The JDK
HttpClientin tests. Pin HTTP/1.1. -
Leaving the Dev UI and development stage in a production image. The dev UI is dev mode only; the stage in a normal build is
Productionunless you set it. Check the startup log for the MyFaces development warning. -
Secrets in the repository. The client-state keys, database passwords and PrimeFaces or OmniFaces tokens belong in the environment or a secret store, as the listener on this page reads them.
Related Pages
The Jakarta EE Servers and Tomcat and Weld pages cover the other ways of running the same application; the Spring Boot with JoinFaces page covers the Spring route (see also web UI frameworks in Spring Boot); the Persistence and Services page covers data access and transactions with Quarkus' Hibernate ORM and Panache as well (see Hibernate ORM and JPA on Quarkus); the Deployment and Clustering page covers the clustered picture; Testing, Migrating to Jakarta Faces and the best-practices checklist cover the rest of the section. For the Faces pages themselves see Project Setup, Getting Started, State Saving and Stateless Views, Security and OmniFaces.