Tomcat and Weld: Faces on a Servlet Container

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 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Apache Tomcat 11.0.26, Weld weld-servlet-shaded 6.0.4.Final, Hibernate Validator 9.1.4.Final, Eclipse Parsson 1.1.9, Jandex 3.6.0 and Eclipse Jetty 12.1.14, all checked on 2026-10-08. The application targets Java 17 (Faces 4.1, Tomcat 11 and Weld 6 all need Java 17 or later); the examples were compiled with --release 17 and run on JDK 26. Faces 4.0 (Jakarta EE 10, Tomcat 10.1, Weld 5) works the same way unless the text says otherwise, and Faces 5.0 (Jakarta EE 12) is still a milestone (jakarta.faces-api 5.0.0-M9) and is mentioned only as upcoming.

The examples on this page were built and run: the Bookshelf WAR below was built with Maven once for Mojarra and once for MyFaces, deployed to a standalone Tomcat 11.0.26, to Jetty 12.1.14 and to embedded Tomcat, and driven with curl, a small Java WebSocket client and a second set of throw-away variants (no beans.xml, no Weld, other Weld releases, with and without the BeanManager binding and so on). Where something was read from the documentation or from decompiled class files and not run, the text says so.

A Jakarta EE application server ships Faces, CDI, Bean Validation and the rest, so a Faces WAR for it contains no Faces JARs (see Jakarta EE Servers). A Servlet container such as Tomcat ships only the web tier. Faces needs more than that: a CDI container, because since Faces 4.0 the managed-bean facility is gone and every bean, scope and injection point is CDI; a Faces implementation; and, when you use them, Bean Validation and JSON Processing. On Tomcat you bring those yourself, inside the WAR, and you choose the implementation. That freedom is the reason to choose Tomcat, and the price is the wiring described here.

What Tomcat Provides and What You Add

Tomcat 11.0.x implements Servlet 6.1, Pages (JSP) 4.0, Expression Language 6.0, WebSocket 2.2, Authentication 3.1 and Annotations 3.0, and requires Java 17 or later. It does not implement CDI, Faces, Bean Validation, JSON Processing or the Standard Tag Library.

Need Provided by What the WAR adds

Servlet 6.1, WebSocket 2.2, Annotations 3.0

Tomcat (servlet-api.jar, websocket-api.jar, annotations-api.jar in $CATALINA_HOME/lib)

Nothing. Compile against them with provided scope and never put them in WEB-INF/lib.

Expression Language 6.0

Tomcat (el-api.jar and the implementation in jasper-el.jar)

Nothing. Faces, Hibernate Validator and the Facelets tags all use Tomcat’s EL (checked: a ${validatedValue} message and a Facelets c:forEach ran on it).

CDI 4.1

Not provided

org.jboss.weld.servlet:weld-servlet-shaded 6.0.4.Final: CDI API, Weld core and the servlet integration in one JAR.

Faces 4.1

Not provided

Mojarra: org.glassfish:jakarta.faces 4.1.17 (API and implementation in one JAR). MyFaces: myfaces-api and myfaces-impl 4.1.4.

Bean Validation 3.1 (optional)

Not provided

org.hibernate.validator:hibernate-validator 9.1.4.Final, which brings the Validation API, JBoss Logging and ClassMate.

JSON Processing 2.1 (only Mojarra with f:websocket)

Not provided

org.eclipse.parsson:parsson 1.1.9, which brings jakarta.json-api.

Standard Tag Library (c:, fn: in Facelets)

Not provided

Nothing: the Facelets tags of both implementations ran without a JSTL JAR.

The Mojarra README lists Java 17, Servlet 6.1, EL 6.0 and CDI 4.1 as the minimum, and WebSocket 2.2, JSON Processing 2.1 and Validation 3.1 as optional extras that matter only for f:websocket and f:validateBean; the MyFaces page lists Java 17, Servlet 6.1, EL 6.0 and CDI 4.1 as requirements and JSTL 3.0 and Bean Validation 3.1 as optional. (Jetty 12 shows that "optional" does not hold for the WebSocket API on a container that does not ship it.) The diagram shows which side each piece comes from.

flowchart LR B["Browser"] --> S subgraph T["Tomcat 11.0.26"] S["Servlet 6.1 and FacesServlet mapping"] E["Expression Language 6.0"] W["WebSocket 2.2"] A["Annotations 3.0"] end subgraph WAR["WEB-INF/lib of the WAR"] F["Faces: Mojarra or MyFaces"] C["Weld: CDI 4.1, container, servlet integration"] V["Hibernate Validator, optional"] J["Parsson, Mojarra with f:websocket"] end S --> F F --> E F --> C F -.-> V F -.-> J F -.-> W

Tomcat’s web application class loader looks in the WAR before it delegates, except for the Jakarta EE APIs that Tomcat implements itself (Servlet, JSP, EL and WebSocket), where it always delegates first. That is why a JAR with jakarta.servlet.* classes in WEB-INF/lib does not replace Tomcat’s own, and why it is pointless. In the run below, jakarta.annotation.PostConstruct was loaded from Tomcat’s annotations-api.jar even when the WAR also carried jakarta.annotation-api-3.0.0.jar; the Maven build here keeps that JAR out of the WAR.

Which Combinations Work

The matrix is what happened on Tomcat 11.0.26 with the Bookshelf pages (catalogue, a session-scoped counter, a form post). Only "Mojarra 4.1.17 or MyFaces 4.1.4 with Weld 6.0.4.Final" is a combination to ship; the others are here because the failures are instructive.

Faces CDI container Result

Mojarra 4.1.17

Weld 6.0.4.Final (CDI 4.1)

Works. This is the recommended pair.

MyFaces 4.1.4

Weld 6.0.4.Final

Works.

Mojarra 4.0.26

Weld 6.0.4.Final or Weld 5.1.7.Final (CDI 4.0)

Works.

MyFaces 4.1.4

Weld 5.1.7.Final

Works in a quick check, although the MyFaces 4.1 page lists CDI 4.1 as the requirement.

Mojarra 4.1.17

Weld 5.1.7.Final

The context fails to start: ClassNotFoundException: jakarta.enterprise.inject.spi.el.ELAwareBeanManager. That interface is new in the CDI 4.1 API, so Mojarra 4.1 needs a CDI 4.1 container.

Mojarra 4.1.17

Weld 7.0.0.Final (CDI 5, the Jakarta EE 12 line)

Started and served the quick-check pages. Unsupported with Faces 4.1; do not ship it.

MyFaces 4.1.4

Weld 7.0.0.Final

The first request fails with NoSuchMethodError: 'jakarta.el.ELResolver jakarta.enterprise.inject.spi.BeanManager.getELResolver()'.

Mojarra 4.1.17

no CDI container

The context fails to start with ClassNotFoundException: jakarta.enterprise.inject.spi.el.ELAwareBeanManager (caused by a NoClassDefFoundError).

MyFaces 4.1.4

no CDI container

The context fails to start: ClassNotFoundException: jakarta.enterprise.context.spi.Context from StartupServletContextListener.

The Weld reference guide says Tomcat 10.1 is the release Weld Servlet is tested with; the runs here on Tomcat 11.0.26 with Weld 6.0.4.Final worked for both implementations, and the Mojarra README names weld-servlet-shaded 6.0.x for Servlet containers. Weld 7.0.0.Final exists as the CDI 5 line for Jakarta EE 12; stay on 6.0.x with Faces 4.1.

The Maven Build

The WAR has three kinds of dependency: APIs that Tomcat already provides (compile-time only), the pieces you must ship, and optional add-ons. The same pom.xml builds the Mojarra WAR by default and the MyFaces WAR with mvn -P myfaces package, because the two implementations are profiles.

