Expression Language

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), which uses Jakarta Expression Language 6.0 (jakarta.el-api 6.0.1), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later and Apache Tomcat 11.0.26 with Weld 6.0.4.Final. The versions were checked on 2026-10-08. Faces 4.0 (Jakarta EE 10) uses Expression Language 5.0 and behaves the same for everything on this page except where the text says otherwise; Faces 5.0 (Jakarta EE 12) is still a milestone and is not covered. The Faces 4.1 specification chapter on the Expression Language still says "version 4.0 or higher" of the EL specification; that wording is simply old, the release that ships with Jakarta EE 11 is 6.0.

Every claim marked as "tested" below was run for real. The Facelets behaviour was checked with a small Bookshelf WAR on Mojarra 4.1.17, Tomcat 11.0.26 and Weld 6.0.4.Final, driven with curl (initial requests, postbacks and Ajax requests). The standalone expression results were evaluated with the Expression Language implementation bundled in Tomcat 11.0.26, which is an implementation of EL 6.0. The page does not claim anything about MyFaces unless it says so; both implementations use whatever EL implementation the container provides, so expression syntax and semantics are the same.

The Expression Language is the glue of every Faces application. A Facelets view never calls Java directly: it holds expressions such as #{catalogueBean.books} or #{catalogueBean.select(book)}, and Faces evaluates them at the right point in the request processing lifecycle. This page explains the language as Faces uses it, in the order a reader meets it: the two syntaxes, the two kinds of expression, operators, the objects that are always available, the collection features, and finally how a name such as catalogueBean is found.

What the Expression Language Is

The Expression Language (EL) is a small expression language with its own specification, Jakarta Expression Language. It is used by Jakarta Faces, Jakarta Server Pages, Jakarta Tags, CDI and Bean Validation messages, and it can also be used stand-alone from Java SE. The Faces specification does not redefine the language; its chapter on the Expression Language describes only how Faces plugs into it. That chapter states that nearly every attribute and property of a component can have a value expression associated with it, and that many properties such as action, actionListener, validator and valueChangeListener can be defined by a method expression pointing at a public method.

The Jakarta EE Tutorial summarises what the language does: it reads application data from beans, various data structures and implicit objects; writes data such as user input to beans; invokes public and static methods; performs arithmetic, boolean and string operations; and constructs and operates on collections.

The pieces involved are few, and the rest of the page keeps returning to them.

Type Role

jakarta.el.ExpressionFactory

Parses an expression string into an object. Faces keeps one per application (Application.getExpressionFactory()).

jakarta.el.ValueExpression

An expression that reads (getValue) and, if it is an lvalue, writes (setValue) a value.

jakarta.el.MethodExpression

An expression that identifies a method and invokes it (invoke).

jakarta.el.ELContext

Carries the context of one evaluation. Faces creates at most one per FacesContext and puts the FacesContext into it, so that resolvers can reach it.

jakarta.el.ELResolver

Resolves one segment of an expression, for example catalogueBean or books in #{catalogueBean.books}.

Immediate ${} and Deferred #{}

The language has two delimiters. The Jakarta EE Tutorial defines them like this:

  • ${expr} is evaluated immediately, as soon as the page is rendered, and its result is passed on.

  • #{expr} is deferred: whatever technology uses the expression decides when to evaluate it, possibly later in the lifecycle and possibly more than once.

Faces has a multi-phase lifecycle in which components are decoded, converted, validated and updated in a fixed order, so it needs deferral. The Tutorial states that Faces uses mostly deferred expressions, and that a deferred expression may be an lvalue (it can be assigned to) while an immediate one never assigns anything.

What Facelets Does With ${}

The Tutorial describes the immediate/deferred split in terms of Jakarta Server Pages, where a tag handler receives the already-evaluated result of ${}. Facelets is not Jakarta Server Pages, and the split does not hold there. This was tested: in a Facelets view a ${} expression in template text and in a component attribute is evaluated exactly like #{}. An h:inputText with value="${catalogueBean.query}" rendered the current value and, on a postback, called setQuery with the submitted text. So ${} is not "read-only" in a Facelets view.

The practical rule is the one that every Faces guide gives: use #{} everywhere in a Facelets view, and keep ${} for places where the technology really is Jakarta Server Pages. Mixing the two only confuses readers, because the old rule that ${} is evaluated once at render time is wrong for Facelets.

Evaluated When

"Deferred" does not mean "evaluated once, later". A component attribute holding a value expression is evaluated every time Faces needs the value, and that happens in several phases. The table lists where the common attributes are evaluated.

Expression Phase What happens

value on an output component

Render Response

Read (rvalue).

value on an input component

Render Response, then Update Model Values

Read to produce the initial value; written (lvalue) after conversion and validation succeeded.

rendered, disabled, readonly, styleClass, itemLabel, …​

Every phase that touches the component

Read. rendered is also evaluated while decoding, which decides whether the component takes part in the postback at all.

action

Invoke Application (Apply Request Values if the command is immediate)

The method runs; its return value is the navigation outcome.

actionListener

Same as action, before it

The method runs.

validator

Process Validations (Apply Request Values if immediate)

The method runs on the submitted value.

valueChangeListener

Process Validations

The method runs when the value changed.

listener of f:ajax

Invoke Application

The method runs.

The lifecycle page explains the phases themselves. The consequence for expressions is that a getter used in an expression runs more than once per request. In the test page of this section a request-scoped bean getBooks() was referenced by a stream expression, a size check, an empty test, an index access and a ui:repeat, and the bean counted six calls in a single GET. A getter must therefore be cheap and free of side effects; a database query belongs in a service method called once, for example from a @PostConstruct method or an f:viewAction, with the result kept in a field.

Expressions in Template Text and Composite Expressions

