Extending Faces: Wrappers, Factories and Listeners

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) with Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Jakarta Expression Language 6.0, Jakarta Servlet 6.1 and Java 17 or later (the examples were compiled with --release 21). The extension mechanisms described here are stable: everything below also applies to Faces 4.0 unless the version section says otherwise, but only 4.1 was run. The code of this page was deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with curl and a small script that performs the form POSTs (every header, redirect and log line shown was observed on 2026-10-08). Claims that were not run are marked as such.

Almost everything that Faces does is done by an object that can be replaced: the object that builds a view, the one that serves a stylesheet, the one that decides where an outcome leads, the one that resolves #{name}, the one that creates the request context. None of these replacements needs a fork of Mojarra or MyFaces and none needs the implementation’s internal classes. The specification defines one uniform recipe, the decorator pattern with FacesWrapper classes, plus a handful of listener interfaces for the places where you only want to observe. This page is the map of those extension points and a worked example of each of the non-visual ones. The visual ones — components, renderers, render kits, converters, validators, behaviours — have their own pages, and the places where the other pages already show an extension point in depth are named in the table that follows.

The Extension Points at a Glance

The table lists what a project can replace or observe, the base class or interface to extend, the element of faces-config.xml that registers it, and where this section treats it. A *Wrapper class implements the whole type by delegating to the wrapped instance, so a subclass overrides only what it changes.

Table 1. Non-visual extension points of Jakarta Faces 4.1
Need Extend Registered with (faces-config.xml) Covered

Change how views are built, found, rendered, or how locale and URLs are calculated

ViewHandlerWrapper

<application><view-handler>

ViewHandlerWrapper below

Change how stylesheets, scripts and images are found and served

ResourceHandlerWrapper, ResourceWrapper

<application><resource-handler>

ResourceHandlerWrapper and ResourceWrapper below; the resource handling page

Change how an outcome leads to a view

ConfigurableNavigationHandlerWrapper (preferred) or NavigationHandlerWrapper

<application><navigation-handler>

NavigationHandlerWrapper and ConfigurableNavigationHandlerWrapper below; the navigation page

Add names or properties to Expression Language

jakarta.el.ELResolver

<application><el-resolver>, or Application.addELResolver

A Custom ELResolver below; the expression language page

Run code around a lifecycle phase on every request

PhaseListener

<lifecycle><phase-listener>

PhaseListener below; the request processing lifecycle page

React to something that happened in the framework (application start, view rendered, component added)

SystemEventListener

<application><system-event-listener>, or Application.subscribeToEvent

SystemEventListener below; the events and listeners page

Wrap every action event

ActionListenerWrapper

<application><action-listener>

the events and listeners page

Replace how view state is saved

StateManagerWrapper, StateManagementStrategy

<application><state-manager>

the state saving page

Decorate the objects that exist once per request

FacesContextWrapper, ExternalContextWrapper, PartialViewContextWrapper, ExceptionHandlerWrapper, FlashWrapper

<factory><faces-context-factory>, <external-context-factory>, <partial-view-context-factory>, <exception-handler-factory>, <flash-factory>

Factories: Decorating What Is Created Once per Request below; the architecture, Ajax and exception handling pages

Decorate the application-wide objects: the Application and the Lifecycle

ApplicationWrapper, LifecycleWrapper

<factory><application-factory>, <lifecycle-factory>

Two More Factories: Application and Lifecycle below

Decorate rendering

RenderKitWrapper, RendererWrapper, ResponseWriterWrapper

<render-kit>, <factory><render-kit-factory>

the custom components and renderers page

Add search keywords or change how search expressions are resolved

SearchKeywordResolver, SearchExpressionHandlerWrapper

<application><search-keyword-resolver>, <search-expression-handler>

the search expressions page

The Decorator Pattern the Specification Mandates

Section 11.3.7 of the specification requires the runtime to support decoration for every artifact that has a wrapper class. The list names about three dozen types, from Application and ViewHandler to VisitContext and ViewDeclarationLanguage, and says "among others". The mechanism has three parts.

  1. The extension class extends the wrapper class (ViewHandlerWrapper, ResourceHandlerWrapper, …​) or implements the type itself, and declares a public constructor with one parameter, of the type it extends.

  2. When Faces starts and reads the configuration, it creates the extension through that constructor and passes in the instance that filled the role before. getWrapped() (from the jakarta.faces.FacesWrapper interface, which every wrapper implements) returns it.

  3. The extension overrides the methods it wants to change and calls getWrapped() or super for the rest.

Three consequences follow, and all three were observed on Mojarra and on MyFaces.

  • The one-argument constructor is what makes a class a decorator. A class that has only a public no-argument constructor is not decorating: it replaces the previous instance, and anything it does not implement is lost. A NavigationHandler with a no-argument constructor that only logged was installed on both implementations, and from then on no form navigated anywhere, because nothing called the original. Use the replacement form only when that is the intent.

  • The no-argument constructors of the wrapper classes (ViewHandlerWrapper(), ResourceHandlerWrapper(), NavigationHandlerWrapper(), ResourceWrapper()) are deprecated in the 4.1 API ("use the other constructor taking the implementation being wrapped"). The constructors that take the wrapped object exist since Faces 2.3. Always use them.

  • Several extensions of the same artifact form a chain, and the order of the chain is the order in which the configuration resources are processed, which section Ordering Several Extensions explains. Faces itself may also put its own wrappers into the chain.

Wrappers also work in code, outside the registration mechanism: new ResourceWrapper(resource) in a handler, or the FacesContextWrapper created by a factory in the architecture page, is the same decorator used by hand.

The first complete example is the one the specification itself uses: a ViewHandler that changes how the locale is calculated and delegates everything else.

ViewHandlerWrapper

The ViewHandler is the application’s object for everything about a view that is not the view’s content: it derives the view id from the request path (deriveViewId), restores or creates the view (restoreView, createView), renders it (renderView), builds the URLs of actions, redirects, bookmarks, resources and WebSocket channels (getActionURL, getRedirectURL, getBookmarkableURL, getResourceURL, getWebsocketURL), answers which view declaration language handles a view (getViewDeclarationLanguage), manages the protected views (addProtectedView, getProtectedViewsUnmodifiable, removeProtectedView), and calculates the locale, character encoding and render kit id (calculateLocale, calculateCharacterEncoding, calculateRenderKitId). ViewHandlerWrapper (since Faces 1.2) delegates all of them.

The Bookshelf catalogue is offered in English and Spanish. The handler below lets a lang request parameter choose the locale, remembers the choice in the session, and otherwise leaves the decision to the wrapped handler. The parameter is checked against the locales that the application declares as supported, so an arbitrary value never reaches the session.

src/main/java/org/example/bookshelf/web/LocaleViewHandler.java
package org.example.bookshelf.web;