pom.xml: Mojarra by default, MyFaces with -P myfaces
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>bookshelf</artifactId>
  <version>1.0.0</version>
  <packaging>war</packaging>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <failOnMissingWebXml>false</failOnMissingWebXml>
    <mojarra.version>4.1.17</mojarra.version>
    <myfaces.version>4.1.4</myfaces.version>
    <weld.version>6.0.4.Final</weld.version>
    <hibernate-validator.version>9.1.4.Final</hibernate-validator.version>
    <parsson.version>1.1.9</parsson.version>
    <jandex.version>3.6.0</jandex.version>
  </properties>

  <dependencies>
    <!-- Compile-time APIs only. Tomcat supplies Servlet and EL; Weld (below) supplies the CDI API. -->
    <dependency>
      <groupId>jakarta.platform</groupId>
      <artifactId>jakarta.jakartaee-web-api</artifactId>
      <version>11.0.0</version>
      <scope>provided</scope>
    </dependency>

    <!-- Tomcat 11 ships Jakarta Annotations 3.0; keep the copies pulled in by Weld and Mojarra out of the WAR -->
    <dependency>
      <groupId>jakarta.annotation</groupId>
      <artifactId>jakarta.annotation-api</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
    </dependency>

    <!-- CDI container: the shaded artifact carries the CDI API, Weld core and the servlet integration -->
    <dependency>
      <groupId>org.jboss.weld.servlet</groupId>
      <artifactId>weld-servlet-shaded</artifactId>
      <version>${weld.version}</version>
    </dependency>

    <!-- Optional: lets Weld scan bean archives faster than with reflection -->
    <dependency>
      <groupId>io.smallrye</groupId>
      <artifactId>jandex</artifactId>
      <version>${jandex.version}</version>
    </dependency>

    <!-- Optional: Bean Validation provider (needed for constraints on Faces input) -->
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>${hibernate-validator.version}</version>
    </dependency>

    <!-- Optional: Jakarta JSON Processing, needed by Mojarra only when f:websocket is used -->
    <dependency>
      <groupId>org.eclipse.parsson</groupId>
      <artifactId>parsson</artifactId>
      <version>${parsson.version}</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>bookshelf</finalName>
  </build>

  <profiles>
    <profile>
      <id>mojarra</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <dependencies>
        <dependency>
          <groupId>org.glassfish</groupId>
          <artifactId>jakarta.faces</artifactId>
          <version>${mojarra.version}</version>
        </dependency>
      </dependencies>
    </profile>
    <profile>
      <id>myfaces</id>
      <dependencies>
        <dependency>
          <groupId>org.apache.myfaces.core</groupId>
          <artifactId>myfaces-api</artifactId>
          <version>${myfaces.version}</version>
        </dependency>
        <dependency>
          <groupId>org.apache.myfaces.core</groupId>
          <artifactId>myfaces-impl</artifactId>
          <version>${myfaces.version}</version>
        </dependency>
      </dependencies>
    </profile>
  </profiles>
</project>

What each block does:

  • jakarta.jakartaee-web-api with provided scope is only for compiling. It contains the Faces, CDI, Servlet, EL, Validation and WebSocket APIs, so nothing from it reaches the WAR. The run-time classes of Servlet, EL and WebSocket come from Tomcat and those of the rest from the JARs below. Do not use compile scope for it: a second copy of jakarta.faces.* or jakarta.servlet.* in WEB-INF/lib is the classic source of class-loading errors.

  • jakarta.annotation-api is declared provided on purpose. Weld and Mojarra both pull version 3.0.0 in as a compile dependency, Tomcat 11 already contains Annotations 3.0, and a declaration at the top level wins over the transitive ones, so the JAR stays out of the WAR. Removing it by hand from WEB-INF/lib of an otherwise identical WAR also worked on both implementations.

  • weld-servlet-shaded is the single-JAR variant of Weld Servlet. The Weld guide also documents weld-servlet-core, which fetches the CDI API and Weld core transitively; I did not run that variant.

  • hibernate-validator and parsson are the optional add-ons (see Bean Validation on Tomcat and JSON Processing and f:websocket). jandex is optional too: with it on the class path Weld logs WELD-ENV-000020: Using jandex for bean discovery instead of falling back to Java reflection (WELD-ENV-000014). Both worked; Jandex only speeds up the scan.

  • The Faces implementation is the only part that differs between the profiles. Mojarra is one JAR that holds the API and the implementation. MyFaces is two JARs, myfaces-api and myfaces-impl.

The Mojarra WAR contains nine JARs (Faces, Weld, Jandex, Hibernate Validator with its three dependencies, Parsson and the JSON API) and the MyFaces WAR ten, which is the whole list: no Servlet, EL, annotation or CDI-API JAR next to Weld.

The Deployment Descriptors

Three small files complete the WAR. web.xml is optional for Faces (Faces registers FacesServlet itself on a Servlet container), but every real application sets the project stage and an explicit mapping.

src/main/webapp/WEB-INF/web.xml
<?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">
  <servlet>
    <servlet-name>facesServlet</servlet-name>
    <servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>facesServlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>
  <context-param>
    <param-name>jakarta.faces.PROJECT_STAGE</param-name>
    <param-value>Production</param-value>
  </context-param>
  <context-param>
    <param-name>jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT</param-name>
    <param-value>true</param-value>
  </context-param>
  <welcome-file-list>
    <welcome-file>catalogue.xhtml</welcome-file>
  </welcome-file-list>

  <!-- Declares the BeanManager reference that META-INF/context.xml defines (see the BeanManager section) -->
  <resource-env-ref>
    <resource-env-ref-name>BeanManager</resource-env-ref-name>
    <resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
  </resource-env-ref>