An expression may appear in an attribute or in the text of the view. In text, Facelets escapes the result for HTML. This was tested: with the query <b>x</b>, #{catalogueBean.query} in a paragraph was sent as <b>x</b>. To send markup deliberately, use h:outputText with escape="false", and only for text that is trusted.

An attribute may mix text and several expressions, such as value="Hello #{user.first} #{user.last}". The Tutorial calls this a composite expression. The parts are evaluated left to right, each is converted to a String, they are concatenated and the result is converted to the type the attribute expects. A composite expression is never an lvalue, so it cannot be used as the value of an input component. The string concatenation operator += builds the same string as one expression.

To print the characters #{ literally, escape the first character with a backslash or generate it from a literal. Both forms were tested in Facelets template text:

<p>\#{not.evaluated} and #{'#'}{also.literal}</p>

Value Expressions

A value expression yields a value. It is an rvalue when it can only be read, and an lvalue when it also names a target that can be assigned: a bean property, a map entry, an array or list element, or a scoped attribute. The expression #{catalogueBean.query} is both. When the page is rendered, it is read as an rvalue and shown as the default value of the text field. When the form is submitted, it is evaluated as an lvalue and the converted and validated value is pushed into the bean through setQuery. An expression that contains an operator, such as #{a + b}, or a method call with a result, such as #{catalogueBean.books.size()}, is only an rvalue; using it as the value of an input component fails at update time with a PropertyNotWritableException.

<h:inputText id="query" value="#{catalogueBean.query}"/>          <!-- lvalue: read and written -->
<h:outputText value="#{catalogueBean.books.size()} books"/>        <!-- rvalue only -->
<h:outputText value="#{book.year gt 1900 ? 'modern' : 'classic'}"/> <!-- rvalue only -->

Property access

A bean property is read with the dot or the bracket notation, and the two are equivalent for a name that is a valid identifier: #{book.title} and #{book['title']} mean the same. The bracket form is more general because the part inside is itself an expression, so #{book[column]} reads the property whose name is held in column. Single and double quotes are interchangeable for string literals, which is useful because the attribute itself already uses one kind. A list or array element needs the bracket form, #{catalogueBean.books[0].title}, and a map entry can use either, #{map.socks} or #{map['socks']}.

Property resolution on a bean follows the JavaBeans naming rules: the property title is read by getTitle() (or isTitle() for a boolean) and written by setTitle(String). EL 6.0 also reads the components of a Java record by their accessor names: the release notes of Jakarta Expression Language 6.0 list the new RecordELResolver, enabled by default. This was tested with the Bookshelf Book record: #{book.title} and #{book.year} resolve without any get prefix on Mojarra 4.1.17 and MyFaces 4.1.4 over the EL 6.0 of Tomcat 11.0.26. A server that ships an older EL (Jakarta EE 10 servers carry EL 5.0, which has no RecordELResolver) cannot resolve a record component by that name, so the record needs real getters there. Having EL 6.0 is necessary but was not shown to be sufficient: on Mojarra 4.0.26 with the same Tomcat 11.0.26 the catalogue page failed with a PropertyNotFoundException for title on the Book record, and the cause was not investigated. The Faces 5.0 milestone page lists the specification of the record and Optional resolvers as part of the resolver chain (issue 1954) as planned work. Records are read-only, so a record can feed h:outputText and h:dataTable but cannot be the target of an input component’s value.

An absent value never throws in the middle of a chain. When any link of #{catalogueBean.selected.title} is null, the whole expression is null and the output is empty. This was tested with selected equal to null. An unknown property is different: the page fails.

Type coercion

An attribute value is evaluated "in the context of an expected type". If the result is not of that type, the EL applies its coercion rules: strings to numbers, numbers to strings, an enum constant name to the enum, and so on. For example the rendered attribute expects a boolean, so rendered="true" and rendered="#{param.show eq 'yes'}" both work, and a String such as "false" coerces to false. Coercion is a feature of the EL and is entirely separate from Faces conversion: Faces converters (f:convertNumber, jakarta.faces.convert.Converter) turn submitted text into the type of the bound property, whereas EL coercion is applied inside the language to operands and results. The Faces specification states this separation explicitly. Because the two are separate, a failure to coerce is an ELException and not a validation message.

Method Expressions

A method expression identifies a public method and is evaluated by invoking it. It has the same syntax as an lvalue expression, so #{catalogueBean.select} and #{catalogueBean['select']} mean the same. Because a method may be invoked in different phases, method expressions always use the deferred syntax #{}. They can only be used in attributes, and in one of two forms: a single expression, or a literal. The literal form exists for action, where action="success" is a method expression that simply returns the string "success" as the navigation outcome.

The signature that the method must have depends on the attribute. The Faces specification fixes some of them, and the others were tested on Mojarra 4.1.17.

Attribute Method signature Notes

action of h:commandButton, h:commandLink, f:viewAction

No parameters, returns String

The return value is the navigation outcome. A method returning void (outcome null, same view) and one returning Object were both called in the test. Parameters passed in the expression are allowed (see below).

actionListener

void m(ActionEvent) or void m()

The specification describes the first form. The no-argument form and a form with a literal parameter, #{act.withParam('p')}, were all called in the test.

validator

void m(FacesContext, UIComponent, Object)

Throws ValidatorException to report an error. The same responsibilities as jakarta.faces.validator.Validator.validate.

valueChangeListener

void m(ValueChangeEvent)

Same responsibilities as ValueChangeListener.processValueChange.

listener of f:ajax

void m(AjaxBehaviorEvent) or void m()

Both forms were called in the test through an Ajax request.

The specification adds that these expressions are wrapped by jakarta.faces.event.MethodExpressionActionListener, jakarta.faces.validator.MethodExpressionValidator and jakarta.faces.event.MethodExpressionValueChangeListener so that they behave exactly like the equivalent Java class. An attribute that accepts a method expression can be set programmatically with a MethodExpression obtained from the ExpressionFactory, covered later on this page.