import jakarta.faces.application.ViewHandler;
import jakarta.faces.application.ViewHandlerWrapper;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Logger;

/**
 * Lets a "lang" request parameter choose the locale, remembers the choice in the session and otherwise keeps the
 * default calculation of the wrapped ViewHandler.
 */
public class LocaleViewHandler extends ViewHandlerWrapper {

    private static final String KEY = LocaleViewHandler.class.getName() + ".locale";
    private static final Logger LOG = Logger.getLogger(LocaleViewHandler.class.getName());

    // Faces calls this constructor and passes the ViewHandler that was installed before this one.
    public LocaleViewHandler(ViewHandler wrapped) {
        super(wrapped);
    }

    @Override
    public Locale calculateLocale(FacesContext context) {
        ExternalContext external = context.getExternalContext();
        String lang = external.getRequestParameterMap().get("lang");
        Locale requested = lang == null ? null : findSupported(context, lang);
        if (requested != null) {
            external.getSessionMap().put(KEY, requested);
            return requested;
        }
        if (external.getSession(false) != null && external.getSessionMap().get(KEY) instanceof Locale remembered) {
            return remembered;
        }
        return getWrapped().calculateLocale(context);
    }

    private Locale findSupported(FacesContext context, String language) {
        Iterator<Locale> supported = context.getApplication().getSupportedLocales();
        while (supported.hasNext()) {
            Locale candidate = supported.next();
            if (candidate.getLanguage().equals(language)) {
                return candidate;
            }
        }
        LOG.fine(() -> "Ignoring unsupported language " + language);
        return null;
    }
}

It is registered in WEB-INF/faces-config.xml, next to the locales it validates against:

WEB-INF/faces-config.xml (the application part)
<?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>
        <locale-config>
            <default-locale>en</default-locale>
            <supported-locale>es</supported-locale>
        </locale-config>
        <view-handler>org.example.bookshelf.web.LocaleViewHandler</view-handler>
    </application>
</faces-config>

The page that shows the result prints #{facesContext.viewRoot.locale}. The runs below used curl-style requests with a cookie jar: a GET with ?lang=es and then a POST of the form without the parameter.

GET  /catalogue.xhtml?lang=es          -> locale es
POST /catalogue.xhtml  (no lang)       -> locale es   (remembered in the session)
GET  /catalogue.xhtml?lang=xx          -> locale en   (xx is not supported; the default applies)

The same outcome on Mojarra and on MyFaces. Three details matter when you write a ViewHandler decorator.

  • calculateLocale is called for the postback as well as for the initial request, on both implementations: a first version of this handler that read only the parameter produced es on the GET and en on the POST, because the POST did not carry lang. A decision that has to last must be stored by the handler itself, as here.

  • The decorator sits in a chain with the implementation’s own wrappers. getWrapped().getClass() is therefore not necessarily the class you expect: with no other extension installed, LocaleViewHandler wrapped a ConversationAwareViewHandler on both implementations, an internal CDI helper of Mojarra or of MyFaces and not the default handler itself, and the ordering section below shows that helper moving around when more decorators are present. Do not cast getWrapped() to an implementation type.

  • An @Inject field of a ViewHandler named in faces-config.xml stayed null on both implementations (the instance is created by Faces through the constructor, not by CDI). CDI.current() works on both when a decorator really needs a bean; prefer to read what you need from the FacesContext instead.

The wrapper is the right tool for the cases the ViewHandler was designed for: a different way to compute the locale, a different URL scheme (getActionURL, getBookmarkableURL), a global pre- or post-processing of rendering (renderView), extra protected views. It is not the place for per-request business logic, and it must be thread-safe: one instance serves every request.

ResourceHandlerWrapper and ResourceWrapper

The ResourceHandler finds the files of the resources directory (and of META-INF/resources in JARs), answers requests to /jakarta.faces.resource/*, and tracks which scripts and stylesheets a page already rendered. Its methods fall into three groups, and ResourceHandlerWrapper delegates all of them:

  • creating resources: createResource(name), createResource(name, library), createResource(name, library, contentType), createResourceFromId(id) (since Faces 2.2) and createViewResource(context, name) (the lookup that finds Facelets files and, through getViewResources, the views of a directory);

  • serving: isResourceRequest(context), handleResourceRequest(context), isResourceURL(url), libraryExists(name);

  • page bookkeeping: markResourceRendered, isResourceRendered (since 2.3) and getRendererTypeForResourceName.

A decorator that changes what is served overrides the createResource methods and returns a different, or a wrapped, Resource. ResourceWrapper is the matching wrapper for the Resource object, with getInputStream(), getURL(), getResponseHeaders(), getRequestPath(), getContentType(), getLibraryName(), getResourceName() and userAgentNeedsUpdate. The Bookshelf example adds a cache header to the files of its own library bookshelf and leaves all other resources alone:

src/main/java/org/example/bookshelf/web/CacheControlResourceHandler.java
package org.example.bookshelf.web;

import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceHandler;
import jakarta.faces.application.ResourceHandlerWrapper;
import jakarta.faces.application.ResourceWrapper;
import java.util.HashMap;
import java.util.Map;

/** Adds a Cache-Control header to the resources of the "bookshelf" library; everything else is untouched. */
public class CacheControlResourceHandler extends ResourceHandlerWrapper {

    public CacheControlResourceHandler(ResourceHandler wrapped) {
        super(wrapped);
    }

    @Override
    public Resource createResource(String resourceName, String libraryName) {
        return decorate(super.createResource(resourceName, libraryName), libraryName);
    }

    @Override
    public Resource createResource(String resourceName, String libraryName, String contentType) {
        return decorate(super.createResource(resourceName, libraryName, contentType), libraryName);
    }

    private Resource decorate(Resource resource, String libraryName) {
        if (resource == null || !"bookshelf".equals(libraryName)) {
            return resource;   // a missing resource must stay null
        }
        return new ResourceWrapper(resource) {
            @Override
            public Map<String, String> getResponseHeaders() {
                Map<String, String> headers = new HashMap<>(super.getResponseHeaders());
                headers.put("Cache-Control", "public, max-age=3600");
                return headers;
            }
        };
    }
}
WEB-INF/faces-config.xml (excerpt)
<application>
    <resource-handler>org.example.bookshelf.web.CacheControlResourceHandler</resource-handler>
</application>

With bookshelf/site.css and other/other.css in the resources directory, the stylesheet link in the page is /bookshelf/jakarta.faces.resource/site.css.xhtml?ln=bookshelf (the .xhtml suffix comes from the extension mapping of the FacesServlet), and the responses were:

GET .../site.css.xhtml?ln=bookshelf   200   Cache-Control: public, max-age=3600
GET .../other.css.xhtml?ln=other      200   Cache-Control: no-store, must-revalidate   (Mojarra, Development stage)
                                            Cache-Control: no-cache                    (MyFaces, Development stage)