</web-app>
  • The facesServlet mapping to *.xhtml is the one the Mojarra README recommends. Without it Faces still registers itself on *.xhtml, *.jsf, *.faces and /faces/*, so the same view answers on four URLs (checked on both implementations with the servlet and its mapping removed from web.xml). An explicit mapping replaces the implicit ones. Project Setup explains the mappings.

  • jakarta.faces.PROJECT_STAGE is Production (see Configuration and Project Stages).

  • jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT is needed only for f:websocket (JSON Processing and f:websocket); without it Mojarra fails when it renders the tag and MyFaces renders it but the handshake fails.

  • The resource-env-ref belongs to the BeanManager binding and is explained in The BeanManager Binding in META-INF/context.xml. It is the one line of this file that is specific to Tomcat: Jetty refuses to deploy the WAR with it (Jetty 12).

src/main/webapp/WEB-INF/beans.xml: the file that makes the WAR a bean archive
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_1.xsd"
       version="4.1" bean-discovery-mode="annotated"/>

The file is not optional on Tomcat; beans.xml and Bean Discovery shows what happens without it. faces-config.xml is optional as well; the Bookshelf one only registers the resource bundle under the variable msg:

src/main/webapp/WEB-INF/faces-config.xml
<?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>
  </application>
</faces-config>

The Java classes of the Bookshelf WAR are the ones used elsewhere in this section (the Book record, the application-scoped BookService, the request-scoped CatalogueBean, the layout template and the msg bundle) plus RuntimeInfoBean from the Jakarta EE servers page, so the identical sources run on Tomcat and on the servers. Three classes are specific to this page and appear below: NoteBean (validation), AnnounceServlet (WebSocket) and BeanManagerInfoServlet (the BeanManager diagnostics).

How Weld Hooks Into Tomcat

Weld’s Servlet module needs no declaration in web.xml. weld-servlet-shaded registers a ServletContainerInitializer (org.jboss.weld.environment.servlet.EnhancedListener), Tomcat calls it before the application starts, and it scans the bean archives, starts the CDI container and makes the BeanManager available. The log of the Mojarra run shows the sequence:

INFO [main] EnhancedListener.onStartup WELD-ENV-001008: Initialize Weld using ServletContainerInitializer
INFO [main] WeldStartup.<clinit> WELD-000900: 6.0.4 (Final)
INFO [main] DiscoveryStrategyFactory.create WELD-ENV-000020: Using jandex for bean discovery
INFO [main] WeldStartup.startContainer WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
INFO [main] TomcatContainer.initialize WELD-ENV-001100: Tomcat 7+ detected, CDI injection will be available in Servlets, Filters and Listeners.
INFO [main] ConfigureListener.contextInitialized Initializing Mojarra 4.1.17 for context '/bookshelf'
INFO [main] Version.<clinit> HV000001: Hibernate Validator 9.1.4.Final

The MyFaces run prints the same Weld lines, then MyFacesContainerInitializer.onStartup, Reading config /WEB-INF/faces-config.xml, MyFaces Core Bean Validation support enabled and MyFaces Core has started. Weld starts first in both runs, which matters: both Faces implementations ask the CDI container for the BeanManager while they initialize (The BeanManager Binding in META-INF/context.xml).

sequenceDiagram participant T as Tomcat participant W as Weld, EnhancedListener participant F as Faces startup listener participant S as FacesServlet T->>W: ServletContainerInitializer.onStartup W->>W: scan the bean archives, start the CDI container W->>T: TomcatContainer, enable injection in servlets, filters and listeners T->>F: contextInitialized F->>W: look up the BeanManager F->>F: read faces-config.xml, register the CDI-backed artefacts T->>S: load-on-startup init Note over S: first request, Restore View starts the lifecycle

What the log lines mean:

  • WELD-ENV-001100 is the Tomcat integration (org.jboss.weld.environment.tomcat.TomcatContainer). It makes Tomcat create servlets, filters and listeners through Weld, so @Inject works in them. The AnnounceServlet below depends on it, and so does a @WebListener that injects a bean. Weld detects the container by itself; the guide documents the servlet initialization parameter org.jboss.weld.environment.container.class for the rare case where detection fails, with TomcatContainer, JettyContainer and UndertowContainer as values (not run).

  • WELD-000101 is information, not a problem. Weld Servlet has no JTA integration: the guide states that session beans, @EJB, @PersistenceContext and transactional events are not supported in a Servlet container. @Transactional and EJBs are therefore a reason to use an application server or a transaction manager that you configure yourself (see Persistence and Services).

  • You do not need to register org.jboss.weld.environment.servlet.Listener in web.xml on Tomcat. Adding it was harmless (the log gains WELD-ENV-001006 and WELD-ENV-001009, "used for ServletRequest and HttpSession notifications") and both implementations ran the same. The Weld guide describes the manual listener for containers without ServletContainerInitializer support and for control over the listener order, and describes org.jboss.weld.module.web.servlet.WeldTerminalListener as the last listener for code that needs the session context in HttpSessionListener.sessionDestroyed(); neither was needed here and the second was not run.

  • org.jboss.weld.environment.servlet.archive.isolation is a servlet initialization parameter (set it as a context-param). Bean archive isolation is on by default, so alternatives, interceptors and decorators enabled in one archive’s beans.xml apply to that archive only; false flattens the deployment so all beans.xml files merge into one (documented in the guide; not run).

  • WELD_CONTEXT_ID_KEY is a context parameter that gives each deployment its own Weld container (useful for several deployments in one JVM, for example with the Arquillian Tomcat container, per the guide; not run).

beans.xml and Bean Discovery

CDI 4 changed the default bean discovery mode to annotated: only classes with a bean-defining annotation (a scope such as @RequestScoped, a stereotype, @Dependent and so on) are beans, and a class that has only @Named is not. The Bookshelf WAR has a TipBean with only @Named, a request-scoped NoteBean and a session-scoped VisitBean for the experiment below (visits.xhtml prints #{visitBean.visits} and #{tipBean.text}). The table is what each WEB-INF/beans.xml variant produced on Tomcat; the results were identical for Mojarra and MyFaces unless a row says otherwise.

WEB-INF/beans.xml Result

version="4.1" bean-discovery-mode="annotated"

The scoped beans are found; TipBean (only @Named) is not (#{tipBean.text} renders empty).

version="4.1" bean-discovery-mode="all"

Every class is a bean, so TipBean works too. Weld also has to load every class: when a class refers to a type that is not on the class path, for example a ConstraintValidator without the Validation API, Weld logs WELD-000119: Not generating any bean definitions from <class> because of underlying class loading error: Type jakarta.validation.ConstraintValidator not found (seen with a test class in a WAR without the Validation API). Avoid all for an application you control.

<beans version="4.1"/> (no mode)

Same as annotated, the CDI 4 default.

a zero-byte file

Same as annotated. With the context parameter org.jboss.weld.environment.servlet.emptyBeansXmlModeAll set to true, Weld treats the empty file as all (the legacy CDI 3 behaviour) and TipBean works. The <beans version="4.1"/> file above did not trigger it.

no file at all

Nothing in the WAR is discovered, although the CDI specification allows an implicit bean archive without the file. The Weld guide says Weld Servlet does not support such an archive and excludes it from discovery. Value expressions render empty and the form post fails with HTTP 500 and PropertyNotFoundException: Target Unreachable, identifier [noteBean] resolved to null. On MyFaces the log also has WELD-ENV-000028: Weld initialization skipped - no bean archive found and An error occurred while initializing MyFaces: Cannot invoke "...BeanManager.getBeans(...)" because "bm" is null at start-up. Mojarra starts without a complaint in the log.

The rule for Tomcat is therefore: ship WEB-INF/beans.xml, keep annotated, and give every class that EL or injection must reach a bean-defining annotation. The same applies to application JARs in WEB-INF/lib: each needs its own META-INF/beans.xml to be scanned. Both Faces implementations are CDI portable extensions (they carry META-INF/services/jakarta.enterprise.inject.spi.Extension, Mojarra five entries and MyFaces twelve), so they need no beans.xml from you. The backing beans page and the project setup page of this section explain discovery for beans in general, and the events page notes that @Inject into a phase listener registered in faces-config.xml worked on MyFaces and not on Mojarra in this setup.

The BeanManager Binding in META-INF/context.xml

Faces talks to CDI constantly: the Expression Language resolver that finds #{noteBean}, the producers behind implicit objects such as #{requestScope}, the view, flow and client-window scopes, injection into converters, validators and listeners, and f:websocket. All of it goes through the CDI BeanManager. In CDI 4.1 an application gets it by injection (@Inject BeanManager) or from CDI.current(); the specification describes no JNDI lookup. On a Jakarta EE server the server additionally binds the manager in JNDI, and both Faces implementations look there first, under java:comp/BeanManager.

Tomcat’s JNDI is a read-only naming context that only Tomcat itself fills, and Tomcat only allows entries under java:comp/env. Weld therefore cannot bind the BeanManager on its own. The Weld guide’s answer is a Tomcat Resource in META-INF/context.xml that names a Weld object factory, plus a resource-env-ref in web.xml. This section explains what the binding is, what it changes, how the two implementations behave with and without it, and what breaks when it is wrong. The first thing to know is the result: on Tomcat 11.0.26 with Weld 6.0.4.Final, neither Mojarra 4.1.17 nor MyFaces 4.1.4 needed the binding to run the Bookshelf WAR, because both fall back to CDI.current(). The binding is optional; it is worth having when you want the real manager in JNDI for other code.

How the Two Implementations Find the BeanManager

The order below was read from the class files (javap -c on com.sun.faces.util.Util.getCdiBeanManager(FacesContext) in Mojarra 4.1.17 and on org.apache.myfaces.webapp.FacesInitializerImpl.initCDIIntegration() in MyFaces 4.1.4) and confirmed by the diagnostics below. It is an implementation detail and may change between releases; neither project documents it.

flowchart TB subgraph MOJ["Mojarra 4.1.17: Util.getCdiBeanManager"] M1["cached under com.sun.faces.cdi.BeanManager in the FacesContext attributes or the application map?"] -->|no| M2["JNDI java:comp/BeanManager"] M2 -->|NamingException| M3["JNDI java:comp/env/BeanManager"] M3 -->|NamingException| M4["CDI.current().getBeanManager()"] M4 -->|fails| M5["servlet context attribute set by the Weld integration"] M5 -->|null| M6["IllegalStateException: CDI is not available"] end subgraph MYF["MyFaces 4.1.4: FacesInitializerImpl.initCDIIntegration"] Y1["servlet context attribute jakarta.enterprise.inject.spi.BeanManager"] -->|null| Y2["JNDI java:comp/BeanManager, then java:comp/env/BeanManager"] Y2 -->|null| Y3["CDI.current().getBeanManager()"] Y3 --> Y4["stored under oam.cdi.BEAN_MANAGER_INSTANCE"] end

Source: Apache MyFaces source and Eclipse Mojarra source (the lookup order was read from the class files of the versions named above).

Mojarra 4.1.17 MyFaces 4.1.4

When

On demand, the first time a Faces artefact needs it; the result is cached in the application map under com.sun.faces.cdi.BeanManager.

Once at start-up (initFaces); the result is stored in the application map under oam.cdi.BEAN_MANAGER_INSTANCE.

JNDI names tried

java:comp/BeanManager, then java:comp/env/BeanManager

java:comp/BeanManager, then java:comp/env/BeanManager, after the servlet context attribute

Fallback

CDI.current().getBeanManager(), then the attribute that Weld Servlet sets on the servlet context

CDI.current().getBeanManager()

No manager found

IllegalStateException: CDI is not available

The CDI integration is not installed; with Weld skipped the start-up error shown in beans.xml and Bean Discovery follows.

On Tomcat the first JNDI name never works (Tomcat binds nothing outside java:comp/env), so with the binding in place the second lookup succeeds, and without it both fail with a NamingException that the implementations swallow before they use CDI.current().

Piece 1: The Resource Element

src/main/webapp/META-INF/context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <!-- Binds Weld's BeanManager at java:comp/env/BeanManager -->
  <Resource name="BeanManager"
            auth="Container"
            type="jakarta.enterprise.inject.spi.BeanManager"
            factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>

The Context element is Tomcat’s per-web-application configuration, and a Resource inside it declares what a JNDI lookup under java:comp/env returns.

  • name="BeanManager" is the name relative to java:comp/env, so the entry is java:comp/env/BeanManager.

  • type is the Java type that a lookup is expected to produce, jakarta.enterprise.inject.spi.BeanManager.

  • factory names the object factory, org.jboss.weld.resources.ManagerObjectFactory from Weld Servlet. It implements javax.naming.spi.ObjectFactory: Tomcat calls it to produce the object behind the name, and it returns Weld’s BeanManager for the web application. Tomcat’s general advice for custom factories is to put them under $CATALINA_HOME/lib; here the factory sits in WEB-INF/lib of the WAR (inside weld-servlet-shaded) and was found there, so no change to the Tomcat installation is needed.

  • auth="Container" is what the Weld guide shows. The Tomcat documentation says the attribute is required when the application declares a resource-ref and optional with a resource-env-ref; the lookup worked the same with the attribute left out.

Piece 2: The resource-env-ref in web.xml

The matching entry in web.xml (already part of the descriptor above):

<resource-env-ref>
  <resource-env-ref-name>BeanManager</resource-env-ref-name>
  <resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
</resource-env-ref>

A resource-env-ref is the Jakarta EE way to declare that the application uses a resource by a logical name. Tomcat links it to the Resource of the same name. On Tomcat 11.0.26 the Resource alone was enough for the lookup to succeed, so the web.xml entry documents the dependency rather than creating it; the Weld guide asks for both and this page keeps both. The reverse is not true: the web.xml entry without the Resource creates nothing (see the failure table below).

Piece 3: Where the Context Fragment Lives

META-INF/context.xml in the WAR is only one of the places Tomcat reads a Context from. Per the Tomcat documentation:

  • /META-INF/context.xml inside the web application (used above). If the host’s copyXML attribute is true, Tomcat copies it to $CATALINA_BASE/conf/<engine>/<host>/<name>.xml and uses the copy on later starts.

  • $CATALINA_BASE/conf/<engine>/<host>/<name>.xml (for example conf/Catalina/localhost/bookshelf.xml). This file takes precedence over the one in the WAR. Putting the Resource there worked with a plain bookshelf.war that contained no context.xml.

  • A Context inside the Host of server.xml, which Tomcat discourages, or the shared conf/context.xml, which would apply the entry to every web application.

Security-conscious installations set deployXML="false" on the Host, so that applications cannot bring their own container configuration. With that setting, Tomcat 11.0.26 refused to deploy the Bookshelf WAR at all and logged The web application with context path [/bookshelf] was not deployed because it contained a deployment descriptor [META-INF/context.xml] which may include configuration necessary for the secure deployment of the application but processing of deployment descriptors is prevented by the deployXML setting of this host. An appropriate descriptor should be created at [...]/conf/Catalina/localhost/bookshelf.xml to deploy this application. In that setup the administrator provides the file under conf/Catalina/localhost, as in the example below, and the WAR carries no context.xml.

$CATALINA_BASE/conf/Catalina/localhost/bookshelf.xml: the same binding supplied by the administrator
<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <Resource name="BeanManager"
            auth="Container"
            type="jakarta.enterprise.inject.spi.BeanManager"
            factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>

What the Binding Changes

The diagnostic servlet prints every place the manager can be found and which instance each Faces implementation cached.

BeanManagerInfoServlet.java: diagnostics only, never deploy it to production
package org.example.bookshelf.web;

import jakarta.enterprise.inject.spi.CDI;
import jakarta.servlet.ServletContext;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import javax.naming.InitialContext;
import javax.naming.NamingException;

/** Diagnostic only: shows where the BeanManager can be found. Do not deploy it to production. */
@WebServlet("/diagnostics/beanmanager")
public class BeanManagerInfoServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        ServletContext ctx = getServletContext();

        out.println("CDI.current()          = " + CDI.current().getBeanManager().getClass().getName());
        out.println("JNDI java:comp/BeanManager     = " + lookup("java:comp/BeanManager"));
        out.println("JNDI java:comp/env/BeanManager = " + lookup("java:comp/env/BeanManager"));
        // Mojarra caches its choice under this key, MyFaces under the second one
        out.println("Mojarra cache  = " + name(ctx.getAttribute("com.sun.faces.cdi.BeanManager")));
        out.println("MyFaces cache  = " + name(ctx.getAttribute("oam.cdi.BEAN_MANAGER_INSTANCE")));
        out.println("Weld attribute = "
                + name(ctx.getAttribute("org.jboss.weld.environment.servlet.jakarta.enterprise.inject.spi.BeanManager")));
    }

    private static String lookup(String jndiName) {
        try {
            return name(new InitialContext().lookup(jndiName));
        } catch (NamingException e) {
            return e.getClass().getSimpleName() + ": " + e.getMessage();
        }
    }

    private static String name(Object value) {
        return value == null ? "null" : value.getClass().getName();
    }
}

