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.

flowchart LR A["pom.xml with the Quarkus Maven plugin"] --> B["Build: Jandex index, ArC, extension build steps"] B --> C["Recorded: FacesServlet, init parameters, view and flow scoped contexts, annotated classes"] C --> D["target/quarkus-app (fast-jar) or one uber-jar"] D --> E["java -jar: Undertow, ArC and MyFaces start"] E --> F["Requests reach FacesServlet at the context path"]

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

Table 1. The Faces extensions for Quarkus, as checked on 2026-10-08
Artifact Maintained by, version What it adds

org.apache.myfaces.core.extensions.quarkus:myfaces-quarkus

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: myfaces.

io.quarkiverse.primefaces:quarkus-primefaces

Quarkiverse, 4.16.0 (bundles PrimeFaces 16.0.0; depends on myfaces-quarkus)

PrimeFaces, plus the Quarkus extensions its exporters and barcode components need (Apache POI, OpenPDF, Okapi Barcode, AWT). Feature name: primefaces. A second artifact, quarkus-primefaces-extensions, adds PrimeFaces Extensions (feature primefaces-extensions).

io.quarkiverse.omnifaces:quarkus-omnifaces

Quarkiverse, 5.5.4 (bundles OmniFaces 5.5.4, one patch behind the current OmniFaces release)

OmniFaces, with its o:socket endpoint registered. Feature name: omnifaces.

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.

pom.xml: the MyFaces extension and nothing else from Faces
<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:

The Bookshelf layout for Quarkus
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

Behaviour observed on this layout:

  • Views, resource libraries and static files under src/main/resources/META-INF/resources are the web root. A file placed in src/main/webapp is ignored: a view put there answered HTTP 404.

  • web.xml and faces-config.xml are read from META-INF of 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.xhtml answers 404 (the directory is hidden), while template="/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 in META-INF/resources/templates/ also works but is a normally addressable view.

  • Everything else under META-INF/resources is public. A resource library file such as resources/css/site.css is served twice: through Faces at /jakarta.faces.resource/site.css.xhtml?ln=css and as a static file at /resources/css/site.css. Nothing under that tree should be secret.

  • Files outside META-INF/resources (for instance messages.properties and META-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.

Book.java, BookService.java and CatalogueBean.java
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;
    }
}

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).

layout.xhtml and catalogue.xhtml
<!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>

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.

faces-config.xml and the bundles: messages, one language file per locale
<?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:

META-INF/web.xml: welcome file and an error page, no servlet declaration
<?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:

application.properties: the project stage is a Quarkus configuration property
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
Table 2. Which setting decided the project stage, checked by printing the stage from a bean in a production jar
Configuration of the build Stage observed

jakarta.faces.PROJECT_STAGE context parameter Development in META-INF/web.xml, nothing else

Production

the same context parameter and -Djakarta.faces.PROJECT_STAGE=Production at build

Production

no context parameter, no property

Production

no context parameter, -Djakarta.faces.PROJECT_STAGE=Development at build

Development

jakarta.faces.PROJECT_STAGE=Production in application.properties and Development in web.xml

Production (the property wins)

quarkus:dev with the property unset

Development (MyFaces prints its development-mode warning)

@QuarkusTest with the property unset

SystemTest (MyFaces prints its system-test banner)

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.

Table 3. Behaviour of ArC with Faces beans (production jar, Quarkus 3.33.1)
Pattern Result

A @ViewScoped bean that is not Serializable

works: a click counter incremented across postbacks with state kept in the view scope

A @Named bean with a private injected field, or a final class with constructor injection and no no-argument constructor

both work (ArC is more permissive than Weld here)

An @Inject field in a plain @FacesConverter("id") class

the field is null; the converter was created without CDI

The same converter with @FacesConverter(value = "id", managed = true)

the field is injected

CDI.current().select(Hidden.class) for an @ApplicationScoped bean nothing injects

not resolvable: the bean was removed as unused; resolvable once annotated @io.quarkus.arc.Unremovable

A @Named bean referenced only from Expression Language

kept: the @Named qualifier makes it unremovable

A @ConversationScoped bean

builds, then fails at render time with ContextNotActiveException

A converter that needs a service: managed, so that the injection happens
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;
    }
}
A bean looked up programmatically has to be kept explicitly
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";
    }
}