GET .../none.css.xhtml?ln=bookshelf   404

The header of the decorated resource replaced the implementation’s default on both, and the missing resource still produced a 404 because the decorator returns null unchanged. Three rules keep a resource handler decorator correct:

  • Return null when the wrapped handler returns null: a null is how a missing resource is reported, and the 404 of the example depends on it.

  • A decorator that overrides createResource must override every variant that matters to it. The header appeared on both implementations with only the two variants above overridden; a decorator that changes names or libraries should also consider createResourceFromId (not run here), which creates a resource from an identifier.

  • Resource requests do not run the lifecycle. The X-Request-Id header that the phase listener below adds to every page response was absent from the resource responses on both implementations, so a decorator is the place for resource-specific logic and a PhaseListener is not. A servlet filter is the place for logic that must see all requests.

The resource handling page covers the resource directories, libraries, versioning, the ResourceHandler constants and the jakarta.faces.RESOURCE_EXCLUDES parameter that decide what a request reaches.

The NavigationHandler turns the outcome of an action into a navigation. It has one working method, handleNavigation(context, fromAction, outcome), plus the overload with a toFlowDocumentId for flows. NavigationHandlerWrapper (since Faces 2.2) delegates both. The navigation page explains the algorithm and shows a logging decorator; this page shows the point that matters for extensions: there are two base classes, and they are not interchangeable.

ConfigurableNavigationHandler adds getNavigationCase, getNavigationCases and performNavigation, and Faces' own handler is of this type. Components that need to know the target of an outcome without navigating — h:link, h:button, and every UIOutcomeTarget component — call the configurable methods. A decorator that extends only NavigationHandlerWrapper is a plain NavigationHandler, so the object that Application.getNavigationHandler() returns is no longer configurable. The Bookshelf migration needs the old site’s outcomes (home, books) to keep working while the pages move to catalogue. This is the decorator written the safe way, with the configurable wrapper:

src/main/java/org/example/bookshelf/web/LegacyOutcomeNavigationHandler.java
package org.example.bookshelf.web;

import jakarta.faces.application.ConfigurableNavigationHandler;
import jakarta.faces.application.ConfigurableNavigationHandlerWrapper;
import jakarta.faces.context.FacesContext;
import java.util.Map;

/** Maps the outcomes of the old site to the current ones before the wrapped handler sees them. */
public class LegacyOutcomeNavigationHandler extends ConfigurableNavigationHandlerWrapper {

    private static final Map<String, String> ALIASES = Map.of("home", "catalogue", "books", "catalogue");

    public LegacyOutcomeNavigationHandler(ConfigurableNavigationHandler wrapped) {
        super(wrapped);
    }

    @Override
    public void handleNavigation(FacesContext context, String fromAction, String outcome) {
        String mapped = outcome == null ? null : ALIASES.getOrDefault(outcome, outcome);
        super.handleNavigation(context, fromAction, mapped);
    }
}
WEB-INF/faces-config.xml (excerpt)
<application>
    <navigation-handler>org.example.bookshelf.web.LegacyOutcomeNavigationHandler</navigation-handler>
</application>
<navigation-rule>
    <navigation-case>
        <from-outcome>catalogue</from-outcome>
        <to-view-id>/catalogue.xhtml</to-view-id>
        <redirect/>
    </navigation-case>
</navigation-rule>

Both buttons action="home" and action="books" answered 302 with Location: /bookshelf/catalogue.xhtml on Mojarra and MyFaces. The plain-wrapper version of the same class (PlainLegacyNavigationHandler extends NavigationHandlerWrapper, constructor taking a NavigationHandler) navigated just as well for buttons, and showed the difference in the links:

                                     ConfigurableNavigationHandlerWrapper     NavigationHandlerWrapper
h:link outcome="catalogue"  Mojarra  link works                                disabled: "...navigation case could not be matched"
h:link outcome="catalogue"  MyFaces  link works                                link works

On Mojarra the plain wrapper disabled even the link whose outcome was valid; MyFaces tolerated it. The portable rule is therefore: extend ConfigurableNavigationHandlerWrapper, whose constructor takes a ConfigurableNavigationHandler.

The decorator above changes the outcome only in handleNavigation, so the links did not benefit from the alias: the h:link outcome="home" of the same page stayed disabled with the alias handler on Mojarra (and was rendered as plain text without a target on MyFaces), because getNavigationCase still saw home. An alias that must also work for links would have to be applied in getNavigationCase as well (not run here). This is the typical trap of partial decoration: every method the other components call must agree.

A Custom ELResolver

Expression Language evaluates #{features.reviews} by asking a chain of jakarta.el.ELResolver objects, first for the top-level name features (with a null base) and then for the property reviews of whatever the first answer was. Faces builds the chain from the implicit objects, the CDI resolver and its own resolvers; an application adds its resolvers in faces-config.xml or in code, and they are consulted before the bean, list, map and array resolvers. The expression language page explains how the chain works and shows a resolver that exposes build information; this one exposes feature flags, to show what a resolver that handles two levels (the name and then the properties) looks like.

A flag bookshelf.feature.reviews is a system property (set with -Dbookshelf.feature.reviews=true or by the server configuration), and the page asks #{features.reviews}. The resolver reads only the flags it knows, never a property chosen by the page, so a view cannot turn an expression into a read of an arbitrary system property.

src/main/java/org/example/bookshelf/web/FeatureFlagsELResolver.java
package org.example.bookshelf.web;

import jakarta.el.ELContext;
import jakarta.el.ELResolver;
import jakarta.el.PropertyNotWritableException;
import java.util.Set;

/** Exposes #{features.reviews} and #{features.live}: the system properties bookshelf.feature.* as booleans. */
public class FeatureFlagsELResolver extends ELResolver {

    private static final Set<String> KNOWN = Set.of("reviews", "live");

    /** The marker object that #{features} resolves to; further segments are resolved by this resolver too. */
    public static final class Features {
        private Features() { }
    }

    private static final Features FEATURES = new Features();

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (base == null && "features".equals(property)) {
            context.setPropertyResolved(true);
            return FEATURES;
        }
        if (base instanceof Features && property instanceof String name) {
            context.setPropertyResolved(true);
            // Only whitelisted names are read: never turn an arbitrary property into a system property lookup.
            return KNOWN.contains(name) && Boolean.getBoolean("bookshelf.feature." + name);
        }
        return null;
    }

    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (isMine(base, property)) {
            context.setPropertyResolved(true);
        }
        return null;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (isMine(base, property)) {
            throw new PropertyNotWritableException("feature flags are read-only");
        }
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (isMine(base, property)) {
            context.setPropertyResolved(true);
            return true;
        }
        return false;
    }

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

    private boolean isMine(Object base, Object property) {
        return (base == null && "features".equals(property)) || base instanceof Features;
    }
}
WEB-INF/faces-config.xml (excerpt)
<application>
    <el-resolver>org.example.bookshelf.web.FeatureFlagsELResolver</el-resolver>