Its output on Tomcat 11.0.26 for the four ways of deploying the Mojarra WAR (full is the descriptor and context.xml above; the MyFaces WAR printed the same with the MyFaces cache line in place of the Mojarra cache line):

Deployment java:comp/env/BeanManager Instance cached by Mojarra Page behaviour

context.xml and resource-env-ref

org.jboss.weld.manager.BeanManagerImpl

BeanManagerImpl

Works.

context.xml only

BeanManagerImpl

BeanManagerImpl

Works.

resource-env-ref only

NamingException: Could not create resource instance

org.jboss.weld.bean.builtin.BeanManagerProxy

Works; the lookup failed and CDI.current() was used.

neither

NameNotFoundException: Name [BeanManager] is not bound in this Context

BeanManagerProxy

Works; same fallback.

java:comp/BeanManager printed NameNotFoundException in all four cases, and CDI.current().getBeanManager() always returned the BeanManagerProxy. The Weld servlet context attribute held the BeanManagerImpl. The binding therefore changes one thing in Faces: the implementation caches the real manager instead of Weld’s proxy. A wrong factory class name gave NamingException: Could not load resource factory class for the lookup and the page still worked through the fallback. In every row the catalogue, the validation form and the session counter behaved identically on both implementations.

Why keep it, then?

  • Other code looks the manager up in JNDI. OmniFaces Beans.getManager() tries CDI.current() and, if that fails, falls back to java:comp/BeanManager and java:comp/env/BeanManager (read from the 5.5.5 class file). Portable libraries and older code written for application servers do the same. Without the binding such a fallback finds nothing.

  • A real BeanManagerImpl is cached, not a proxy that must be resolved through the thread’s context class loader on each use. I did not find a failure that this prevents on Tomcat 11.0.26; it is a robustness argument, not a demonstrated fix.

  • The binding is what the Weld guide documents as the supported way to expose the manager on Tomcat.

If nothing in your application or its libraries needs JNDI, leaving the binding out is a legitimate choice, and it removes the Tomcat-only lines that make the WAR non-portable.