A method expression is looked up by name and parameter count and type, so an overloaded or misspelled method fails when it is used, not when the view is built. With a bean that only had select(Book) and select(long), the expression action="#{catalogueBean.select}" failed on the postback with Method not found: ...CatalogueBean...select(), because no method without parameters existed.

Method Parameters

Since EL 2.2 (Java EE 6, used by Faces 2.0 and later) both value and method expressions can pass arguments: expr-a.name(parameters) or expr-a[expr-b](parameters). The parameters are expressions themselves: literals, other value expressions, or any expression. This replaced the old workarounds with f:param, f:attribute and f:setPropertyActionListener for passing a row to an action.

<ui:repeat value="#{catalogueBean.books}" var="book" varStatus="st">
    <p>#{st.index}: #{book.title}
        <h:commandButton id="select" value="Select" action="#{catalogueBean.select(book)}"/>
    </p>
</ui:repeat>

Inside ui:repeat or h:dataTable, the argument book is resolved in the context of the clicked row. This was tested: a postback with the second button submitted called select with the second book. The argument is evaluated when the method is invoked, during Invoke Application, so it uses the state of the restored view and of the rebuilt row context, not the state at render time.

package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.model.Status;

@Named
@RequestScoped
public class CatalogueBean {

    private final List<Book> books = List.of(
            new Book(1, "Dune", "Frank Herbert", 1965, Status.AVAILABLE),
            new Book(2, "Neuromancer", "William Gibson", 1984, Status.BORROWED),
            new Book(3, "Emma", "Jane Austen", 1815, Status.AVAILABLE));

    private String query = "";
    private Book selected;

    public List<Book> getBooks() {
        return books;
    }

    public String getQuery() {
        return query;
    }

    public void setQuery(String query) {
        this.query = query;
    }

    public String select(Book book) {
        this.selected = book;
        return null;
    }

    public String refresh() {
        return null;
    }

    public Book getSelected() {
        return selected;
    }
}

The model types used by this page are the Book record of the project setup page with an added status, an enum, and a holder of constants:

package org.example.bookshelf.model;

public enum Status {
    AVAILABLE, BORROWED
}
package org.example.bookshelf.model;

public record Book(long id, String title, String author, int year, Status status) {
}
package org.example.bookshelf.model;

public final class Limits {

    public static final int MAX_LOANS = 5;
    public static final String PREFIX = "bk-";

    private Limits() {
    }
}

Literals and Operators