</application>
catalogue.xhtml (excerpt)
<h:outputText value="#{features.reviews}"/>
<h:outputText value="#{features.secret}"/>

Started with -Dbookshelf.feature.reviews=true -Dbookshelf.feature.secret=true, the page rendered true for features.reviews and false for features.secret (set, but not on the whitelist) on Mojarra and on MyFaces. The page without the properties rendered false for both. Notes on writing resolvers:

  • Call setPropertyResolved(true) only when the resolver really handled the segment. A resolver that claims too much hides beans and implicit objects.

  • getFeatureDescriptors() no longer exists in the Expression Language 6.0 API, so the class has the five abstract methods shown and no more.

  • The resolver can also be added in code with Application.addELResolver(resolver), typically from a listener for the application start (the section on system event listeners below shows such a listener). Application.addELContextListener is the hook for code that must configure each ELContext.

  • An @Inject field in the resolver was null on Mojarra and injected on MyFaces; use CDI.current() if a bean is needed, or keep the resolver free of state, as here.

  • VariableResolver and PropertyResolver, the JSF 1.x ways of adding names, were removed in Faces 4.0. Everything they did is done with an ELResolver.

PhaseListener

A PhaseListener is called before and after each phase of the request processing lifecycle. It sees every Faces request, which makes it the right place for cross-cutting behaviour that depends on lifecycle position: timing, auditing, a request id, a per-request setup and clean-up. The interface has three methods: getPhaseId() (the phase it cares about, or PhaseId.ANY_PHASE), beforePhase(PhaseEvent) and afterPhase(PhaseEvent). The specification adds two rules that are easy to forget:

  • If beforePhase was called on a listener, afterPhase will also be called, even when the phase threw an exception. The afterPhase calls run in the reverse order of the beforePhase calls. If listener B throws in beforePhase, listener C is never called, and neither afterPhase of B or C.

  • Exceptions thrown by the listeners are caught and published to the ExceptionHandler, they do not propagate out of the lifecycle.

Listeners must be thread-safe: Faces creates one instance of a listener named in faces-config.xml and calls it from every request thread, so a per-request value cannot be a field. The request map is the place for it.

The example gives every Faces request an id. It generates a random UUID in beforePhase(RESTORE_VIEW) — the first moment of the lifecycle — stores it in the request map for the beans and logs, and sends it back in an X-Request-Id response header that a support person can ask the user for.

src/main/java/org/example/bookshelf/web/RequestIdPhaseListener.java
package org.example.bookshelf.web;

import jakarta.faces.context.ExternalContext;
import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PhaseListener;
import java.util.UUID;
import java.util.logging.Logger;

/** Gives every Faces request an id, in the request map and in an X-Request-Id response header. */
public class RequestIdPhaseListener implements PhaseListener {

    public static final String ATTRIBUTE = RequestIdPhaseListener.class.getName() + ".id";
    private static final Logger LOG = Logger.getLogger(RequestIdPhaseListener.class.getName());

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    @Override
    public void beforePhase(PhaseEvent event) {
        ExternalContext external = event.getFacesContext().getExternalContext();
        String id = UUID.randomUUID().toString();
        external.getRequestMap().put(ATTRIBUTE, id);
        external.setResponseHeader("X-Request-Id", id);
        LOG.fine(() -> "request id " + id);
    }

    @Override
    public void afterPhase(PhaseEvent event) {
        // nothing to do
    }
}
WEB-INF/faces-config.xml (excerpt)
<lifecycle>
    <phase-listener>org.example.bookshelf.web.RequestIdPhaseListener</phase-listener>
</lifecycle>

Every page, postback and redirect response of the example carried an X-Request-Id header (a GET, a POST that answered 200, and a POST that answered 302, on both implementations). The responses that ended in a server error did not: the container discarded the headers when it rendered its error page. As noted in the resource section, requests for resources did not carry it either, because they do not run the lifecycle.

A listener can be attached in four places, and the events and listeners page of this section shows the order in which they run when several are present:

  1. <lifecycle><phase-listener> in faces-config.xml, for the whole application (this example);

  2. Lifecycle.addPhaseListener(listener) in code, for example from a start-up listener;

  3. <f:phaseListener type="..."/> inside f:view, for one view;

  4. the beforePhase and afterPhase attributes of f:view, for one view and with method expressions.

SystemEventListener

A SystemEventListener observes the events that the framework publishes about itself: the application starting (PostConstructApplicationEvent) or stopping (PreDestroyApplicationEvent), a component being added to the tree, a view about to render (PreRenderViewEvent), a validation about to run, and so on. The interface has two methods: isListenerForSource(Object), which says whether this listener wants the events of a given source object, and processEvent(SystemEvent). Listeners that run for every event of a type must therefore check the source cheaply.

The events and listeners page lists all events and their order. This page shows the extension side: how a listener is registered, and the pattern most libraries use, a listener for the application start that does the setup that needs code.

Registration has two forms. In faces-config.xml the system-event-listener element names the listener class, the event class (SystemEvent if omitted) and optionally the source class. In code, Application.subscribeToEvent(eventClass, listener) (or the overload with a source class) subscribes, and unsubscribeFromEvent removes it. The example uses both. ExtensionBootstrap is registered in XML for PostConstructApplicationEvent, with Application as source, and subscribes a second listener in code. The second one adds two defensive headers to each Faces page just before it renders:

src/main/java/org/example/bookshelf/web/ExtensionBootstrap.java
package org.example.bookshelf.web;

import jakarta.faces.application.Application;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.PostConstructApplicationEvent;
import jakarta.faces.event.PreRenderViewEvent;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;
import java.util.logging.Logger;

/** Runs once when the Application is ready and registers the listeners that need code to be set up. */
public class ExtensionBootstrap implements SystemEventListener {

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

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof Application;
    }

    @Override
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        Application application = (Application) event.getSource();
        application.subscribeToEvent(PreRenderViewEvent.class, new SecurityHeadersListener());
        LOG.info("SecurityHeadersListener subscribed for " + PostConstructApplicationEvent.class.getSimpleName());
    }
}
src/main/java/org/example/bookshelf/web/SecurityHeadersListener.java
package org.example.bookshelf.web;

import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.SystemEvent;
import jakarta.faces.event.SystemEventListener;