Where the Binding Does Not Apply

  • Jetty has no context.xml and refuses a resource-env-ref that nothing binds: the Bookshelf WAR with the entry failed to start with IllegalStateException: Nothing to bind for name BeanManager (HTTP 503). Remove the entry from the WAR you deploy to Jetty. The Jetty documentation says such names must be bound in Jetty XML (not run). With the entry removed both implementations used CDI.current() (Jetty 12).

  • Embedded Tomcat has JNDI switched off. Without tomcat.enableNaming() both JNDI lookups failed with NoInitialContextException and the implementations used CDI.current(); with enableNaming() the META-INF/context.xml of the exploded WAR was honoured and java:comp/env/BeanManager returned the BeanManagerImpl (Embedded Tomcat).

  • Jakarta EE servers bind java:comp/BeanManager themselves. The context.xml is ignored there (WildFly, GlassFish, Payara, Open Liberty and TomEE were not run with this WAR). A resource-env-ref for a name the server does not provide may be a deployment problem, so keep the Tomcat-specific lines in a Tomcat-specific profile or descriptor when one WAR targets several runtimes.

Shutdown

Stopping the web application showed a difference between the implementations that is independent of the binding. Mojarra stopped without a message. MyFaces 4.1.4 logged a SEVERE entry from StartupServletContextListener while destroying itself, because Weld had already shut down: WELD-001333: BeanManager method getBeans() is not available after shutdown with the proxy, and WELD-001456: Argument bean must not be null with the JNDI-bound manager. Tomcat then added checkThreadLocalMapForLeaks entries mentioning StartupFacesContextImpl. The application kept working until it was stopped, and I found no effect other than the log noise, but expect those lines on every redeploy of a MyFaces application on Tomcat 11.0.26 with Weld 6.0.4 and do not read them as a failure of your own code.

Bean Validation on Tomcat

Bean Validation is optional on Tomcat. Faces calls it for any input component whose value expression points at a bean property with constraints (see Bean Validation for the lifecycle and f:validateBean).

  • No provider on the class path. The constraints are silently not enforced. A 250-character note and a note that was a single space (which @NotBlank rejects) were both saved by the Bookshelf form on both implementations, also when only the Validation API JAR was left out; MyFaces logged MyFaces Core Bean Validation support disabled at start-up, Mojarra said nothing. A form that relies on @NotBlank therefore accepts blanks until a provider is added. This is the failure to watch for: it produces no error.

  • Hibernate Validator on the class path (the dependency in the pom.xml above). Mojarra and MyFaces both pick it up, with no configuration, through the Validation API’s service lookup; MyFaces logs Bean Validation support enabled. The messages are interpolated with Tomcat’s EL implementation, so ${validatedValue} in a message works without an extra EL JAR. Never build a constraint message from user input: the template is evaluated as an expression.

  • Constraint validators that use @Inject. On Tomcat the @Inject field stays null: the form fails with HTTP 500 and ValidationException: HV000028: Unexpected exception during isValid call caused by a NullPointerException, with Hibernate Validator alone and with hibernate-validator-cdi added. Faces builds its own default ValidatorFactory, which knows nothing about the bean manager. The fix, which was run on both implementations, is to inject the CDI-aware factory from a listener and give it to Faces under BeanValidator.VALIDATOR_FACTORY_KEY; the Bean Validation page shows the listener (the injection section and the ValidatorFactory section), so it is not repeated here. The listener needs hibernate-validator-cdi, and without that JAR the context fails to start because there is nothing to inject.

NoteBean.java: constraints on the property that the form binds
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/** A short reader note: Bean Validation constraints are checked by Faces in Process Validations. */
@Named
@RequestScoped
public class NoteBean {

    @NotBlank
    @Size(max = 200)
    private String text;
    private String saved;

    public String getText() { return text; }
    public void setText(String text) { this.text = text; }
    public String getSaved() { return saved; }

    public void save() {
        saved = "Saved " + text.length() + " characters.";
    }
}
note.xhtml: the form
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:ui="jakarta.faces.facelets">
<ui:composition template="/WEB-INF/templates/layout.xhtml">
  <ui:define name="title">Reader note</ui:define>
  <ui:define name="content">
    <h:form id="noteForm">
      <h:messages id="msgs"/>
      <h:inputTextarea id="text" value="#{noteBean.text}"/>
      <h:commandButton id="save" value="Save" action="#{noteBean.save}"/>
      <p><h:outputText id="saved" value="#{noteBean.saved}"/></p>
    </h:form>
  </ui:define>
</ui:composition>
</html>

A valid note answers Saved 11 characters.; a blank one must not be blank and a 250-character one size must be between 0 and 200. The message text is the same on both implementations. The only difference in the page is the presentation of h:messages: MyFaces prefixes the client id (noteForm:text: must not be blank) and Mojarra shows the bare summary.

JSON Processing and f:websocket

f:websocket and @Push need three things on Tomcat: the jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT context parameter (in the web.xml above), the WebSocket API and implementation (Tomcat has both), and, for Mojarra, a JSON Processing implementation. Tomcat has no JSON-P. The Bookshelf "new arrivals" page and the servlet that pushes to it:

arrivals.xhtml: the page that listens on the channel
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<h:head>
  <title>New arrivals</title>
  <script>
    function onArrival(message) {
      document.getElementById('status').textContent = 'Update: ' + message;
    }
  </script>
</h:head>
<h:body>
  <f:websocket channel="arrivals" scope="application" onmessage="onArrival"/>
  <p id="status">Waiting for new arrivals.</p>
</h:body>
</html>
AnnounceServlet.java: injects the push context and sends one message (POST only; protect it in a real application)
package org.example.bookshelf.web;

import jakarta.faces.push.Push;
import jakarta.faces.push.PushContext;
import jakarta.inject.Inject;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

/** Tells every open catalogue page that the shelf changed. Protect it with a security constraint in a real application. */
@WebServlet("/arrivals/announce")
public class AnnounceServlet extends HttpServlet {

    @Inject
    @Push(channel = "arrivals")
    private PushContext arrivals;

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        arrivals.send("catalogue-changed");
        resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
    }
}

The WebSocket Push page covers the tag, scopes, security and clustering. Observed on Tomcat 11.0.26, with a Java client that loads the page, opens the socket and triggers the servlet:

ArrivalsClient.java: opens the socket and prints the raw frame
import java.net.CookieManager;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Opens the page's WebSocket, triggers the push and prints the raw frame: java ArrivalsClient.java http://localhost:8080/bookshelf */
public class ArrivalsClient {

    public static void main(String[] args) throws Exception {
        String base = args[0];
        HttpClient client = HttpClient.newBuilder().cookieHandler(new CookieManager()).build();
        String html = client.send(HttpRequest.newBuilder(URI.create(base + "/arrivals.xhtml")).build(),
                HttpResponse.BodyHandlers.ofString()).body();
        // The f:websocket tag renders a call such as faces.push.init('id','/bookshelf/jakarta.faces.push/arrivals?token', ...)
        Matcher m = Pattern.compile("faces\\.push\\.init\\('[^']*','([^']*)'").matcher(html);
        if (!m.find()) {
            System.out.println("no faces.push.init in the page: is ENABLE_WEBSOCKET_ENDPOINT set?");
            return;
        }
        String path = m.group(1);
        URI socket = URI.create(path.startsWith("/")
                ? "ws://" + URI.create(base).getAuthority() + path : path);

        CountDownLatch received = new CountDownLatch(1);
        WebSocket ws = client.newWebSocketBuilder().buildAsync(socket, new WebSocket.Listener() {
            @Override
            public void onOpen(WebSocket webSocket) {
                System.out.println("open");
                webSocket.request(1);
            }

            @Override
            public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
                System.out.println("raw frame: " + data);
                received.countDown();
                return null;
            }
        }).get(5, TimeUnit.SECONDS);

        Thread.sleep(500);
        int status = client.send(HttpRequest.newBuilder(URI.create(base + "/arrivals/announce"))
                .POST(HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.discarding()).statusCode();
        System.out.println("announce status " + status);
        System.out.println(received.await(5, TimeUnit.SECONDS) ? "received" : "timeout");
        ws.sendClose(WebSocket.NORMAL_CLOSURE, "done");
    }
}
WAR Raw frame received Notes

Mojarra with Parsson

{"data":"catalogue-changed"}

