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 |
|---|---|
|
Parses an expression string into an object. Faces keeps one per application ( |
|
An expression that reads ( |
|
An expression that identifies a method and invokes it ( |
|
Carries the context of one evaluation. Faces creates at most one per |
|
Resolves one segment of an expression, for example |
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 |
|---|---|---|
|
Render Response |
Read (rvalue). |
|
Render Response, then Update Model Values |
Read to produce the initial value; written (lvalue) after conversion and validation succeeded. |
|
Every phase that touches the component |
Read. |
|
Invoke Application (Apply Request Values if the command is |
The method runs; its return value is the navigation outcome. |
|
Same as |
The method runs. |
|
Process Validations (Apply Request Values if |
The method runs on the submitted value. |
|
Process Validations |
The method runs when the value changed. |
|
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 |
|---|---|---|
|
No parameters, returns |
The return value is the navigation outcome. A method returning |
|
|
The specification describes the first form. The no-argument form and a form with a literal parameter, |
|
|
Throws |
|
|
Same responsibilities as |
|
|
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() > 0 && 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 |
|---|---|---|
|
|
Integers stay integers. |
|
|
|
|
|
|
|
|
|
|
|
A |
|
|
|
|
|
|
|
|
|
|
|
The conditional operator. |
|
|
|
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 |
|---|---|---|
|
|
request |
|
|
request |
|
|
request |
|
|
session |
|
|
application |
|
|
request |
|
|
session |
|
|
application |
|
|
request (the map belongs to the view) |
|
|
request |
|
|
request |
|
|
request |
|
|
request |
|
|
application |
|
|
request |
|
The current flow’s map, and the current |
flow |
|
|
dependent |
|
|
dependent |
|
|
request |
Source: Jakarta Faces 4.1 specification, 5.6.2.1 Implicit Objects for Facelets and Programmatic Access
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 |
|---|---|
|
|
|
|
|
the locale of the view, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Notes on specific objects:
-
param['name']andheader['name']need the bracket form when the key is not a valid identifier, for example a header such asX-Forwarded-Foror a request parameter namedmycom.productId. -
requestScope,sessionScope,applicationScopeandviewScopeare maps of attributes. They hold what was stored withsetAttributeor byexternalContext.getRequestMap().put(...);viewScopeis the view map of theUIViewRoot. 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. -
ccis only meaningful inside a composite component implementation, where#{cc.attrs.title}reads the attributetitleof the composite component and#{cc.clientId}its client id. Outside one it is empty (tested). -
componentis the component currently being processed. In the test, inside the tagh:outputText,#{component.id}returned the id of that output component (ofor an explicitid="o", or a generatedj_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. -
flashis a map that survives one redirect. It is covered by the flash page, which also explains#{flash.keep.name}. -
resourceturns 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=Developmentin the test, and#{resource['css/site.css']}was empty because no such resource existed. The part before the colon is the library name;h:outputStylesheetandh:outputScriptare the normal way to include resources. -
sessionandrequestare the raw Servlet objects. Using them ties a view to Servlet classes; prefer a bean, orfacesContext.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:
-
varonh:dataTable,ui:repeatandc:forEach:bookin 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 onscope. -
f:importConstants type="..." var="..."creates a map of constants, described below. -
ui:repeathasvarStatus(herest) with the read-only propertiesindex,first,last,even,odd,begin,endandstep. It has nocount:#{st.count}evaluated to0on Mojarra 4.1.17 and failed with aPropertyNotFoundExceptionon MyFaces 4.1.4, so the one-based number of the row is#{st.index + 1}.h:dataTablehas novarStatusat 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}wastrue; -
<h:inputText value="#{nosuchbean.name}"/>failed at Update Model Values withPropertyNotFoundException: ... 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 |
|---|---|
|
a list |
|
a list holding an integer, a string and another list |
|
a set; the test printed |
|
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 |
|---|---|
|
|
|
|
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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 aString. -
var(optional): the name of the request-scope attribute under which the map is exposed. It defaults to the simple class name, which is whyStatusworks withoutvar.
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:
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 |
|---|---|
|
Handles |
|
The extension points for application resolvers. |
|
Turns |
|
The EL API resolver for |
|
Resolves the |
The EL API resolvers |
|
|
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 |
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.xmlis 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
nullquietly, 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
@Dependentbean (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;
@Namedis the only part visible to EL. -
The name space is shared with the implicit objects. A bean named
requestorsessionhides 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 |
|---|---|
|
The shortcut for a one-off read of an expression. |
|
Create |
|
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.
-
renderedand the postback.renderedis 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 isfalse, 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
testattribute ofc:ifand thesrcattribute ofui:includeare evaluated while the view is built, not while it is rendered; a component attribute such asrenderedis 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. Uselt,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 anulllink evaluates tonull, not an exception, but a method call onnullsuch as#{catalogueBean.selected.title.length()}fails. Test withemptyor== nullfirst. -
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,sessionScopeandapplicationScopedo not contain CDI beans, so#{sessionScope.cart}is empty for a@SessionScoped @Namedbean; 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 withApplication.evaluateExpressionGeton 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 |
The language is specific to Faces; JSP has |
JSF 1.2 (Java EE 5) |
The Unified EL: JSP and Faces share one language, so |
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 |
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, |
Faces 3.0 (Jakarta EE 9) |
|
EL 4.0: the package rename. |
Faces 4.0 (Jakarta EE 10) |
|
EL 5.0 (Java SE 11): |
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): |
Faces 5.0 (upcoming, milestone |
Planned, not current behaviour: |
Not covered by this page. |
Source: Jakarta Expression Language 6.0, 5.0 and 4.0 release pages, and the Faces 5.0 milestone 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:setPropertyActionListenerandUIData.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
@ManagedBeanwith@ManagedProperty("#{...}")and<managed-bean>infaces-config.xml. These are gone; use@Namedand@Inject. -
ValueBinding,MethodBinding,VariableResolverandPropertyResolverare the old programmatic API, removed in 4.0; useValueExpression,MethodExpressionandELResolver. -
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.elandjavax.facespackages arejakarta.elandjakarta.faces.