/** Adds two defensive response headers just before a view is rendered. */
public class SecurityHeadersListener implements SystemEventListener {

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof UIViewRoot;
    }

    @Override
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        ExternalContext external = FacesContext.getCurrentInstance().getExternalContext();
        if (!external.isResponseCommitted()) {
            external.setResponseHeader("X-Content-Type-Options", "nosniff");
            external.setResponseHeader("Referrer-Policy", "same-origin");
        }
    }
}
WEB-INF/faces-config.xml (excerpt)
<application>
    <system-event-listener>
        <system-event-listener-class>org.example.bookshelf.web.ExtensionBootstrap</system-event-listener-class>
        <system-event-class>jakarta.faces.event.PostConstructApplicationEvent</system-event-class>
        <source-class>jakarta.faces.application.Application</source-class>
    </system-event-listener>
</application>

At start-up both implementations logged SecurityHeadersListener subscribed for PostConstructApplicationEvent once, and each page response then carried X-Content-Type-Options: nosniff and Referrer-Policy: same-origin. The listener is a demonstration of the mechanism; headers that apply to every response of an application, not only to Faces views, belong in a servlet filter or in the server configuration, and the security page treats the headers a Faces application should send.

Notes that apply to all system event listeners:

  • The listener named in XML is created by Faces with its public no-argument constructor. An @Inject field in it was null on Mojarra and injected on MyFaces (the events and listeners page records the same difference); use CDI.current() for portable code.

  • Faces does not fire CDI events for system events. The events and listeners page shows a bridge that does.

  • A listener subscribed in code is subscribed for the life of the application; subscribe it once, from a start-up event, and not from request code, or listeners accumulate.

  • The specification publishes PostConstructApplicationEvent immediately after all application configuration resources have been processed, so it is the moment to register things in Application programmatically (addELResolver, addELContextListener). It is the usual alternative to a ServletContextListener in a library that must work with any server.

Factories: Decorating What Is Created Once per Request

A factory creates the objects that Faces needs per request, per application or per view: the FacesContext, the ExternalContext, the PartialViewContext, the ExceptionHandler, the Flash, the Lifecycle, the RenderKit, the ClientWindow, the Application itself. FactoryFinder.getFactory(name) returns the factory registered under one of the FactoryFinder constants, and every factory type is decoratable in the same way as a handler: the extension class extends the factory, takes the previous factory in its constructor, and delegates to it. The usual shape is a factory that asks the wrapped factory for the object and wraps the result in a wrapper.

Table 2. Factory elements of <factory> in the 4.1 schema
Element Factory Creates (wrapper)

application-factory

ApplicationFactory

Application (ApplicationWrapper); Two More Factories: Application and Lifecycle shows one

faces-context-factory

FacesContextFactory

FacesContext (FacesContextWrapper); the architecture page shows a complete example

external-context-factory

ExternalContextFactory

ExternalContext (ExternalContextWrapper); the example below

partial-view-context-factory

PartialViewContextFactory

PartialViewContext (PartialViewContextWrapper); the Ajax page shows one

exception-handler-factory

ExceptionHandlerFactory

ExceptionHandler (ExceptionHandlerWrapper); the exception handling page

flash-factory

FlashFactory

Flash (FlashWrapper)

lifecycle-factory

LifecycleFactory

Lifecycle (LifecycleWrapper); Two More Factories: Application and Lifecycle adds a phase listener this way

render-kit-factory

RenderKitFactory

RenderKit (RenderKitWrapper)

client-window-factory

ClientWindowFactory

ClientWindow (ClientWindowWrapper)

view-declaration-language-factory

ViewDeclarationLanguageFactory

ViewDeclarationLanguage (ViewDeclarationLanguageWrapper)

flow-handler-factory

FlowHandlerFactory (wrapper: FlowHandlerFactoryWrapper)

FlowHandler

visit-context-factory

VisitContextFactory

VisitContext (VisitContextWrapper)

search-expression-context-factory

SearchExpressionContextFactory

SearchExpressionContext

facelet-cache-factory, tag-handler-delegate-factory

FaceletCacheFactory, TagHandlerDelegateFactory

the Facelet cache and the tag handler delegates

The Bookshelf example protects its redirects. Anything that calls ExternalContext.redirect(url) — an action that redirects to a returnTo address, the navigation handler for a faces-redirect=true outcome, a viewAction — goes through that method, so a decorator on the ExternalContext is a single place to refuse redirects that leave the application. A log line in redirect showed that both an action calling redirect directly and a navigation case with <redirect/> end there, on Mojarra and on MyFaces. The decorator allows relative paths and the https URLs of hosts listed in a context parameter, and rejects everything else, in particular absolute URLs to other hosts and the //host form that browsers read as a different host. The allow-list comes from web.xml, not from the code, and holds no secret.

src/main/java/org/example/bookshelf/web/SafeRedirectExternalContextFactory.java
package org.example.bookshelf.web;

import jakarta.faces.FacesException;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.ExternalContextFactory;
import jakarta.faces.context.ExternalContextWrapper;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;

/** Decorates the ExternalContext so that redirect() cannot leave the application (or an allow-list of hosts). */
public class SafeRedirectExternalContextFactory extends ExternalContextFactory {

    public SafeRedirectExternalContextFactory(ExternalContextFactory wrapped) {
        super(wrapped);
    }

    @Override
    public ExternalContext getExternalContext(Object context, Object request, Object response) throws FacesException {
        ExternalContext original = getWrapped().getExternalContext(context, request, response);
        return new SafeRedirectExternalContext(original);
    }

    static final class SafeRedirectExternalContext extends ExternalContextWrapper {

        SafeRedirectExternalContext(ExternalContext wrapped) {
            super(wrapped);
        }

        @Override
        public void redirect(String url) throws IOException {
            if (!isAllowed(url)) {
                throw new IllegalArgumentException("Redirect to " + url + " is not allowed");
            }
            super.redirect(url);
        }

        private boolean isAllowed(String url) {
            if (url == null) {
                return false;
            }
            URI uri;
            try {
                uri = URI.create(url);
            } catch (IllegalArgumentException e) {
                return false;
            }
            if (uri.getHost() == null) {
                return !url.startsWith("//") && !url.startsWith("\\");   // relative to this server
            }
            String allowed = getWrapped().getInitParameter("bookshelf.redirect.allowedHosts");
            Set<String> hosts = allowed == null ? Set.of()
                    : Arrays.stream(allowed.split(",")).map(String::trim).collect(Collectors.toSet());
            return hosts.contains(uri.getHost()) && "https".equalsIgnoreCase(uri.getScheme());
        }
    }
}
WEB-INF/faces-config.xml and web.xml (excerpts)
<factory>
    <external-context-factory>org.example.bookshelf.web.SafeRedirectExternalContextFactory</external-context-factory>
</factory>

<context-param>
    <param-name>bookshelf.redirect.allowedHosts</param-name>
    <param-value>login.example.org</param-value>