Works. The faces.js of Mojarra 4.1.17 reads JSON.parse(event.data).data, so the object form is what its client expects.

MyFaces with Parsson

"catalogue-changed"

Works. A bare JSON string; the faces.js of MyFaces 4.1.4 hands the parsed message to the page’s function.

Mojarra without Parsson

nothing

The servlet answers 500 with NoClassDefFoundError (root cause ClassNotFoundException: jakarta.json.Json) at com.sun.faces.util.Json.encode. The page itself rendered fine; only the sender failed.

MyFaces without Parsson

"catalogue-changed"

Works without any JSON-P JAR in this run (the WebSocket Push page did not test it).

Add parsson (the Eclipse JSON-P reference implementation; it brings jakarta.json-api) to every Tomcat WAR that uses f:websocket with Mojarra, and to the MyFaces WAR too if you want the same classpath on both. The Mojarra README names org.glassfish:jakarta.json 2.1.x for this role; Parsson 1.1.9 is its successor and worked. The two frame shapes differ, and each implementation’s own client script (read from the 4.1.17 and 4.1.4 JARs) expects its own, so use the script that Faces renders for f:websocket and do not hand-write WebSocket client code against the channel.

Expression Language, JSTL and JSP

Tomcat’s EL 6.0 is the EL for Faces and for Hibernate Validator; do not package an EL JAR (org.glassfish.expressly or jakarta.el-api) in the WAR. Facelets does not use JSP, but Tomcat still has the JSP servlet, which matters only for embedded Tomcat (Embedded Tomcat).

The Facelets tag libraries jakarta.tags.core (c:) and jakarta.tags.functions (fn:) are implemented by Mojarra and MyFaces themselves. A page that used c:if, c:forEach and fn:toUpperCase rendered the same on both implementations with no JSTL JAR in the WAR and none in Tomcat. Build Time versus Render Time explains why c: tags are not interchangeable with ui: and h: ones. In Faces 4.x the namespaces are jakarta.tags.core and jakarta.tags.functions.

jstl-check.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:c="jakarta.tags.core"
      xmlns:fn="jakarta.tags.functions">
<h:head><title>JSTL check</title></h:head>
<h:body>
  <p id="out">c:if = <c:if test="#{true}">yes</c:if>,
     c:forEach = <c:forEach items="#{['a', 'b']}" var="x">#{x}</c:forEach>,
     fn:toUpperCase = #{fn:toUpperCase('abc')}</p>
</h:body>
</html>

The output on Mojarra and on MyFaces was c:if = yes, c:forEach = ab, fn:toUpperCase = ABC.

Running the WAR on a Standalone Tomcat

Build both WARs, then deploy one of them. The steps below keep the Tomcat installation (CATALINA_HOME) separate from the instance configuration and deployments (CATALINA_BASE), which is how Tomcat is meant to be run and what these tests used (on other ports than the default 8080).

mvn clean package                       # Mojarra: target/bookshelf.war
mvn clean package -P myfaces            # MyFaces: target/bookshelf.war (overwrites the first)

curl -fLO https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.26/bin/apache-tomcat-11.0.26.tar.gz
curl -fLO https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.26/bin/apache-tomcat-11.0.26.tar.gz.sha512
shasum -a 512 -c apache-tomcat-11.0.26.tar.gz.sha512        # must print: apache-tomcat-11.0.26.tar.gz: OK
tar xzf apache-tomcat-11.0.26.tar.gz

export CATALINA_HOME="$PWD/apache-tomcat-11.0.26"
export CATALINA_BASE="$PWD/bookshelf-base"
mkdir -p "$CATALINA_BASE"/{conf,logs,temp,webapps,work}
cp "$CATALINA_HOME"/conf/* "$CATALINA_BASE"/conf/
cp target/bookshelf.war "$CATALINA_BASE"/webapps/
"$CATALINA_HOME"/bin/catalina.sh run                        # Ctrl+C stops it

Then open http://localhost:8080/bookshelf/ for the catalogue, /bookshelf/runtime.xhtml for the implementation report (remove that page before a real deployment) and /bookshelf/note.xhtml for the validation form. Tomcat unpacks the WAR into webapps/bookshelf and starts it; stop the server before you replace the files of an exploded deployment.

  • Context root. The WAR file name decides it (bookshelf.war becomes /bookshelf). Parallel versions and the root context follow Tomcat’s own naming rules, described in the Context documentation.

  • Logs. logs/catalina.<date>.log and logs/localhost.<date>.log hold the Weld, Faces and servlet messages used on this page; with catalina.sh run they also go to the console.

  • Containers. A Tomcat image from a registry works the same way: copy the WAR into the image’s webapps. The Docker pages of this documentation cover images and the deployment and clustering page covers the Faces side of containers.

Embedded Tomcat

Tomcat can run inside your own main, which is the usual way to run the same WAR from a test or from a Java launcher. The launcher below starts the exploded WAR that Maven leaves in target/bookshelf.

launcher/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>bookshelf-launcher</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <tomcat.version>11.0.26</tomcat.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>${tomcat.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-el</artifactId>
      <version>${tomcat.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-websocket</artifactId>
      <version>${tomcat.version}</version>
    </dependency>
    <!-- the default web.xml that addWebapp installs declares the JSP servlet -->
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>${tomcat.version}</version>
    </dependency>
  </dependencies>
</project>
EmbeddedBookshelf.java
package org.example.bookshelf.launcher;

import java.io.File;
import org.apache.catalina.startup.Tomcat;

/** Starts an exploded Bookshelf WAR in embedded Tomcat: EmbeddedBookshelf target/bookshelf 8080 */
public class EmbeddedBookshelf {

    public static void main(String[] args) throws Exception {
        File webapp = new File(args[0]).getAbsoluteFile();
        Tomcat tomcat = new Tomcat();
        tomcat.setBaseDir(new File("target/tomcat").getAbsolutePath());
        tomcat.setPort(Integer.parseInt(args[1]));
        tomcat.getConnector();
        if (args.length > 2 && args[2].equals("naming")) {
            tomcat.enableNaming(); // JNDI is off in embedded Tomcat unless you ask for it
        }
        tomcat.addWebapp("/bookshelf", webapp.getPath());
        tomcat.start();
        tomcat.getServer().await();
    }
}
mvn package                                                    # in launcher/, compiles the class
mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt     # the Tomcat classpath
java -cp "target/classes:$(cat cp.txt)" org.example.bookshelf.launcher.EmbeddedBookshelf ../bookshelf/target/bookshelf 8080
  • Weld started through its ServletContainerInitializer as in the standalone server; the programmatic ctx.addApplicationListener("org.jboss.weld.environment.servlet.Listener") that the Weld guide shows for a context created with addContext was not needed with addWebapp. If you build the context by hand with addContext, which skips Tomcat’s web.xml and annotation processing, the guide’s listener registration is required (that variant was not run).

  • tomcat-embed-jasper is in the launcher because addWebapp installs Tomcat’s default web.xml, which declares the JSP servlet. Without it the application still ran but logged SEVERE: Servlet [jsp] in web application [/bookshelf] threw load() exception.

  • JNDI is off in embedded Tomcat. The third argument naming of the launcher calls tomcat.enableNaming(); only then is the META-INF/context.xml binding applied (The BeanManager Binding in META-INF/context.xml). Without it both implementations used CDI.current() and the application worked. Only Mojarra was run in the embedded launcher.

Jetty 12

Eclipse Jetty 12 is the other common Servlet container for Faces. The Weld guide says Jetty 12 and newer are supported, that context activation and injection into servlets, filters and listeners work out of the box, and that a standalone Jetty needs its CDI module; it also warns that Jetty is not regularly tested with Weld. Jetty 12.1.14 with the ee11 modules (Servlet 6.1) ran the Bookshelf WAR on both implementations. The base below is the set used; the CDI and WebSocket modules were shown to be required (see the list after the commands), and ee11-apache-jsp is there because it supplies the Expression Language API and implementation (Jetty has no EL of its own: the diagnostic showed jakarta.el-api-6.0.1.jar, which that module lists, as the source of jakarta.el.ExpressionFactory).

curl -fLO https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/12.1.14/jetty-home-12.1.14.tar.gz
tar xzf jetty-home-12.1.14.tar.gz
export JETTY_HOME="$PWD/jetty-home-12.1.14"