What this means in practice:

  • The extension registers ArC contexts for @ViewScoped, @FlowScoped, @ClientWindowScoped and 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 @Unremovable or a reference in the dependency tree. Beans reached through Expression Language need no annotation, because @Named marks them as in use.

  • Injection into Faces artefacts that are not CDI beans (a custom NavigationHandler, ViewHandler and the like) is not supported by the extension, according to its documentation. Use a managed converter or validator, a CDI bean, or CDI.current(). See Extending Faces for the artefacts involved and Backing Beans and CDI for the standard CDI rules.

  • @Inject FacesContext works 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 ViewExpiredException on the next postback: the error-page of the web.xml above renders expired.xhtml in Production, and in Development (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.ViewState value (not Base64) answered HTTP 500 with an IllegalArgumentException ("Last unit does not have enough valid bits") instead of an expired-view page. The page redirect from error-page is only applied for ViewExpiredException.

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:

web.xml (state saving method) and StateKeys.java (keys from configuration)
<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.

Starting dev mode without prompts
mvn -Dquarkus.analytics.disabled=true -Dquarkus.console.enabled=false quarkus:dev
# Dev UI at http://localhost:8080/q/dev/  (dev mode only)
Table 4. What the dev-mode session did on each kind of edit
Edit Observed

An existing .xhtml under META-INF/resources

"Files changed but restart not needed"; the next request showed the change (a heading edit in the run)

A new .xhtml file

served at once, HTTP 200

A new resource under resources/css

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

messages*.properties, including a newly added messages_en.properties

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

ViewExpiredException (server state is lost with the restart); in dev mode MyFaces shows its development error page

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.

CatalogueTest.java: a Quarkus test over HTTP/1.1
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.

pom.xml addition, ReviewNoteBean.java and note.xhtml
<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>

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:

pom.xml and catalogue-pf.xhtml
<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.xhtml and a GreetingBean. With the Maven create goal of plugin 3.40.1 and -Dextensions=io.quarkiverse.primefaces:quarkus-primefaces:4.16.0 the generated project contained only the pom.xml, the README.md, the Docker files, the wrapper and an empty application.properties: the released quarkus-primefaces-4.16.0.jar has 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 the error-page entries. See the PrimeFaces parameters for the primefaces.* context parameters.

  • The same page lists limitations for native image: PrimeFaces' FeedReader is 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.

pom.xml and the Bookshelf live feed with o:socket
<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:

web.xml entry for f:websocket and the view that was tried
<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.

Building and running the two package types
# 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

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 private members in beans, since the build must register reflective access for them; use package-private fields, as CatalogueBean does.

  • PrimeFaces on native: FeedReader is 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 @RegisterForReflection annotation. 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

Table 5. The Bookshelf on the runtimes of this section
Aspect Jakarta EE server (WAR) Tomcat plus Weld (WAR) Quarkus (jar)

Packaging

WAR with provided APIs

WAR with the implementation and Weld in WEB-INF/lib

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

src/main/webapp, WEB-INF

src/main/webapp, WEB-INF

src/main/resources/META-INF/resources, META-INF

Project stage

web.xml context parameter

web.xml context parameter

jakarta.faces.PROJECT_STAGE property, build time, defaulted by launch mode

Hot reload

redeploy or a plugin

redeploy

quarkus:dev: views without restart, classes in a fraction of a second

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-quarkus on 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-primefaces 4.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_MAPPING is handled by the extension build step.

  • Quarkus 3.31. quarkus-junit5 became a relocation to quarkus-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.xml mapping FacesServlet to *.jsf, /faces/* or *.faces. On Quarkus there is no WAR, the servlet is registered for *.xhtml by the extension, and the descriptors live in META-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 Development in web.xml and 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:dev live 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-themes setup 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 under src/main/resources/META-INF/resources.

  • Setting jakarta.faces.PROJECT_STAGE in web.xml. It is overridden by the extension: the stage is Production whatever the file says. Use the property, in the build.

  • Expecting QUARKUS_SERVLET_CONTEXT_PATH or the stage to change at launch. Both are build-time values.

  • Extensionless mapping and the library folders. With jakarta.faces.AUTOMATIC_EXTENSIONLESS_MAPPING set to true, my reading of the extension’s build step (not documented) is that the mapping is built from the views found under META-INF/resources before the step meets a folder named WEB-INF or resources. Observed once, on one machine and file system (directory order of the build output), a project that had WEB-INF/templates and resources/css mapped 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 .xhtml suffix.

  • Plain converters and validators with @Inject. The field stays null; use managed = true.

  • Looking beans up with CDI.current() or Instance. Unused beans are removed; annotate with @Unremovable.

  • @ConversationScoped. It compiles and then fails at render time with ContextNotActiveException.

  • 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.xhtml page 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 unregistered Accept-Language produces HTTP 500 with HV000250.

  • 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:websocket without testing it. The socket was closed with 1011 on Quarkus in the runs; use o:socket.

  • The JDK HttpClient in 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 Production unless 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.

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.