</context-param>

Three buttons of the test page call an action that redirects. The results were the same on Mojarra and MyFaces:

redirect(contextPath + "/catalogue.xhtml")        -> 302  Location: /bookshelf/catalogue.xhtml
redirect("https://login.example.org/start")       -> 302  Location: https://login.example.org/start   (host on the allow-list)
redirect("https://evil.example/phish")            -> 500  IllegalArgumentException: Redirect to https://evil.example/phish is not allowed

The refused redirect surfaces as an exception, so the exception handling page applies to it: in production an error page, not the stack trace, is what the user should see. A redirect that is produced by user input (a returnTo parameter) should still be validated where it is read; the decorator is the second line of defence, not the first.

Factories can be registered in three ways, and the first two decorate in the same way:

  1. <factory> elements in WEB-INF/faces-config.xml or in META-INF/faces-config.xml of a JAR (the usual way);

  2. META-INF/services/jakarta.faces.context.ExternalContextFactory (the file name is the factory class name, its first line is the implementation class) in a JAR visible to the web application’s class loader, as section 11.2.6.1 of the specification describes for FactoryFinder;

  3. FactoryFinder.setFactory(factoryName, implementationClassName), which the API documents as a hook for implementations while they parse the configuration; it has no effect once getFactory has been called for that name, so it is not a way for applications.

The service-file route was tried with a second ExternalContextFactory in a JAR that had no faces-config.xml at all. Both implementations picked it up, and the decoration order was: the implementation’s factory, then the one from the service file, then SafeRedirectExternalContextFactory from faces-config.xml (that constructor received the service-file factory as the wrapped one). So the configuration file of the application is always the outermost.

Two More Factories: Application and Lifecycle

The ApplicationFactory and the LifecycleFactory are the two factories of the list that applications decorate least often, and both return objects that live as long as the application (one Application, one Lifecycle per id), so the decorated object has to be cached. The factory classes are abstract and the implementation hands the previous factory to the one-argument constructor.

ApplicationFactory has getApplication() and setApplication(Application); ApplicationWrapper decorates the Application that getApplication() returns, so it intercepts the calls that go through that object: here createConverter(String), which is called when a tag names a converter by id.

src/main/java/org/example/bookshelf/web/ConverterLogApplicationFactory.java
package org.example.bookshelf.web;

import jakarta.faces.application.Application;
import jakarta.faces.application.ApplicationFactory;
import jakarta.faces.application.ApplicationWrapper;
import jakarta.faces.convert.Converter;

import java.util.logging.Logger;

/** Decorates the Application so that every converter created by id is logged. */
public class ConverterLogApplicationFactory extends ApplicationFactory {

    private volatile Application decorated;

    public ConverterLogApplicationFactory(ApplicationFactory wrapped) {
        super(wrapped);
    }

    @Override
    public Application getApplication() {
        if (decorated == null) {
            decorated = new LoggingApplication(getWrapped().getApplication());
        }
        return decorated;
    }

    @Override
    public void setApplication(Application application) {
        decorated = null;
        getWrapped().setApplication(application);
    }

    static final class LoggingApplication extends ApplicationWrapper {

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

        LoggingApplication(Application wrapped) {
            super(wrapped);
        }

        @Override
        public Converter<?> createConverter(String converterId) {
            LOG.info("createConverter(" + converterId + ")");
            return super.createConverter(converterId);
        }
    }
}

LifecycleFactory maps lifecycle ids to Lifecycle instances. A decorator that returns a LifecycleWrapper can register a PhaseListener on the lifecycle however faces-config.xml is configured, which suits a library that must always measure or audit the request. LifecycleFactory has three abstract methods in 4.1 (addLifecycle, getLifecycle and getLifecycleIds), so the decorator delegates the two it does not change:

src/main/java/org/example/bookshelf/web/TimingLifecycleFactory.java
package org.example.bookshelf.web;

import jakarta.faces.event.PhaseEvent;
import jakarta.faces.event.PhaseId;
import jakarta.faces.event.PhaseListener;
import jakarta.faces.lifecycle.Lifecycle;
import jakarta.faces.lifecycle.LifecycleFactory;
import jakarta.faces.lifecycle.LifecycleWrapper;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

/** Decorates every Lifecycle so that a PhaseListener is always registered on it, whatever faces-config.xml says. */
public class TimingLifecycleFactory extends LifecycleFactory {

    private final Map<String, Lifecycle> decorated = new ConcurrentHashMap<>();

    public TimingLifecycleFactory(LifecycleFactory wrapped) {
        super(wrapped);
    }

    @Override
    public Lifecycle getLifecycle(String lifecycleId) {
        return decorated.computeIfAbsent(lifecycleId, id -> new TimingLifecycle(getWrapped().getLifecycle(id)));
    }

    @Override
    public void addLifecycle(String lifecycleId, Lifecycle lifecycle) {
        getWrapped().addLifecycle(lifecycleId, lifecycle);
    }

    @Override
    public Iterator<String> getLifecycleIds() {
        return getWrapped().getLifecycleIds();
    }

    static final class TimingLifecycle extends LifecycleWrapper {

        TimingLifecycle(Lifecycle wrapped) {
            super(wrapped);
            wrapped.addPhaseListener(new RenderTimer());
        }
    }

    static final class RenderTimer implements PhaseListener {

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

        @Override
        public void beforePhase(PhaseEvent event) {
            event.getFacesContext().getAttributes().put("renderStart", System.nanoTime());
        }

        @Override
        public void afterPhase(PhaseEvent event) {
            Long start = (Long) event.getFacesContext().getAttributes().get("renderStart");
            LOG.info("render response took " + (System.nanoTime() - start) / 1_000_000 + " ms");
        }

        @Override
        public PhaseId getPhaseId() {
            return PhaseId.RENDER_RESPONSE;
        }
    }
}
WEB-INF/faces-config.xml (excerpt)
<factory>
    <application-factory>org.example.bookshelf.web.ConverterLogApplicationFactory</application-factory>
    <lifecycle-factory>org.example.bookshelf.web.TimingLifecycleFactory</lifecycle-factory>
</factory>

Both classes were compiled with --release 21 and run on Mojarra 4.1.17 and MyFaces 4.1.4 on Tomcat 11.0.26 with Weld 6.0.4.Final, with a page holding <h:inputText converter="jakarta.faces.Integer"/>. Each request logged createConverter(jakarta.faces.Integer) and, after Render Response, render response took 34 ms for the first request (Mojarra; 41 ms on MyFaces) and 1 ms for the following ones, on both implementations. Two points follow from the code. Which calls inside an implementation go through the decorated Application was not examined, so a registration that must always apply is safer made with an PostConstructApplicationEvent listener or the <application> elements. And the lifecycle is a singleton per id: the decorator adds the listener once, when it wraps, and caches the wrapper, otherwise every call of getLifecycle would add another listener.