mkdir jetty-base && cd jetty-base
java -jar "$JETTY_HOME"/start.jar --add-modules=http,ee11-deploy,ee11-annotations,ee11-cdi,ee11-apache-jsp,ee11-websocket-jakarta
cp /path/to/bookshelf.war webapps/
java -jar "$JETTY_HOME"/start.jar jetty.http.port=8080

What the runs showed:

  • Weld logged WELD-ENV-001213: Jetty CDI SPI support detected, CDI injection will be available in Listeners, Servlets and Filters instead of the Tomcat line, and the validation form, the catalogue and the session counter behaved as on Tomcat.

  • The ee11-cdi module is required. Without it Weld logged WELD-ENV-001001: No supported servlet container detected, CDI injection will NOT be available in Servlets, Filters or Listeners, the context failed to start with IllegalStateException: Unable to locate CDIProvider (MyFaces run) and every request answered 503.

  • The WebSocket module is required even if you never use f:websocket. Without ee11-websocket-jakarta, Weld’s bootstrap failed with a DefinitionException caused by ClassNotFoundException: jakarta.websocket.CloseReason$CloseCode (Mojarra, from com.sun.faces.push.WebsocketSessionManager) or by the load error of org.apache.myfaces.push.cdi.WebsocketSession (MyFaces): both implementations register CDI beans for the push support, and Weld loads them at start-up. On Tomcat the WebSocket API is always present, which is why the Mojarra README’s "optional" holds there.

  • The resource-env-ref must go. The WAR with the BeanManager reference from The BeanManager Binding in META-INF/context.xml failed with IllegalStateException: Nothing to bind for name BeanManager and answered 503. Jetty ignores META-INF/context.xml, so the same WAR without the web.xml entry runs on both servers. In the Jetty runs java:comp/env/BeanManager was not bound, and both implementations cached the BeanManagerProxy.

  • The Jetty log carries many WARN ... scanned from multiple locations lines about the Servlet API classes that several Jetty JARs contain; they appeared on every start and did not affect the application.

  • Embedded Jetty was not run; the Weld guide says it needs no further configuration.

Tomcat Settings That Matter for Faces

The remaining settings are the ones this page ran. The security, configuration, state-saving and exception-handling pages cover the Faces-level equivalents.

Skip TLD Scanning of the Faces JARs

At start-up Tomcat’s TldScanner opens every JAR of the WAR looking for tag library descriptors and logs INFO ... At least one JAR was scanned for TLDs yet contained no TLDs when some had none. Facelets does not use TLD files, so the scan is wasted work. A JarScanFilter in the same context.xml skips the JARs; the line disappeared on both implementations and everything else worked. The effect on start-up time was not measured.

META-INF/context.xml: the BeanManager binding plus TLD scan skipping
<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <!-- Binds Weld's BeanManager at java:comp/env/BeanManager -->
  <Resource name="BeanManager"
            auth="Container"
            type="jakarta.enterprise.inject.spi.BeanManager"
            factory="org.jboss.weld.resources.ManagerObjectFactory"/>

  <!-- Facelets does not use TLD files: do not scan the JARs of the WAR for them -->
  <JarScanner>
    <JarScanFilter tldSkip="${tomcat.util.scan.StandardJarScanFilter.jarsToSkip},jakarta.faces-*.jar,myfaces-*.jar,weld-servlet-shaded-*.jar,jandex-*.jar,hibernate-validator-*.jar,parsson-*.jar,jakarta.json-api-*.jar,jakarta.validation-api-*.jar,classmate-*.jar,jboss-logging-*.jar"/>
  </JarScanner>
</Context>

The pattern ${tomcat.util.scan.StandardJarScanFilter.jarsToSkip} expands to the list in conf/catalina.properties, so Tomcat’s own skip list is kept and your JARs are appended.

Sessions and Cookies

A Faces application stores the view state in the session unless you use client state, so the session cookie is the credential. These elements in web.xml keep the identifier out of URLs and script-readable code. In the run, the response carried Set-Cookie: JSESSIONID=...; Path=/bookshelf; Secure; HttpOnly and the form action was /bookshelf/note.xhtml; without tracking-mode the first response of a client without a cookie carried ;jsessionid=... in the action.

<session-config>
  <session-timeout>30</session-timeout>
  <cookie-config>
    <http-only>true</http-only>
    <secure>true</secure>
  </cookie-config>
  <tracking-mode>COOKIE</tracking-mode>
</session-config>

A Secure cookie is only returned over HTTPS, so terminate TLS in front of Tomcat (or in its connector) before you set it, and test the login and the postbacks over HTTPS. Security covers cookies, CSRF, headers and authentication, and State Saving covers the client-state key (which on Tomcat is the JNDI entry java:comp/env/faces/ClientSideSecretKey for Mojarra or org.apache.myfaces.SECRET for MyFaces; set it from the environment, never in a file in the repository).

Production Hardening of Tomcat Itself

Tomcat ships with web applications and settings meant for a development machine. Its security how-to recommends removing what you do not use and says the following about the defaults: the examples application should always be removed from a security-sensitive installation, the docs and ROOT applications identify the Tomcat version and are normally removed from a public instance, and the manager and host-manager applications allow remote deployment and are frequent targets, so leave them out unless you need them and then follow the guide’s section on securing them. The shutdown port in server.xml should be disabled (port="-1" on the Server element) or protected with a strong password. A CATALINA_BASE created as above contains none of these applications because you created webapps yourself. The error page that Tomcat prints for an unhandled exception shows a stack trace; Exception Handling shows error-page mappings and the ErrorReportValve settings.

Troubleshooting

Every message below was produced by the variant named in the second column; the fixes were tried.

Symptom Cause Fix

The context fails to start with ClassNotFoundException: jakarta.enterprise.inject.spi.el.ELAwareBeanManager

Mojarra 4.1 without a CDI container, or with Weld 5 (CDI 4.0)

Add weld-servlet-shaded 6.0.x.

The context fails to start with ClassNotFoundException: jakarta.enterprise.context.spi.Context from StartupServletContextListener

MyFaces without a CDI container

Add weld-servlet-shaded 6.0.x.

WELD-ENV-000028: Weld initialization skipped - no bean archive found, then An error occurred while initializing MyFaces ... "bm" is null

No WEB-INF/beans.xml (MyFaces)

Add the file (beans.xml and Bean Discovery).

HTTP 500 with PropertyNotFoundException: Target Unreachable, identifier [noteBean] resolved to null; value expressions render empty

Bean not discovered: no beans.xml, or a @Named class without a scope in annotated mode

Add beans.xml and a scope annotation.

Constraints on Faces input are ignored without any message

No Bean Validation provider in the WAR

Add hibernate-validator (Bean Validation on Tomcat).

ValidationException: HV000028: Unexpected exception during isValid call with a NullPointerException in your validator

@Inject in a ConstraintValidator; Faces uses a ValidatorFactory that does not know CDI

Publish the CDI-aware factory from a listener (Bean Validation page).

NoClassDefFoundError: jakarta/json/Json when a message is pushed (Mojarra)

No JSON-P implementation

Add parsson (JSON Processing and f:websocket).

NoSuchMethodError: ... BeanManager.getELResolver() on the first request (MyFaces)

Weld 7 (CDI 5) with Faces 4.1

Use Weld 6.0.x.

Jetty: 503, IllegalStateException: Unable to locate CDIProvider

The ee11-cdi module is not enabled

--add-modules=ee11-cdi (Jetty 12).

Jetty: Weld DefinitionException, ClassNotFoundException: jakarta.websocket.CloseReason$CloseCode

The WebSocket module is not enabled

--add-modules=ee11-websocket-jakarta.

Jetty: 503, Nothing to bind for name BeanManager

resource-env-ref with no binding

Remove it from the WAR for Jetty.

Tomcat: was not deployed because it contained a deployment descriptor [META-INF/context.xml]

deployXML="false" on the host

Put the Context in conf/Catalina/localhost/bookshelf.xml (The BeanManager Binding in META-INF/context.xml).

Embedded: SEVERE: Servlet [jsp] ... threw load() exception

The JSP servlet of the default web.xml has no Jasper