The literals are true and false, integers and floating-point numbers as in Java, strings in single or double quotes (with \", \' and \\ escapes), and null. The operators of the language, from the highest precedence to the lowest, are:

[]  .
()                                  (grouping)
-  (unary)   not  !   empty
*   /   div   %   mod
+   -  (binary)
+=                                  (string concatenation)
<   >   <=   >=   lt   gt   le   ge
==  !=   eq   ne
&&  and
||  or
? :                                 (conditional)
->                                  (lambda)
=                                   (assignment)
;                                   (semicolon)

The reserved words, which cannot be used as identifiers, are and, or, not, eq, ne, lt, gt, le, ge, true, false, null, instanceof, empty, div and mod. A bean named empty or a property named div cannot be reached with the dot notation; use the bracket form.

The words matter in XHTML because a Facelets view is an XML document. In an attribute value a bare < or && makes the file not well-formed. Use the word operators (lt, le, and, …​) or the escaped forms; both were tested:

<h:outputText value="#{catalogueBean.books.size() gt 0 and catalogueBean.query ne null}"/>
<h:outputText value="#{catalogueBean.books.size() &gt; 0 &amp;&amp; true}"/>

The following results were evaluated with the Tomcat 11.0.26 EL 6.0 implementation (books is the Bookshelf catalogue, book its first element).

Expression Result Remark

1 + 2 * 3

7

Integers stay integers.

7 div 2 and 7 / 2

3.5

/ and div are the same operator and always divide as floating point.

7 mod 4

3

% is the same.

'5' + 3

8

+ is arithmetic only; the operands are coerced to numbers.

null + 1

1

A null operand counts as zero in arithmetic.

'a' += 'b' += 1

ab1

+= concatenates strings; this is the operator to use for text.

empty '', empty null

true

empty is true for null, an empty string, an empty collection, an empty map and an empty array.

empty books

false

book.year > 1900 ? 'modern' : 'old'

modern

The conditional operator.

book.title == 'Dune' and book.year lt 2000

true

== compares with equals semantics after coercion.

Assignment = and the semicolon ; exist so that lambda expressions can be given a name and called, which is covered in the next section; they are not a way to set a bean property from the view, and an assignment never replaces a value binding.

Implicit Objects

An identifier that is not a bean name may be an implicit object, a name that Faces makes available in every expression. The Faces specification lists them in a table together with their scope. Since Faces 4.0 these objects are supplied through CDI (section 5.6.2 of the specification states that they must be resolved using CDI), which is why a Faces application that has no CDI container does not work in 4.x.

Name Source Scope

facesContext

FacesContext.getCurrentInstance()

request

externalContext

facesContext.getExternalContext()

request

request

externalContext.getRequest() (the HttpServletRequest)

request

session

externalContext.getSession() (the HttpSession)

session

application

externalContext.getContext() (the ServletContext)

application

requestScope

externalContext.getRequestMap()

request

sessionScope

externalContext.getSessionMap()

session

applicationScope

externalContext.getApplicationMap()

application

viewScope

facesContext.getViewRoot().getViewMap()

request (the map belongs to the view)

view

facesContext.getViewRoot()

request

param, paramValues

externalContext.getRequestParameterMap() and getRequestParameterValuesMap()

request

header, headerValues

externalContext.getRequestHeaderMap() and getRequestHeaderValuesMap()

request

cookie

externalContext.getRequestCookieMap()

request

initParam

externalContext.getInitParameterMap()

application

flash

externalContext.getFlash()

request

flowScope, flow

The current flow’s map, and the current Flow

flow

component

UIComponent.getCurrentComponent(facesContext)

dependent

cc

UIComponent.getCurrentCompositeComponent(facesContext)

dependent

resource

facesContext.getApplication().getResourceHandler()

request

Some of them are used constantly, and a few results were captured from the test page (a GET of index.xhtml?q=hi):

Expression Result

#{request.contextPath}, #{facesContext.externalContext.requestContextPath}

/bookshelf

#{view.viewId}

/index.xhtml

#{view.locale}

the locale of the view, es_ES on the test machine, which came from the JVM default

#{param.q} and #{param['q']}

hi

#{header['accept']}

*/* (the value sent by curl)

#{initParam['jakarta.faces.PROJECT_STAGE']}

Development

#{facesContext.currentPhaseId}

RENDER_RESPONSE 6

#{facesContext.application.projectStage}

Development

#{request.method}

GET

#{application.serverInfo}

Apache Tomcat/11.0.26

Notes on specific objects:

  • param['name'] and header['name'] need the bracket form when the key is not a valid identifier, for example a header such as X-Forwarded-For or a request parameter named mycom.productId.

  • requestScope, sessionScope, applicationScope and viewScope are maps of attributes. They hold what was stored with setAttribute or by externalContext.getRequestMap().put(...); viewScope is the view map of the UIViewRoot. A CDI bean is not reached through these maps: in the test, #{requestScope.catalogueBean.greeting} was empty although #{catalogueBean.greeting} worked. Reach CDI beans by their name, not through the scope maps.

  • cc is only meaningful inside a composite component implementation, where #{cc.attrs.title} reads the attribute title of the composite component and #{cc.clientId} its client id. Outside one it is empty (tested).

  • component is the component currently being processed. In the test, inside the tag h:outputText, #{component.id} returned the id of that output component (o for an explicit id="o", or a generated j_idt... id) and #{component.parent.id} that of its parent. #{component.clientId} is the id that appears in the HTML. It is how a page can refer to its own component without naming it.

  • flash is a map that survives one redirect. It is covered by the flash page, which also explains #{flash.keep.name}.

  • resource turns a resource name into a URL: #{resource['jakarta.faces:faces.js']} evaluated to /bookshelf/jakarta.faces.resource/faces.js.xhtml?ln=jakarta.faces&v=4.1.17&stage=Development in the test, and #{resource['css/site.css']} was empty because no such resource existed. The part before the colon is the library name; h:outputStylesheet and h:outputScript are the normal way to include resources.

  • session and request are the raw Servlet objects. Using them ties a view to Servlet classes; prefer a bean, or facesContext.externalContext, which also works outside Servlet containers.

Where Names Come From

A top-level identifier, catalogueBean in #{catalogueBean.books}, can refer to one of the following, according to the Tutorial: a lambda parameter, an EL variable, a managed bean, an implicit object, or a class with static fields and methods.

EL variables are created by tags. They are plain names that exist only inside the tag that declares them:

  • var on h:dataTable, ui:repeat and c:forEach: book in the example above.

  • ui:param name="x" value="..." passes a value into a template or an included file (see the templating page); c:set var="x" value="..." creates a variable at view build time, and the JSTL page shows the two things it does depending on scope.

  • f:importConstants type="..." var="..." creates a map of constants, described below.

  • ui:repeat has varStatus (here st) with the read-only properties index, first, last, even, odd, begin, end and step. It has no count: #{st.count} evaluated to 0 on Mojarra 4.1.17 and failed with a PropertyNotFoundException on MyFaces 4.1.4, so the one-based number of the row is #{st.index + 1}. h:dataTable has no varStatus at all (the attribute is ignored); its row number is #{component.namingContainer.rowIndex}. Both are measured on the data tables page.

Managed beans are CDI beans. A bean annotated @Named is available under the name given by the annotation, or, if the annotation has no value, under the class name with its first letter in lower case: CatalogueBean becomes catalogueBean. CDI is the only managed-bean facility in Faces 4.x. The old @ManagedBean annotation of Faces 2.x, deprecated in 2.3, was removed in 4.0, and so were faces-config.xml <managed-bean> entries and @ManagedProperty from javax.faces.bean. Books written for Faces 1.x and 2.x use them throughout.

An unknown name is not an error when it is only read, and is an error when it is a target. In the test, with no bean named nosuchbean:

  • #{nosuchbean} and #{nosuchbean.name} rendered empty, and #{empty nosuchbean} was true;

  • <h:inputText value="#{nosuchbean.name}"/> failed at Update Model Values with PropertyNotFoundException: ... value="#{nosuchbean.name}": Target Unreachable, identifier [nosuchbean] resolved to null.

A misspelled property of a known bean, #{catalogueBean.titel}, failed at once with Property [titel] not found on type [org.example.bookshelf.web.CatalogueBean]. In the Development project stage Mojarra shows both messages in its error page, with the view and the file position of the failing attribute.

A CDI bean name is resolved by the CDI container’s own ELResolver, which is described in the resolver section below. Tomcat needs Weld (or another CDI container) for this to work at all. The Tomcat and Weld page covers it.

Collections, Lambdas and Streams

The Expression Language since version 3.0 (Java EE 7) can build collections, define lambda expressions and run operations on streams, so some filtering that was once written in a bean can be written in the view. These results were all tested on the Bookshelf catalogue.

Collection literals

Expression Result

[1,3,5,2]

a list

[1, 'two', [3, 4]]

a list holding an integer, a string and another list

{1,2,3}

a set; the test printed [1, 2, 3]

{'one':1,'two':2}

a map

Lambda expressions

A lambda expression is a value expression with parameters, written with the arrow ->. The body is an EL expression. It can be called at once, or assigned to an identifier with = and called after the semicolon.

Expression Result

(x -> x + 1)(3)

4

v = (x, y) -> x + y; v(3, 4)

7

In Faces the more common use is as an argument of a stream operation.

Streams

A collection or array becomes a stream with the stream() method. A stream pipeline has a source, any number of intermediate operations that return a stream (filter, map, sorted, limit, distinct, flatMap, peek, substream) and a terminal operation that does not (toList, toArray, count, sum, average, min, max, reduce, findFirst, anyMatch, allMatch, noneMatch, forEach, iterator). That is the subset the Tutorial lists for the language. The operations do not change the source collection.

<h:outputText value="#{catalogueBean.books.stream().filter(b -> b.year gt 1900).map(b -> b.title).toList()}"/>

With the three Bookshelf books this printed [Dune, Neuromancer]. Other results from the same catalogue:

Expression Result

books.stream().map(b->b.year).max().get()

1984

books.stream().map(b->b.year).sum()

5764

books.stream().filter(b->b.year gt 1900).count()

2

books.stream().map(b->b.year).average().get()

1921.3333333333333

books.stream().sorted((x,y)->x.year - y.year).map(b->b.title).toList()

[Emma, Dune, Neuromancer]

[1,3,5,2].stream().sorted().toList()

[1, 2, 3, 5]

Two cautions apply. First, a stream expression is re-evaluated whenever the attribute is, so the pitfall from the section on evaluation applies: a heavy pipeline over a big list in a rendered or value attribute runs in several phases. Second, a pipeline in a view is logic in the view. Use it for presentation (a count, a label, a short filtered list), not for business rules, which belong to a service that can be tested without Faces.

Ordinary methods of the Java types can be called too, with parameters, since EL 2.2: #{catalogueBean.books.size()}, #{book.title.toUpperCase()} (tested: DUNE), #{book.title.length()} (4), #{catalogueBean.books.get(1).title}. A method call is an rvalue, never a target.

Static Fields, Enums and f:importConstants

The Expression Language can read the static fields of a class and call its static methods: Boolean.FALSE, Integer.MAX_VALUE. The class is named without its package. By default only java.lang is imported, which is why these two work in a view without any setup (tested: false and 2147483647). Other classes have to be imported into the ELContext before an expression can name them. Outside Faces, with the stand-alone ELProcessor, this is done in Java with importClass, importStatic or importPackage, and then DayOfWeek.MONDAY, Math.max(3,4) and Collections.emptyList() all evaluate (tested).

Inside a Facelets view there is no import tag for the language itself. Faces 2.3 added the tag f:importConstants for the common case: constants. It is placed in the f:metadata element of a view, and it exposes all the public static final fields of a class, interface or enum as a map.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<f:metadata>
    <f:importConstants type="org.example.bookshelf.model.Limits" var="Limits"/>
    <f:importConstants type="org.example.bookshelf.model.Status"/>
</f:metadata>
<h:head><title>Constants</title></h:head>
<h:body>
    <p>Maximum loans: #{Limits.MAX_LOANS}, id prefix: #{Limits['PREFIX']}</p>
    <p>#{Status.BORROWED == 'BORROWED'}</p>
</h:body>
</html>

The page printed 5, bk-, BORROWED for #{Status.BORROWED}, and true for the comparison above. The tag has two attributes, as documented in the tag library documentation:

  • type (required): the fully qualified class name. It is itself a value expression, evaluating to a String.

  • var (optional): the name of the request-scope attribute under which the map is exposed. It defaults to the simple class name, which is why Status works without var.

The map key is the name of the constant field, and the value is the constant, so #{Limits.MAX_LOANS} and #{Limits['MAX_LOANS']} are the same. The tag works for enums too; an enum constant is a static field. A convenient effect is that comparing against an enum constant needs no string: #{book.status == Status.BORROWED} rather than #{book.status == 'BORROWED'}. Both worked in the test because the EL coerces the string to the enum constant, but the first form fails at once if the constant is renamed, whereas the second one only compares to a string that no longer exists.

Compared with older approaches, f:importConstants replaces a bean that exposes constants through a getter, which Faces 1.x and 2.x books recommend, and OmniFaces' o:importConstants that did the same before 2.3. The restriction to f:metadata is a known limitation: the Faces 5.0 milestone page, which is upcoming work and not current behaviour, lists the issue "importConstants should be allowed everywhere, not only in f:metadata" (issue 1466). With the 4.1 tag, keep the f:importConstants elements inside the f:metadata of the view; in a template that means the metadata slot described on the templating page. The OmniFaces tag of the same name is covered on the OmniFaces page.

ELResolvers and Resolution Order

Every segment of an expression is resolved by an ELResolver. The Faces specification gives the example of <h:outputText value="#{user.address.street}"/>: the resolver chain is called three times, once for user, once for the property address of the result and once for street. The first call has a null base and the property user; the later ones have the previous result as base. Each resolver in the chain gets a chance. When one resolves the segment, it calls context.setPropertyResolved(true) and returns the value; when it does not recognise the segment it leaves the flag alone, and the next resolver is tried.

The chain Faces builds for Facelets and for programmatic access, in the order the specification adds the resolvers, is:

flowchart TD seg["Segment to resolve:
a base object and a property name"] r1["1. CompositeComponentAttributesELResolver
cc.attrs"] r2["2. Resolvers from faces-config.xml el-resolver
in file order"] r3["3. Resolvers from Application.addELResolver
in the order added"] r4["4. ResourceELResolver
resource name to URL"] r5["5. ResourceBundleELResolver
resource-bundle var in faces-config.xml"] r6["6. Stream, StaticField, Map, List,
Array and Bean resolvers from the EL API"] r7["7. ScopedAttributeELResolver
request, view, session, application maps"] done(["propertyResolved is true:
return the value"]) seg --> r1 --> r2 --> r3 --> r4 --> r5 --> r6 --> r7 --> done

The specification describes each of them.

Resolver Behaviour

faces.CompositeComponentAttributesELResolver

Handles cc.attrs. It returns a map that wraps the attributes of the composite component; when an attribute holds a ValueExpression the map evaluates it, and when it is absent but has a declared default, the default is used.

el.CompositeELResolver with <el-resolver> entries from faces-config.xml, then resolvers added with Application.addELResolver()

The extension points for application resolvers.

faces.ResourceELResolver

Turns #{resource['library:name']} into a resource URL by calling the ResourceHandler. A name with more than one colon throws an ELException.

el.ResourceBundleELResolver

The EL API resolver for ResourceBundle bases.

faces.ResourceBundleELResolver

Resolves the var of each <resource-bundle> declared in faces-config.xml, for example msg, to the ResourceBundle for the locale of the current view. The result is read-only.

The EL API resolvers

ExpressionFactory.getStreamELResolver(), StaticFieldELResolver, MapELResolver, ListELResolver, ArrayELResolver and BeanELResolver, in that order. The specification requires these actual instances.

faces.ScopedAttributeELResolver

The resolver for a top-level name that is a plain attribute. It looks in the request map, then the view map, then the session map, then the application map, and returns null (still marking the property resolved) if none has it. It also handles writes: a name found in a scope is replaced there, otherwise it is put in the request map.

The consequences of this order explain several common surprises:

  • Because the scoped-attribute resolver is the last, and because the implicit objects and CDI beans have their own resolution as described next, a name that you put into the request map with externalContext.getRequestMap().put(...) is found by #{name}, in the order request, view, session, application.

  • An application resolver registered through faces-config.xml is asked before the bean, list and map resolvers, so it can override how a property of a bean is read. This power should be used with care.

  • An unresolved top-level name resolves to null quietly, which is why a misspelled bean name does not fail when it is only read.

The ELResolver for the Faces side is also returned from FacesContext.getELContext().getELResolver() and Application.getELResolver(), and the CDI container maintains one top-level resolver, BeanManager.getELResolver(), in which a Faces-specific resolver is added. Resolvers have the lifetime of the application. A conformant implementation may arrange them differently internally as long as the behaviour is the same.

Writing an ELResolver

An application resolver is a class that extends jakarta.el.ELResolver with a public no-argument constructor. In the Bookshelf it exposes build information under the name build, so a footer can show #{build.version} without a bean. The resolver handles a null base with the property build, and returns a BuildInfo record. Further segments such as version are then resolved by the EL’s own bean resolver, which understands records.

package org.example.bookshelf.web;

public record BuildInfo(String version, String commit) {
}
package org.example.bookshelf.web;

import jakarta.el.ELContext;
import jakarta.el.ELResolver;
import jakarta.el.PropertyNotWritableException;

public class BuildInfoELResolver extends ELResolver {

    private static final BuildInfo BUILD = new BuildInfo("1.4.0", "9f3c2ab");

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (base == null && "build".equals(property)) {
            context.setPropertyResolved(true);
            return BUILD;
        }
        return null;
    }

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

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (base == null && "build".equals(property)) {
            throw new PropertyNotWritableException("build is read-only");
        }
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (base == null && "build".equals(property)) {
            context.setPropertyResolved(true);
            return true;
        }
        return false;
    }

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

It is registered in WEB-INF/faces-config.xml, where the element is el-resolver inside application:

<?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>
        <el-resolver>org.example.bookshelf.web.BuildInfoELResolver</el-resolver>
    </application>
</faces-config>

With the resolver in place, #{build.version} rendered 1.4.0 and #{build.commit} rendered 9f3c2ab (tested on Mojarra 4.1.17). The same effect can be reached in code with Application.addELResolver(...), which is what a library typically does at startup from a listener for jakarta.faces.event.PostConstructApplicationEvent.

Two points make resolvers correct. A resolver must call setPropertyResolved(true) only when it really handled the segment; resolvers that claim too much hide beans and implicit objects. And the method set of ELResolver changed: getFeatureDescriptors(), used by design-time tools in older versions, no longer exists in the EL 6.0 API (jakarta.el-api 6.0.1), so a resolver written for EL 5 that carries @Override on that method fails to compile against EL 6, and a resolver written for EL 6 compiles with the five abstract methods shown above.

CDI @Named Lookup

In Faces 4.x the expression #{catalogueBean.books} is resolved by CDI. A bean class carries @Named and a scope annotation from jakarta.enterprise.context (@RequestScoped, @SessionScoped, @ApplicationScoped, @ConversationScoped) or jakarta.faces.view.ViewScoped, as the beans page explains. The container then makes the bean available by name to the Expression Language. What matters for expressions:

  • The name is the value of @Named, or the decapitalised simple class name. @Named("shelf") makes #{shelf}.

  • The bean is looked up through a contextual reference, so a scoped bean is created on first use in its scope and the expression always reaches the right instance for the current request, session or view. A @Dependent bean (no scope annotation) is created for every reference to it.

  • A normal-scoped bean must be proxyable by CDI: a non-private no-argument constructor, and a class and methods that are not final.

  • Alternatives, stereotypes and qualifiers work as in any CDI application; @Named is the only part visible to EL.

  • The name space is shared with the implicit objects. A bean named request or session hides or collides with the built-in object of the same name; avoid those names.

On a Jakarta EE server this is already set up. On Tomcat, Faces needs a CDI container (Weld) and a beans.xml; without CDI, Mojarra 4.1 fails at startup with a ClassNotFoundException for its CDI-aware bean manager. The Tomcat and Weld page explains the setup.

Evaluating Expressions from Java

A bean, converter or phase listener sometimes needs to evaluate an expression in code, or to attach an expression to a component it creates. Faces gives three entry points.

API Use

Application.evaluateExpressionGet(facesContext, "#{...}", Type.class)

The shortcut for a one-off read of an expression.

Application.getExpressionFactory() and FacesContext.getELContext()

Create ValueExpression and MethodExpression objects and evaluate them.

UIComponent.setValueExpression(name, ve) and getValueExpression(name)

Bind an attribute of a component to an expression.

The following bean was run on Mojarra 4.1.17. Its evaluate action reads an expression, builds a value expression and checks whether it is read-only, and builds and invokes a method expression; the page shows the stored text.

package org.example.bookshelf.web;

import jakarta.el.ELContext;
import jakarta.el.ExpressionFactory;
import jakarta.el.MethodExpression;
import jakarta.el.ValueExpression;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.Application;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Named;

@Named
@RequestScoped
public class EvalBean {

    private String result;

    public void evaluate() {
        FacesContext context = FacesContext.getCurrentInstance();
        Application application = context.getApplication();

        String version = application.evaluateExpressionGet(context, "#{build.version}", String.class);

        ELContext elContext = context.getELContext();
        ExpressionFactory factory = application.getExpressionFactory();

        ValueExpression ve = factory.createValueExpression(elContext, "#{catalogueBean.query}", String.class);
        String query = ve.getValue(elContext);
        boolean readOnly = ve.isReadOnly(elContext);

        MethodExpression me = factory.createMethodExpression(
                elContext, "#{catalogueBean.refresh}", String.class, new Class<?>[0]);
        Object outcome = me.invoke(elContext, null);

        result = version + "|" + query + "|" + readOnly + "|" + outcome;
    }

    public String getResult() {
        return result;
    }
}
<h:form id="f">
    <h:commandButton id="go" value="Evaluate" action="#{evalBean.evaluate}"/>
    <h:outputText id="r" value="#{evalBean.result}"/>
</h:form>

After a click the output was 1.4.0||false|null: the version from the resolver of the previous section, the empty query, a writable expression and a null outcome. The work is done in an action method on purpose. An earlier variant of this bean did the same evaluations inside getResult(), which was itself reached through the expression #{evalBean.result}, and it failed on Mojarra 4.1.17 with Tomcat 11.0.26 with ELResolver did not handle type: [null] with property of [result], because the nested evaluations ran in the middle of the outer one on the same ELContext. Evaluate expressions from action methods, listeners and lifecycle callbacks, not from a getter that the view itself calls through an expression.

Because the FacesContext can be injected in Faces 4.x (@Inject FacesContext, one of the injectable Faces objects in the specification), a bean does not need FacesContext.getCurrentInstance(). The ELContext is created lazily on the first call to getELContext(); it has the same lifetime as the FacesContext, and when it is created Faces puts the FacesContext into it, which is how resolvers reach the context, and notifies the registered `ELContextListener`s.

The evaluation exceptions are jakarta.el.ELException and its subclasses PropertyNotFoundException (the property or a bean name does not exist), MethodNotFoundException (no such public method) and PropertyNotWritableException (the target is read-only).

The Bookshelf Catalogue Page

The whole page below uses the beans and model types shown earlier and the Facelets features of this page. It is a catalogue with a search box, a list with a per-row action and a short statistics line.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<f:metadata>
    <f:importConstants type="org.example.bookshelf.model.Limits" var="Limits"/>
    <f:importConstants type="org.example.bookshelf.model.Status"/>
</f:metadata>
<h:head>
    <title>Bookshelf</title>
</h:head>
<h:body>
    <h1>Catalogue</h1>

    <h:form id="search">
        <h:inputText id="query" value="#{catalogueBean.query}"/>
        <h:commandButton id="go" value="Search"/>
    </h:form>

    <p>
        #{catalogueBean.books.size()} books;
        newest: #{catalogueBean.books.stream().map(b -> b.year).max().get()};
        you may borrow up to #{Limits.MAX_LOANS}.
    </p>

    <h:form id="list">
        <ui:repeat value="#{catalogueBean.books}" var="book" varStatus="st">
            <p>
                #{st.index + 1}. #{book.title} (#{book.author}, #{book.year})
                <h:outputText value=" - on loan" rendered="#{book.status == Status.BORROWED}"/>
                <h:commandButton id="select" value="Select"
                                 action="#{catalogueBean.select(book)}"
                                 disabled="#{book.status == Status.BORROWED}"/>
            </p>
        </ui:repeat>
        <h:outputText id="chosen" value="Chosen: #{catalogueBean.selected.title}"
                      rendered="#{not empty catalogueBean.selected}"/>
    </h:form>
</h:body>
</html>

Common Pitfalls

  • Logic in getters. A getter is called several times per request (six calls were counted on a small page). It must not query a database, call a web service or have side effects. Compute once, in a method that runs once, and return a field.

  • rendered and the postback. rendered is evaluated while the submitted form is decoded. If it depends on state that exists only in the request that rendered the form (a request-scoped property set by a previous action, or a request parameter), then on the postback it is false, the component is skipped and its action never runs. Keep the state in a view-scoped bean, or in the component tree, so that it is the same on both requests.

  • Build time versus render time. The test attribute of c:if and the src attribute of ui:include are evaluated while the view is built, not while it is rendered; a component attribute such as rendered is evaluated while the view is rendered and decoded. Expressions that look alike behave differently. The page on build time and render time explains this.

  • A getter without a setter on an input. value="#{catalogueBean.books.size()}" or a property with only a getter fails on the postback. Use only an lvalue for an input component.

  • Operators in XHTML. <, > and && in an attribute break the XML. Use lt, gt, and, or escape them.

  • Quotes. The attribute value is delimited by one kind of quote, so write string literals with the other kind, for example value="#{x == 'a'}". Both kinds are valid for EL strings.

  • Checking null. A chain with a null link evaluates to null, not an exception, but a method call on null such as #{catalogueBean.selected.title.length()} fails. Test with empty or == null first.

  • Misspelled names. A property name that does not exist is a PropertyNotFoundException; a bean name that does not exist only fails when it is a target of an assignment, otherwise it quietly yields nothing.

  • Overloaded methods and action. A method expression picks a method by name and arguments. Pass the argument you intend, or provide the overload without parameters.

  • ${} and #{}. In Facelets they behave the same, so do not rely on ${} for a read-only guarantee.

  • CDI beans in scope maps. requestScope, sessionScope and applicationScope do not contain CDI beans, so #{sessionScope.cart} is empty for a @SessionScoped @Named bean; use #{cart}.

  • Long expressions. A stream pipeline or a long conditional in a view is hard to read and to test. When it is more than a line, move it to a method of a bean and call that.

  • Expression injection. Anything reachable by EL can be read by a view author: #{session}, #{request}, #{initParam}. Expressions come from the view files; never build an expression from user input (for example with Application.evaluateExpressionGet on a request parameter), because the Expression Language can call public methods.

What Changed Between Versions

The Expression Language is a specification of its own, with its own release for each Jakarta EE release. The Expression Language column for 4.0, 5.0 and 6.0 comes from the release pages and from the change appendix of the Jakarta Expression Language 6.0 specification, and the 1.2 row from the specification’s historical note. For the releases before 3.0 no Jakarta release page exists, so those rows name no EL version number. The Faces column states what Faces did with each release.

Version Faces Expression Language

JSF 1.0 / 1.1

Faces defines the #{} syntax for its own Jakarta Server Pages (then JSP) views. ValueBinding, MethodBinding, VariableResolver and PropertyResolver do the work.

The language is specific to Faces; JSP has ${} only.

JSF 1.2 (Java EE 5)

The Unified EL: JSP and Faces share one language, so ${} and #{} come from the same specification. ELResolver replaces the variable and property resolvers.

The unified language is defined in JSR 245 and took effect with JSP 2.1 and Faces 1.2.

JSF 2.0 (Java EE 6)

Facelets is the standard view technology, with expressions in template text. Method calls with parameters work in value and method expressions on a server that provides the Java EE 6 Expression Language.

The language is still part of the JSP and Faces specifications.

JSF 2.2 and 2.3 (Java EE 7 and 8)

2.3 adds f:importConstants; java.time support in converters; @ManagedBean is deprecated.

EL 3.0, the first release as an independent specification (JSR 341): APIs for stand-alone evaluation and operations on collection objects. The lambda expressions, collection literals, static fields and methods, +=, assignment and semicolon described on this page belong to the 3.0 language.

Faces 3.0 (Jakarta EE 9)

javax.el becomes jakarta.el.

EL 4.0: the package rename.

Faces 4.0 (Jakarta EE 10)

@ManagedBean, @ManagedProperty (from javax.faces.bean), <managed-bean> and the deprecated MethodBinding/ValueBinding/VariableResolver/PropertyResolver APIs are removed.

EL 5.0 (Java SE 11): BeanELResolver considers default methods, lambda coercion to functional interfaces, array coercion, MethodReference, generics in the API; ELResolver.getFeatureDescriptors() deprecated.

Faces 4.1 (Jakarta EE 11)

Uses EL 6.0. The 4.1 specification chapter still says "version 4.0 or higher" of the language.

EL 6.0 (Java SE 17): RecordELResolver enabled by default, OptionalELResolver disabled by default, the length property of arrays, the java.desktop module no longer required, ELResolver.getFeatureDescriptors() removed.

Faces 5.0 (upcoming, milestone 5.0.0-M9)

Planned, not current behaviour: f:importConstants allowed outside f:metadata, and the record and Optional resolvers specified as part of the chain.

Not covered by this page.

Where the Books Are Outdated

The two Faces books that treat the language, in particular JavaServer Faces 2.0: The Complete Reference by Ed Burns and Chris Schalk, describe the Expression Language as it was in Faces 1.2 and 2.0 (EL 1.2 and 2.1). Most of it is still correct for value expressions, method expressions, the scopes and the implicit objects. What is out of date:

  • No method parameters. Before EL 2.2, a method expression could not take arguments, so books show f:param, f:attribute, f:setPropertyActionListener and UIData.getRowData() to pass a row to an action. These still work, but #{bean.select(book)} is the normal way today.

  • No lambdas, streams, collection literals or static fields; those arrived with EL 3.0.

  • Managed beans are @ManagedBean with @ManagedProperty("#{...}") and <managed-bean> in faces-config.xml. These are gone; use @Named and @Inject.

  • ValueBinding, MethodBinding, VariableResolver and PropertyResolver are the old programmatic API, removed in 4.0; use ValueExpression, MethodExpression and ELResolver.

  • Examples with Java Server Pages (<%@ taglib %>, ${} as an immediate expression) do not apply to Facelets views.

  • Aranda and Wadia, Facelets Essentials (2008). The book argues for Facelets partly because it uses the Unified EL in template text, where JSP split the work between a container that evaluated ${} first and Faces that evaluated #{} later, with surprising results. That split is gone, and so is the premise that ${} and #{} behave differently in a view: in Facelets both are evaluated like a deferred expression (see What Facelets Does With ${}). What the book describes about writing expressions in Facelets pages is still right; what it says about installing and configuring Facelets 1.x is covered on the templating page.

  • The javax.el and javax.faces packages are jakarta.el and jakarta.faces.