Registering Extensions: faces-config.xml and Its Neighbours

Everything above is registered in an application configuration resource, a file in the faces-config.xml format. At start-up, before any request, the specification (11.3.2) has Faces build a list of resources in this way: if the context parameter jakarta.faces.CONFIG_FILES exists, its comma-separated, context-relative paths are the list and the search of META-INF is skipped; otherwise every META-INF/faces-config.xml and every file named *.faces-config.xml directly in META-INF (in the JARs of WEB-INF/lib) is added; and the documents supplied by the ServiceLoader service ApplicationConfigurationPopulator are added too (described below). The list is parsed before /WEB-INF/faces-config.xml, which is therefore always last. The order matters because a decorator wraps what was registered before it. The elements used on this page, all taken from the 4.1 schema (web-facesconfig_4_1.xsd), are:

Parent Element Effect

application

view-handler, resource-handler, navigation-handler, state-manager, action-listener

Decorates the corresponding handler through its one-argument constructor (replaces it when only a no-argument constructor exists)

application

el-resolver

Adds a resolver to the application’s chain

application

system-event-listener (with system-event-listener-class, system-event-class, source-class)

Subscribes the listener with Application.subscribeToEvent

application

search-expression-handler, search-keyword-resolver

Decorates the search expression handler, or adds a keyword resolver

factory

one element per factory (table above)

Decorates the factory

lifecycle

phase-listener

Adds the listener to the Lifecycle

faces-config

name, ordering, absolute-ordering

Names the file and orders it relative to others (next section)

Classes named in these elements need a public constructor of the right shape: no-argument for el-resolver, phase-listener and system-event-listener-class, and the specification says the same for the factories that only replace; one argument of the wrapped type for the decorators. Annotations do not replace these registrations: the annotations of the configuration (@FacesComponent, @FacesConverter, @FacesRenderer, @FacesBehavior and so on) cover components, converters, validators, behaviours and renderers, while handlers, resolvers, listeners and factories are registered only in XML, in service files (factories) or in code, or through the populator service of the next section. The project setup page of this section shows the layout of a project’s faces-config.xml.

Registering from Code: ApplicationConfigurationPopulator

A library that cannot ship a faces-config.xml (or that computes its configuration) can implement jakarta.faces.application.ApplicationConfigurationPopulator, a java.util.ServiceLoader service (since Faces 2.2). Faces calls populateApplicationConfiguration(Document) once at start-up and merges the document, which has the namespace of the configuration format, with the other application configuration resources. The Bookshelf example adds a phase listener this way; the populator and the listener are in a JAR whose only descriptor is the service file.

org/example/bookshelf/pop/BookshelfPopulator.java
package org.example.bookshelf.pop;

import jakarta.faces.application.ApplicationConfigurationPopulator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/** Registers a PhaseListener by adding a lifecycle element to the application configuration document. */
public class BookshelfPopulator extends ApplicationConfigurationPopulator {

    @Override
    public void populateApplicationConfiguration(Document toPopulate) {
        String namespace = toPopulate.getDocumentElement().getNamespaceURI();
        Element phaseListener = toPopulate.createElementNS(namespace, "phase-listener");
        phaseListener.setTextContent("org.example.bookshelf.pop.PopulatedPhaseListener");
        Element lifecycle = toPopulate.createElementNS(namespace, "lifecycle");
        lifecycle.appendChild(phaseListener);
        toPopulate.getDocumentElement().appendChild(lifecycle);
    }
}
META-INF/services/jakarta.faces.application.ApplicationConfigurationPopulator
org.example.bookshelf.pop.BookshelfPopulator

With the JAR in WEB-INF/lib, the listener was called before the render response phase of every request on Mojarra and on MyFaces, without any faces-config.xml entry. Use the elements of the schema in the order the schema defines when you build the document; the populator is for libraries, and an application should keep its registrations in faces-config.xml, where they can be read.

Ordering Several Extensions

When two libraries decorate the same artifact, the order decides which sees the call first. The specification (11.3.8) defines it. Each configuration resource can have a <name>, and a resource in WEB-INF/lib can declare <ordering> with <before> or <after> (a list of names, or <others/>). WEB-INF/faces-config.xml may instead declare <absolute-ordering>, which lists the other resources by name and overrides every relative ordering. The application’s own WEB-INF/faces-config.xml is always processed last, so its decorators are the outermost, and a decorator registered later wraps those registered earlier.

The experiment used two small JARs, each with a META-INF/faces-config.xml that registers one ViewHandler decorator that logs the class of the handler it wraps. The first JAR is named trace; the second, named audit, asks to be processed after it:

META-INF/faces-config.xml of the audit JAR
<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">
    <name>audit</name>
    <ordering>
        <after>
            <name>trace</name>
        </after>
    </ordering>
    <application>
        <view-handler>org.example.bookshelf.audit.AuditOrderViewHandler</view-handler>
    </application>
</faces-config>

The trace JAR has the same shape with <name>trace</name>, no ordering and its own class. The calls to calculateLocale then travelled through the chain below. The application’s LocaleViewHandler is outermost, the audit JAR (processed after trace) is next, and the trace JAR is innermost, in front of the implementation’s own handler.

flowchart LR call["Faces calls calculateLocale"] --> app["LocaleViewHandler: WEB-INF/faces-config.xml, processed last"] app --> audit["AuditOrderViewHandler: JAR audit, after trace"] audit --> trace["TraceViewHandler: JAR trace"] trace --> impl["The implementation's ViewHandler"]

The log lines (class of the wrapped object on the right) were:

Mojarra 4.1.17                                       MyFaces 4.1.4
LocaleViewHandler      -> AuditOrderViewHandler      LocaleViewHandler      -> AuditOrderViewHandler
AuditOrderViewHandler  -> ConversationAwareViewHandler   AuditOrderViewHandler  -> TraceViewHandler
TraceViewHandler       -> MultiViewHandler           TraceViewHandler       -> ConversationAwareViewHandler

With <before> in place of <after> in the audit JAR, both implementations produced the order LocaleViewHandler, TraceViewHandler, AuditOrderViewHandler: the ordering elements work as specified on both. The two columns differ in where the implementation’s own CDI helper (ConversationAwareViewHandler) sits in the chain — between two of the application’s decorators on Mojarra, innermost on MyFaces — which is why a decorator must never assume a particular class below it.

Names must be unique: a duplicate <name> among the JARs is a deployment error, and circular <before>/<after> constraints also fail the deployment. Without any ordering element the relative order of the JARs is unspecified and may differ between implementations and between builds, so a library whose decorator has to be inside or outside another one should state it with <ordering>, and an application that needs a definite order of all libraries should use <absolute-ordering> in WEB-INF/faces-config.xml. An <absolute-ordering> that does not contain <others/> ignores every resource it does not list, including the extensions of unlisted libraries; that is rarely what is intended.