Add tomcat-embed-jasper (Embedded Tomcat).

MyFaces on undeploy: SEVERE ... WELD-001333 or WELD-001456 from StartupServletContextListener

MyFaces destroys itself after Weld has shut down

Expected with 4.1.4 and Weld 6.0.4; no fix tried (The BeanManager Binding in META-INF/context.xml).

INFO ... At least one JAR was scanned for TLDs yet contained no TLDs, WELD-ENV-000014: Falling back to Java Reflection, WELD-000101: Transactional services not available

Information only

Optional: JarScanFilter, Jandex. The last one cannot be fixed on Tomcat.

Mojarra vs MyFaces on Tomcat

  • Start-up without a beans.xml. Mojarra starts and fails at the first expression; MyFaces logs two start-up messages and fails the same way (beans.xml and Bean Discovery).

  • BeanManager lookup. Mojarra looks it up on demand and caches it under com.sun.faces.cdi.BeanManager; MyFaces looks it up once at start-up and stores it under oam.cdi.BEAN_MANAGER_INSTANCE; both try JNDI before CDI.current() (The BeanManager Binding in META-INF/context.xml).

  • Shutdown. MyFaces 4.1.4 logs SEVERE entries while destroying itself on Tomcat with Weld 6.0.4; Mojarra does not.

  • Weld versions. Mojarra 4.1 needs Weld 6 (it fails with Weld 5); MyFaces 4.1.4 ran with Weld 5.1.7 and 6.0.4 and failed with Weld 7.0.0.

  • JSON Processing. Mojarra needs a JSON-P implementation to push; MyFaces did not in this run. The wire formats differ.

  • Messages. h:messages showed the bare summary on Mojarra and clientId: summary on MyFaces for the same validation failure.

  • Jetty. Both need the CDI and WebSocket modules and both fail with the resource-env-ref.

  • Weld servlet attribute. Mojarra falls back to the attribute that Weld Servlet sets on the servlet context (org.jboss.weld.environment.servlet.jakarta.enterprise.inject.spi.BeanManager); MyFaces reads the attribute named jakarta.enterprise.inject.spi.BeanManager, which was null here, and then CDI.current().

What Changed

  • JSF 1.x and 2.0 to 2.2 on Tomcat. The API and implementation JARs, the JSTL JARs and a web.xml with the Faces servlet; managed beans came from faces-config.xml, so no CDI container was needed.

  • Faces 2.3 (Java EE 8). Faces became a CDI citizen: @Inject on Faces artefacts, CDI evaluation of the implicit objects, f:websocket with @Push, @FacesConfig, and the deprecation of the managed-bean facility (see the versions page). A Servlet container had to bring a CDI container to use them.

  • Faces 4.0 / Jakarta EE 10 (Tomcat 10.1, Weld 5, Mojarra 4.0, MyFaces 4.0). The jakarta.* namespace; JSP support and managed beans removed, so a CDI container is mandatory on Tomcat. Mojarra 4.0.26 ran with Weld 5.1.7 and 6.0.4 on Tomcat 11.0.26 here; Tomcat 10.1 itself was not run.

  • Faces 4.1 / Jakarta EE 11 (Tomcat 11, Weld 6, Mojarra 4.1, MyFaces 4.1). Java 17 minimum; Mojarra 4.1 needs the CDI 4.1 API (ELAwareBeanManager), so Weld 6 and not Weld 5. Weld 6.0.4 is the pairing used on this page.

  • Faces 5.0 / Jakarta EE 12 (upcoming). Only milestones exist (jakarta.faces-api 5.0.0-M9); Weld 7 is the CDI 5 line. Nothing here describes 5.0 behaviour, and Weld 7 failed with MyFaces 4.1.4.

  • Jetty. The module names carry the Jakarta EE level (ee10-cdi in the Weld guide, ee11-cdi in Jetty 12.1.14).

Where the Books Are Outdated

The books of this section’s bibliography that deal with a servlet container were written for Tomcat 5 and 6, JSF 1.x and 2.0 and a javax.* world. I checked what they say about it in the copies the section was written from; in prose and without reproducing their listings:

  • The JAR lists. Mann’s JavaServer Faces in Action (2004) develops its examples on Tomcat 5.0 and has you copy the JSF API and implementation JARs, jstl.jar and standard.jar into WEB-INF/lib. Dudney et al.'s Mastering JavaServer Faces (2004) lists the API and reference-implementation JARs, the JSTL JARs and four Apache Commons JARs as required resources, and suggests Tomcat’s shared library directory for the API JAR. Burns and Schalk’s JavaServer Faces 2.0: The Complete Reference (2010) asks for a Servlet 2.5 container and names Tomcat 5.x, and uses the jsf-api and jsf-impl Maven artifacts. Today the dependency set is the pom.xml above: one or two Faces JARs, a Weld JAR, optional add-ons; Facelets needs no JSTL JARs; the packages are jakarta.faces; and the Servlet API and EL come from Tomcat.

  • No CDI container. None of these books has a CDI step, because the managed-bean facility of faces-config.xml did the work. Faces 4.x has no such facility, so a Servlet container without Weld cannot start Mojarra 4.1 or MyFaces 4.1 (Which Combinations Work).

  • Container versions. Leonard’s JSF 2.0 Cookbook (2010) gives separate setup notes for older Servlet containers (Tomcat 5, Jetty 5) and for Servlet containers with JSP 2.1 (Tomcat 6, Jetty 6). Those releases and the javax.* APIs they carry cannot run the jakarta.* Faces releases of this page, which need Tomcat 10.1 or later for Faces 4.0 and Tomcat 11 for the versions used here.

  • JSP views. The 2004 books use JSP as the view technology on Tomcat. Facelets (XHTML) replaced it, and JSP support was removed in Faces 4.0.

  • Deployment by hand. The books describe copying files into the container and, in one case, containers' shared class directories; the build is Maven, the WAR is self-contained, and the Tomcat default web applications should be removed (Tomcat Settings That Matter for Faces).

Pitfalls

  • Shipping Servlet, EL or CDI API JARs in the WAR next to Tomcat’s own. Servlet, JSP, EL and WebSocket classes always come from Tomcat; a CDI API JAR in WEB-INF/lib conflicts with the one inside weld-servlet-shaded. Use provided scope for the compile-time API JAR.

  • Forgetting WEB-INF/beans.xml. Nothing is discovered (beans.xml and Bean Discovery).

  • A scope-less @Named class. With the default annotated mode it is not a bean; EL silently sees nothing.

  • Choosing all discovery to avoid the previous mistake. Every class becomes a bean and class-loading problems show up as Weld warnings.

  • Assuming Bean Validation is there. On Tomcat the constraints do nothing until a provider is in the WAR.

  • Putting the Tomcat-only resource-env-ref in a WAR that also goes to Jetty. The Jetty deployment fails.

  • Treating the MyFaces undeploy errors as application bugs. They come from the shutdown order.

  • Weld 7 or Weld 5 because "newer is better". Match Weld to the Faces release: Weld 6 for Faces 4.1.

  • Leaving the diagnostic servlet and runtime.xhtml deployed. They print internal class names; delete them or put a security constraint on them.

  • A Secure session cookie over plain HTTP. The browser does not send it back, so login loops appear to be an application bug.

  • EJB, @PersistenceContext and @Transactional on Weld Servlet. Not supported; use an application server or wire the transaction manager yourself.

  • Running Tomcat as root, with the shutdown port open, or with the default web applications. See the Tomcat security how-to.

The Jakarta EE Servers page is the alternative with a bundled implementation. The Spring Boot with JoinFaces page and the Quarkus page cover the runtimes without a web.xml, the Persistence and Services page covers data access without a server-provided transaction manager, the Testing page covers running these WARs from Arquillian, Selenium and Playwright, and the Deployment and Clustering page covers packaging, containers and session replication for the Tomcat setup built here. See also the Maven WAR setup, beans.xml in the project setup page, bean discovery, the ValidatorFactory listener, enabling f:websocket, project stages, cookies and sessions, client-state keys, error pages, the version matrix and the first application on Tomcat, Migrating to Jakarta Faces (moving an older Tomcat application) and the runtime rules of the best-practices checklist.