Mojarra and MyFaces

The specification fixes the contracts, and on the extension points of this page the two implementations agreed in every run. The differences are in the details around them.

Table 3. Observed differences (Mojarra 4.1.17, MyFaces 4.1.4, Tomcat 11.0.26 and Weld 6.0.4)
Situation Mojarra MyFaces

@Inject into an ELResolver named in faces-config.xml

null

injected

@Inject into a ViewHandler decorator

null

null

h:link outcome="catalogue" with a plain NavigationHandlerWrapper

link disabled

link works

Position of the implementation’s own CDI helper in the ViewHandler chain

between two decorators

innermost

Dev-stage cache header of a normal resource

Cache-Control: no-store, must-revalidate

Cache-Control: no-cache

A JAR in WEB-INF/lib without META-INF/faces-config.xml

Scanned anyway (recorded on the custom components page)

Scanned for configuration only with META-INF/faces-config.xml (same page); a service file for a factory or for the populator worked on both without it

The form of a POST also differs, which matters if a script drives the page: MyFaces expects the hidden f_SUBMIT field that its h:form renders, Mojarra does not. A request without it was treated as a non-postback by MyFaces and no action ran.

What Changed

Table 4. Extension points by version
Version Change

1.2

ViewHandlerWrapper (the API gives "Since: 1.2"); the oldest wrapper class of the section

2.0

ResourceHandler and ResourceHandlerWrapper, SystemEventListener and the system events, ExternalContextFactory, FacesWrapper (each "Since: 2.0" in the API)

2.2

NavigationHandlerWrapper, ConfigurableNavigationHandlerWrapper, ResourceHandler.createResourceFromId, protected views on the ViewHandler, ApplicationConfigurationPopulator; the specification’s change list for the release also adds the FlashFactory

2.3

Constructors that take the wrapped instance on the wrapper classes (their no-argument constructors are deprecated in the 4.1 API), getWebsocketURL on the ViewHandler, getViewResources, markResourceRendered and isResourceRendered on the ResourceHandler, search expression handler and keyword resolver registration

3.0

The javax.faces.* packages become jakarta.faces.*; the registration elements are unchanged, only the XML namespace https://jakarta.ee/xml/ns/jakartaee replaces the Java EE one

4.0

ResourceResolver, VariableResolver and PropertyResolver removed: use ResourceHandler decorators and ELResolver; the old XML elements for them are gone from the schema

4.1

No new decoration mechanism. The release removed the references to the Java SecurityManager from the API, added ExternalContext.setResponseContentLengthLong (an ExternalContextWrapper delegates it), and requires Java 17 or later and Jakarta EE 11 components (Expression Language 6.0, Servlet 6.1)

5.0 (upcoming)

Under development (5.0.0-M9 at the time of writing); the extension model described here is not announced to change, but milestones are not a contract

Source: Faces 4.1 specification, Appendix B Changes Between Releases and the "Since" and "Deprecated" notes of the API pages linked in the sections above

Where the Books Are Outdated

The JSF books consulted for this section treat extension points in two places: a discussion of the design patterns behind JSF (the decorator and observer patterns, in the book of Dudney and colleagues) and a chapter on the non-UI extension points — phase listeners, the view handler, the exception handler, EL resolvers, the factories and the decorator pattern (Burns and Schalk, appendix and later chapters) — with related material on extending the core classes (Mann), custom resource handlers (Leonard) and component resources (Jacobi and Fallows). The concepts hold; the details need correcting.

  • The books register extensions with the javax.faces packages and the Java EE namespace. In 4.1 the classes are in jakarta.faces.*, the file uses the https://jakarta.ee/xml/ns/jakartaee namespace and version="4.1".

  • The JSF 1.x APIs that the older books use to add names and bean access — VariableResolver, PropertyResolver, MethodBinding, ValueBinding — were removed in Faces 4.0, as was ResourceResolver. The current mechanism for adding names and properties is the ELResolver of this page, and for finding Facelets files the ResourceHandler.

  • Standalone Facelets 1.x, in the book of Aranda and Wadia, extended the view through its own view handler; since Faces 2.0 Facelets is part of the specification and the view handler is the standard ViewHandler with its ViewDeclarationLanguage.

  • Decoration is now specified for about three dozen types, each with a *Wrapper class, so a decorator does not have to implement a whole interface such as ViewHandler or NavigationHandler; writing a complete implementation, as the older books show, is no longer necessary and, as the experiment above shows, loses the original behaviour.

  • Weblets and similar libraries, used in the books to serve component resources, are replaced by the standard ResourceHandler (and ResourceHandlerWrapper for customisation) and by h:outputScript and h:outputStylesheet.

  • An extension that needs a bean gets it from CDI (CDI.current()) or through an expression (Application.evaluateExpressionGet), not from the JSF 1.x managed-bean facility, and the phase listeners and decorators are single instances shared by all requests: per-request state belongs in the request map, not in a field.

Pitfalls

  • A class with only a no-argument constructor replaces, it does not decorate. Everything it does not implement is lost. Give every wrapper a constructor that takes the wrapped type.

  • Partial decoration of related methods. The navigation alias worked for buttons and not for links, because only handleNavigation was overridden. Decide which methods carry the behaviour and override all of them.

  • Extending NavigationHandlerWrapper instead of ConfigurableNavigationHandlerWrapper. Mojarra then disables h:link and h:button whose outcome is valid.

  • Casting getWrapped() to an implementation class, or relying on what is below the decorator. The chain contains the implementation’s own wrappers.

  • Assuming a decorator order without declaring it. Use <ordering> in libraries and <absolute-ordering> in the application, and remember that an absolute ordering without <others/> silently drops the unlisted resources.

  • Per-request state in a field. Handlers, resolvers, listeners and factories are shared by all requests; keep request data in the request map.

  • @Inject in classes that Faces instantiates from XML. It is not portable (it differed between implementations on this page); use CDI.current() or read from the FacesContext.

  • Doing too much in beforePhase. The listener runs on every request; anything slow or blocking (a database call, a remote call) becomes the latency floor of the application.

  • Expecting a PhaseListener to see resource requests. It sees only requests that run the lifecycle.

  • Subscribing in code on every request. Application.subscribeToEvent and Lifecycle.addPhaseListener add a listener permanently; subscribe from a start-up event, once.

  • Redirect allow-lists in code. A host list belongs in configuration, and a redirect target taken from user input must be validated where it is read in addition to the decorator that guards ExternalContext.redirect.

  • Hard-coded secrets in extension classes or in faces-config.xml. Read them from the server configuration or the environment, as the allow-list above is read from a context parameter.