EL Configuration & ELProcessor
|
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 Expression Language 6.0 (API jakarta.el:jakarta.el-api 6.0.1), Jakarta Pages 4.0 (chapters
2 and 3), Jakarta Servlet 6.1 and Apache Tomcat 11.0.26 on Java 21. Everything was verified on 2026-10-08: the facts against
the specifications and the Javadoc, and every example by building it into a Maven WAR and running it on Tomcat 11.0.26 with
curl, or, for the standalone part, by running it with the Eclipse Expressly 6.0.0 implementation; the outputs and error
messages shown are the ones those runs produced. The expression language itself (what ${book.title} means, the
operators, lambdas and streams) is the subject of the neighbouring EL pages of this section: Expression Language Basics, EL Operators & Type Coercion, EL Lambdas, Collections & Streams and EL Static Access, Records & Optional. This page is about the
environment of EL: the switches a container gives a page author to turn evaluation off or make it stricter, the two
syntaxes ${...} and #{...} and where each is legal, the way a web application adds its own resolver to the chain
that every expression goes through, and the way a program that is not a JSP page evaluates EL by itself. The running scenario
is the Bookshelf, a small catalogue of books.
Where EL Runs
The expression language was designed for the presentation layer, but since EL 3.0 it is a stand-alone specification with an API that does not need a servlet container. There are three situations, and the rest of the page keeps them apart because the defaults differ.
| Environment | Who evaluates the expression | Configuration |
|---|---|---|
A JSP page or tag file |
The JSP container (Jasper in Tomcat) parses |
|
Application code inside a web application |
The code, through the |
The |
A stand-alone program |
The code, with |
|
Implementations: Tomcat ships its own (org.apache.el.ExpressionFactoryImpl, verified below by asking a servlet inside
Tomcat 11.0.26), Eclipse GlassFish 8 uses Expressly (org.glassfish.expressly:expressly 6.0.0 on Maven Central), and a
stand-alone program or a unit test adds one dependency: the API alone (jakarta.el-api) cannot create an ExpressionFactory.
Switching EL Off: el-ignored and isELIgnored
The patterns ${expr} and #{expr} were not reserved before JSP 2.0 (and JSP 2.1 for the second), so a page written for
JSP 1.2 can contain them as plain text. The specification therefore lets a page, a group of pages or a tag file say that the
characters must pass through untouched. The rules are in Pages 4.0 sections 2.5 and 3.3.2, and the
configuration page and the
directives page show the descriptor and directive forms; this section keeps the
parts that depend on EL itself.
Group el-ignored |
isELIgnored in the page directive |
Result |
|---|---|---|
unspecified |
unspecified |
Ignored if the |
|
unspecified |
Evaluated |
|
unspecified |
Ignored |
any |
|
Evaluated |
any |
|
Ignored |
(no group applies to tag files) |
|
Evaluated |
(no group applies to tag files) |
|
Ignored |
Two consequences are easy to miss. First, a tag file never inherits el-ignored from the page that uses it: a legacy
directory whose group turns EL off still gets EL inside any tag file it calls. Second, the setting also decides whether the
quote sequences \$ and \# are recognised: with EL ignored they are not quotes, and the backslash is output as it
stands. The Bookshelf shows the first consequence with a group for an old legacy/ directory and a tag file title.tag:
<jsp-config>
<jsp-property-group>
<url-pattern>/legacy/*</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>
<%-- WEB-INF/tags/title.tag --%>
<%@ tag body-content="empty" pageEncoding="UTF-8" %>
<%@ attribute name="text" required="true" %>
title=${text} sum=${1 + 2}
<%-- legacy/page.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="t" tagdir="/WEB-INF/tags" %>
page: sum=${1 + 2}
tag file: <t:title text="Dune"/>
The response of /legacy/page.jsp was HTTP 200 with:
page: sum=${1 + 2}
tag file:
title=Dune sum=3
The page printed its expression verbatim, the tag file evaluated it. A page that needs EL inside the same directory overrides
the group with <%@ page isELIgnored="false" %>; /legacy/override.jsp printed page: sum=3 (verified). Without any group,
/ignored-default.jsp printed sum=3, because the descriptor is version="6.1".
isELIgnored="true" is a compatibility tool and not a security feature: it does not make a page safe, because a scriptlet or
a tag can still print request data, and the directive is itself in the page, so it protects against nothing an attacker
controls. Output escaping (c:out, fn:escapeXml) is what protects against cross-site scripting; the
guidance on Security collects the rest.
Unknown Identifiers: error-on-el-not-found and errorOnELNotFound
In a JSP page an identifier that no resolver knows is not an error by default. Pages 4.0 section 2.7 explains that the last
resolver of the chain, NotFoundELResolver, "always resolves the requested value, returning null"; EL’s lenient semantics
then turn the null into an empty string, a zero or false. That keeps a half-filled page presentable and hides typing
mistakes. Since Pages 3.1 a page can ask for the strict behaviour: error-on-el-not-found in a group and errorOnELNotFound
in the page and tag directives make the same lookup throw jakarta.el.PropertyNotFoundException.
Group error-on-el-not-found |
errorOnELNotFound in the directive |
An unknown identifier |
|---|---|---|
unspecified or |
unspecified |
|
|
unspecified |
|
any |
|
|
any |
|
|
(tag files have no group) |
|
|
(tag files have no group) |
|
|
<%-- notfound.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" %>
[${missing}] [${missing.title}] [${empty missing}]
<%-- notfound-strict.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" errorOnELNotFound="true" %>
[${missing}]
The first page returned HTTP 200 with [] [] [true]: the three lookups of an undefined missing were silently null, empty
and true. The second returned HTTP 500 with jakarta.el.PropertyNotFoundException: Unknown identifier [missing] as the
innermost cause. A group /strict/* with error-on-el-not-found set to true made /strict/page.jsp fail the same way, and
/strict/override.jsp with errorOnELNotFound="false" printed [] (all verified).
The setting concerns the first name of an expression only. A property that does not exist on an object that does exist is a
PropertyNotFoundException in any configuration: with book in the request, ${book.nope} returned HTTP 500 with
Property [nope] not found on type [org.example.bookshelf.Book] (verified). The practical rule for the Bookshelf is to
turn error-on-el-not-found on for the whole application during development and for a regression run, and to guard the
optional attributes of a live page with c:if and empty instead of relying on silence.
Invalid Expressions and Default Values
A syntactically invalid expression is a translation error (Pages 4.0 section 2.8), reported when the page is first compiled and not when a user is looking at it. A page with a broken expression:
<%@ page contentType="text/plain" %>
Total: ${1 +}
returned HTTP 500 from Tomcat, and the chain of causes (verified) ended with:
jakarta.el.ELException: Failed to parse the expression [${1 +}]
org.apache.el.parser.ParseException: Encountered " "}" "} "" at line 1, column 6.
Section 2.9 states the philosophy of the whole language: the EL "does not provide warnings, just default
values and errors". A default value is a type-correct value assigned to a subexpression with a problem (null becomes 0
in arithmetic, "" as text, false as a condition) and an error is an exception handled by the standard JSP machinery, for
example an error page. The Bookshelf errors are therefore always one of three kinds: a translation error (the syntax), a
PropertyNotFoundException or MethodNotFoundException (a property or method that is not there), and any exception thrown by
a getter, which EL wraps in an ELException.
Immediate and Deferred Syntax
The two delimiters are parsed the same way by EL, and Pages adds rules about where each may be used: ${expr} is evaluated
immediately, #{expr} is a deferred expression whose evaluation is left to someone else (a tag handler, a Faces
component). Before JSP 2.1 only ${} existed, which is why #{ in an old page is an error (next section). Which
delimiter is legal depends on where the expression appears:
-
In template text, only
${}.#{}is a translation error unless the compatibility switch below is on. -
In an attribute value, it depends on the attribute’s category in the tag library descriptor (TLD) or in the
attributedirective of a tag file. This is the only place deferred syntax is meaningful in a JSP page.
| Category in the TLD | Accepts | What the tag handler receives |
|---|---|---|
Static ( |
A literal only; no EL at all |
The text, converted to the setter’s type |
Dynamic ( |
A literal, a scriptlet expression or |
The value, already evaluated and coerced to the declared |
Deferred value ( |
A literal or |
A |
Deferred method ( |
A literal or |
A |
Dynamic or deferred (both |
A literal, a scriptlet, |
An |
The specification’s own table of examples (Table JSP.2-1) is the quickest reference: for an attribute declared static,
${x[y]} and #{x[y]} are errors; for a dynamic one, ${x[y]} is fine and #{x[y]} is an error; for a deferred one,
#{x[y]} is fine and ${x[y]} is an error; a plain word is fine in all three. The Bookshelf checks it with one
deferred and one dynamic attribute. The deferred tag keeps the expression and evaluates it when it chooses; the dynamic one
receives the value:
<tag>
<name>lazy</name>
<tag-class>org.example.bookshelf.LazyTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>value</name>
<required>true</required>
<deferred-value>
<type>java.lang.String</type>
</deferred-value>
</attribute>
</tag>
<tag>
<name>eager</name>
<tag-class>org.example.bookshelf.EagerTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>value</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
<type>java.lang.String</type>
</attribute>
</tag>
public class LazyTag extends SimpleTagSupport {
private ValueExpression value;
public void setValue(ValueExpression value) { // deferred: the argument is the expression
this.value = value;
}
@Override
public void doTag() throws JspException, IOException {
var out = getJspContext().getOut();
out.print("literal=" + value.isLiteralText() + " expr=" + value.getExpressionString());
if (!value.isLiteralText()) {
Object v = value.getValue(getJspContext().getELContext()); // evaluated now, by the handler
out.print(" value=" + v);
}
out.println();
}
}
<%-- deferred.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="bs" uri="https://example.org/bookshelf/tags" %>
<c:set var="title" value="Dune" scope="request"/>
<bs:lazy value="#{title}"/>
<bs:lazy value="plain text"/>
<bs:eager value="${title}"/>
The response was HTTP 200 with:
literal=false expr=#{title} value=Dune
literal=true expr=plain text
eager value=Dune
The deferred attribute reached the handler as an expression object (literal=false, the expression string intact), which
the handler evaluated with the page’s own ELContext (getJspContext().getELContext()), and the literal became a
literal-text ValueExpression. Two of the invalid combinations were run: <bs:eager value="#{title}"/> (dynamic attribute
with deferred syntax) and <bs:lazy value="${title}"/> (deferred attribute with immediate syntax) both returned HTTP 500
with According to TLD or attribute directive in tag file, attribute [value] does not accept any expressions (observed; the
message says the same for both, and for each the specification says "error"). A tag file declares the same categories with
the deferredValue, deferredValueType, deferredMethod and deferredMethodSignature attributes of its attribute
directive (see Directives). Deferred expressions are what Jakarta Faces tags and component libraries use; a plain
JSP application meets them mainly when it writes its own tags that must read and write a value, because a ValueExpression
supports setValue as well as getValue.
The Reserved Sequence in Plain Text
Because #{ has been reserved since JSP 2.1, plain text that contains it is a translation error. Total: #{books} in a
page returned HTTP 500 with #{...} is not allowed in template text (observed, line and column included). There are three
ways out, in the order of preference: write \#{ for the one occurrence, set deferredSyntaxAllowedAsLiteral="true" in the
page directive, or set deferred-syntax-allowed-as-literal to true for a group of legacy pages. The group and directive
forms are on Configuration and Property Groups; the escape rules are on the syntax page.
Adding Your Own ELResolver
Every identifier and every . or [] in an expression is resolved by an ELResolver. Pages 4.0 section 2.10 specifies the
chain that the container passes to all expressions of a page or tag file, in order, as a jakarta.el.CompositeELResolver:
the implicit-object resolver, any resolvers added through JspApplicationContext.addELResolver() (in registration order),
the resolver from ExpressionFactory.getStreamELResolver(), then the resolvers for static fields, maps, resource bundles,
lists, arrays, records (read-only) and beans, and finally the three JSP resolvers for scoped attributes, imports and "not
found". The first resolver that sets setPropertyResolved(true) on the ELContext wins; the later ones are not asked. The
Expression Language Basics page draws the whole chain; what matters here is that the application’s own resolvers sit very early
in it, before the map, list, array and bean resolvers and before the scoped-attribute resolver that looks up request, session
and application attributes.
That ordering has a security and a correctness side. Security: a resolver you register is asked first for every expression of every page of the application, so it must answer only for names it owns and must never resolve a name taken from the request. Correctness: a resolver that answers a name hides a request, session or application attribute of the same name, as the example below shows on purpose.
| Method | Contract |
|---|---|
|
Return the value, and call |
|
The type of the property for |
|
Assign, or throw |
|
|
|
The most general type of the properties this resolver handles for that base, or |
|
Method calls on the base and type conversion; a resolver that does not override them leaves the pair unresolved |
The Bookshelf keeps its site-wide settings in an immutable map and exposes them as ${settings.siteName}. The resolver is
read-only, answers one name and never looks at the request:
public class BookshelfSettings extends ELResolver {
private static final String NAME = "settings";
private final Map<String, String> values;
public BookshelfSettings(Map<String, String> values) {
this.values = Map.copyOf(values);
}
@Override
public Object getValue(ELContext context, Object base, Object property) {
if (base == null && NAME.equals(property)) {
context.setPropertyResolved(true);
return values;
}
return null; // not mine: leave the pair unresolved
}
@Override
public Class<?> getType(ELContext context, Object base, Object property) {
if (base == null && NAME.equals(property)) {
context.setPropertyResolved(true);
}
return null; // read-only: getType must return null
}
@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
if (base == null && NAME.equals(property)) {
context.setPropertyResolved(true);
throw new PropertyNotWritableException("settings is read-only");
}
}
@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
if (base == null && NAME.equals(property)) {
context.setPropertyResolved(true);
return true;
}
return false;
}
@Override
public Class<?> getCommonPropertyType(ELContext context, Object base) {
return base == null ? String.class : null;
}
}
Registration happens once, while the application starts. The ServletContextListener asks the JSP factory for the
application’s JspApplicationContext:
@WebListener
public class SettingsInstaller implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext ctx = event.getServletContext();
JspFactory.getDefaultFactory().getJspApplicationContext(ctx)
.addELResolver(new BookshelfSettings(Map.of("siteName", "Bookshelf", "pageSize", "20")));
}
}
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="settings" value="request attribute" scope="request"/>
site=${settings.siteName} pageSize=${settings.pageSize + 1} missing=[${settings.nope}]
other=${requestScope.settings}
The response was HTTP 200 with:
site=Bookshelf pageSize=21 missing=[]
other=request attribute
${settings.siteName} found the resolver’s map even though a request attribute called settings existed, which is the
ordering of section 2.10 at work: the request attribute is reachable only by its scope, ${requestScope.settings}.
${settings.pageSize + 1} shows the coercion of the string "20" to a number, and ${settings.nope} is an unknown key of
a Map, hence null and an empty string (a Map never throws for a missing key).
Registration is time-limited. The Javadoc says it is illegal to register an ELResolver "after the application has received
any request from the client", and that the call throws IllegalStateException once all ServletContextListener`s have run;
the restriction lets the container optimise the common case of an unextended chain, and a resolver cannot be removed. On
Tomcat 11.0.26 the limit is the first evaluation of a JSP: a servlet that tried to register a second resolver as the very
first request of a freshly deployed application succeeded, and the same servlet called after one JSP page had been served
returned `IllegalStateException: Cannot call addELResolver after the first request has been made (both observed).
Register from the listener and treat anything later as an error. Frameworks use the same hook: Faces registers its resolver
this way, and addELContextListener lets a resolver that needs extra context (not only the JspContext) put it into every
new ELContext.
EL Outside a Page: ELProcessor, ELManager and StandardELContext
EL 3.0 made the language usable without a JSP page. There are two layers.
-
jakarta.el.ELProcessoris the simple one: a facade that evaluates expressions written without delimiters, assigns values, defines beans, variables and functions. It is documented as "not thread safe": one instance per task, or per session if the user is meant to configure an environment that lasts. -
jakarta.el.ELManagerandjakarta.el.StandardELContextare the environment behind it: they hold theExpressionFactory, the resolvers, the imports and the function and variable maps.ELProcessor.getELManager()returns the manager, which is where custom resolvers, bean-name resolvers and imports are added. The complete, lower-level API, withExpressionFactory.createValueExpressionandcreateMethodExpression, is the one JSP and Faces containers use.
| Method | What it does |
|---|---|
|
Evaluates the expression and returns the result. The string is bracketed by the processor, so |
|
Evaluates and coerces to the type |
|
Assigns through the |
|
Binds a name to an expression (stored as a |
|
Adds a bean to the local bean repository; a bean hides another of the same name |
|
Maps a public static method to |
|
The manager for the resolver, import and function configuration |
| Operation | Purpose |
|---|---|
|
Adds a custom resolver to the chain of this environment (same idea as |
|
Registers a |
|
Adds to the |
|
Maps a static method to an EL function |
|
Assigns a |
|
Observes the evaluation of every expression |
|
Finds the implementation on the class path |
|
The |
The next program exercises every row of the first table and the most useful ones of the second. It does not run in a servlet
container: it needs jakarta.el-api 6.0.1 and Expressly 6.0.0 on the class path, and the Book record of the Bookshelf.
public class Standalone {
public static void main(String[] args) throws Exception {
ELProcessor elp = new ELProcessor();
System.out.println("1+2 -> " + elp.eval("1 + 2"));
elp.defineBean("book", new Book(1, "Dune", "Frank Herbert", 1965));
System.out.println("book.title -> " + elp.eval("book.title"));
System.out.println("book.year + 1 -> " + elp.eval("book.year + 1"));
Integer year = elp.getValue("book.year", Integer.class);
System.out.println("getValue year -> " + year);
// assignment goes through the ELResolver; records are read-only
try {
elp.setValue("book.title", "Emma");
} catch (RuntimeException e) {
System.out.println("setValue on a record -> " + e.getClass().getName() + ": " + e.getMessage());
}
elp.defineBean("shelf", new java.util.ArrayList<>(List.of(
new Book(1, "Dune", "Frank Herbert", 1965),
new Book(2, "Emma", "Jane Austen", 1815),
new Book(3, "Ubik", "Philip K. Dick", 1969))));
System.out.println("titles after 1900 -> "
+ elp.eval("shelf.stream().filter(b -> b.year > 1900).map(b -> b.title).toList()"));
elp.setVariable("limit", "1900");
System.out.println("variable limit -> " + elp.eval("shelf.stream().filter(b -> b.year > limit).count()"));
elp.defineFunction("bs", "isbn", "org.example.bookshelf.Isbn", "format");
System.out.println("function -> " + elp.eval("bs:isbn('9780441172719')"));
System.out.println("assign and semicolon -> " + elp.eval("n = 4; n * n"));
System.out.println("lambda -> " + elp.eval("(x -> x * 2)(21)"));
// ELManager: the environment behind ELProcessor
ELManager mgr = elp.getELManager();
mgr.importClass("java.util.Locale");
System.out.println("importClass -> " + elp.eval("Locale.ROOT.toString().isEmpty()"));
mgr.importStatic("java.lang.Math.max");
System.out.println("importStatic -> " + elp.eval("max(3, 7)"));
// BeanNameResolver: look beans up by name on demand
Map<String, Object> repo = Map.of("featured", new Book(9, "Solaris", "Stanislaw Lem", 1961));
mgr.addBeanNameResolver(new BeanNameResolver() {
@Override
public boolean isNameResolved(String beanName) {
return repo.containsKey(beanName);
}
@Override
public Object getBean(String beanName) {
return repo.get(beanName);
}
});
System.out.println("bean name resolver -> " + elp.eval("featured.title"));
// unknown identifier in standalone mode
try {
elp.eval("nothing.title");
} catch (PropertyNotFoundException e) {
System.out.println("unknown identifier -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
// Low level: ExpressionFactory + StandardELContext, with deferred syntax
ExpressionFactory factory = ELManager.getExpressionFactory();
StandardELContext ctx = new StandardELContext(factory);
ctx.getVariableMapper().setVariable("book",
factory.createValueExpression(new Book(2, "Emma", "Jane Austen", 1815), Book.class));
ValueExpression ve = factory.createValueExpression(ctx, "#{book.title}", String.class);
System.out.println("ValueExpression -> " + ve.getValue(ctx) + " (" + ve.getExpressionString() + ")");
System.out.println("implementation -> " + factory.getClass().getName());
}
}
Run with java -cp 'target/classes:lib/*' org.example.bookshelf.Standalone on Java 21, it printed:
1+2 -> 3
book.title -> Dune
book.year + 1 -> 1966
getValue year -> 1965
setValue on a record -> jakarta.el.PropertyNotWritableException: The class 'org.example.bookshelf.Book' does not have a writable property 'title'.
titles after 1900 -> [Dune, Ubik]
variable limit -> 2
function -> 978-0441172719
assign and semicolon -> 16
lambda -> 42
importClass -> true
importStatic -> 7
bean name resolver -> Solaris
unknown identifier -> PropertyNotFoundException: ELResolver cannot handle a null base Object with identifier 'nothing'
ValueExpression -> Emma (#{book.title})
implementation -> org.glassfish.expressly.ExpressionFactoryImpl
Inside a container no implementation has to be added: the API is a provided dependency, as in the Bookshelf POM, and
the container supplies the factory.
@WebServlet("/elprocessor")
public class ElProcessorServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain;charset=UTF-8");
ELProcessor elp = new ELProcessor();
elp.defineBean("book", new Book(1, "Dune", "Frank Herbert", 1965));
resp.getWriter().println(elp.<String>eval("book.title"));
resp.getWriter().println(jakarta.el.ELManager.getExpressionFactory().getClass().getName());
}
}
The response was Dune and org.apache.el.ExpressionFactoryImpl.
Four observations carry over to real code:
-
The defaults differ from a page. In a page an unknown identifier is
null; in the stand-alone runnothing.titlethrewPropertyNotFoundException, whereas the resolver chain of a page ends withNotFoundELResolver, which answersnull(Pages 4.0 section 2.10). The same expression can therefore behave differently in a unit test withELProcessorand in the page it was extracted from. -
Assignment is the resolver’s decision. The EL specification says an assignment "is determined by the ELResolver": a stand-alone environment lets
n = 4create a bean namedn, a JSP container may assign to a scoped attribute, and arecordrefuses (PropertyNotWritableException). The;operator evaluates and discards the left side, son = 4; n * nreturns 16. -
No delimiters.
eval("${book.title}")is wrong forELProcessor(the processor adds the brackets); the fullExpressionFactoryAPI needs the delimiters (#{book.title}above) and distinguishescreateValueExpressionfromcreateMethodExpression. -
The implementation is chosen by the class path.
ELManager.getExpressionFactory()returnedorg.glassfish.expressly.ExpressionFactoryImplin the stand-alone run, andorg.apache.el.ExpressionFactoryImplinside Tomcat 11.0.26, where a servlet in the Bookshelf WAR callednew ELProcessor()with the API as aprovideddependency and returnedDuneforbook.title(verified). A web application must therefore not bundle its own EL implementation.
Security: Do Not Evaluate Untrusted Text
EL is a programming language: it can call methods, assign, use lambdas and, when classes and static members are imported,
reach static fields and methods. ELProcessor.eval(String) compiles and runs whatever it is given, so a string built from a
request parameter, a header or a database column the user can edit is code execution, and a page that passes user input to
an EL-evaluating tag attribute or to ExpressionFactory.createValueExpression has the same flaw (this is what "EL injection"
means). Four rules follow.
-
Never pass untrusted text as the expression. Pass it as a value:
elp.defineBean("query", userText)and an expression such asquery.length()keep the data and the code apart. -
Keep expression strings in the application: constants, configuration files that only operators can write, resources inside the WAR.
-
In a custom resolver, answer only the names you own, make it read-only, and do not resolve names taken from the request (an attacker would use it to reach beans or settings you did not intend).
-
Keep output escaped: a resolved value is still text that goes into HTML, so
c:outorfn:escapeXmlapplies to settings just as to any other value. Do not combineisELIgnored="true"with the idea that it is a defence; it only changes the characters a page treats as EL.
A Look at EL 6.1 (Jakarta EE 12, Under Development)
Jakarta EE 12 is in progress and none of it is released. The Jakarta EE site lists Jakarta Expression Language 6.1 as "under development" with these planned items:
-
Language enhancements requested by users: an Elvis operator and the use of
+to merge collections. -
A performance optimisation for scoped attributes, done together with Jakarta Pages 4.1.
-
Pluggable caching mechanisms.
-
The TCK moves to the Expression Language project. No removals, deprecations or incompatible changes are planned.
The same page gives Java SE 17 as the minimum. Nothing on this page needs 6.1, the 6.1 API has only milestone releases on Maven Central (6.1.0-M2), and an example written with an Elvis operator would not run on Tomcat 11, so no code is shown for it. The advice is to write for 6.0 and to re-read the release page when 6.1 and Pages 4.1 are final.
The Bookshelf Scenario, Put Together
-
In development the Bookshelf turns
error-on-el-not-foundon for*.jsp(a group inweb.xml), so a mistyped${bokk.title}is an error on the first request instead of an empty cell. -
The old admin pages under
/legacy/keepel-ignoredwhile they are migrated; their tag files still evaluate EL, so those tag files are reviewed first. -
A start-up listener registers
BookshelfSettings; pages say${settings.siteName}. -
A custom
bs:lazytag takes a#{...}expression and evaluates it when it renders. -
The nightly import job evaluates configured price rules with
ELProcessoronjakarta.el-apiand Expressly, with the catalogue records defined as beans and the rules read from a file the operators control.
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
|
A group with |
Change the group, or set |
|
Groups do not apply to tag files, whose default is to evaluate |
Set |
|
|
Write |
|
The wrong delimiter for the attribute category: |
Match the delimiter to the TLD category (Table JSP.2-1) or declare the attribute as dynamic or deferred |
|
|
Fix the name or the scope; guard optional data with |
|
A property that the bean does not have; this is an error whatever the configuration says |
Fix the property name or add the getter |
|
Invalid syntax: a translation error, found on first compilation |
Fix the expression; precompile the JSPs (see the Jasper page of this section) so such errors appear at build time |
|
The target is a record, a read-only resolver or an immutable map |
Assign to a mutable bean or define the variable with |
|
Registration after the first evaluated JSP |
Register in a |
|
The resolver order: custom resolvers before the scoped-attribute resolver |
Choose names that do not collide, and use |
|
|
Pass |
A unit test with |
Stand-alone context throws on unknown identifiers; a page returns |
Test with the same switches ( |
|
It keeps mutable state and is not thread safe |
One instance per task or session; configure then copy the immutable data |
|
Only |
Add Expressly (or another implementation) |
Application bundles an EL implementation and the container’s version conflicts |
The implementation JAR is inside |
Use the API as |
What Changed Recently
| Version | Change |
|---|---|
EL 2.1 and JSP 2.1 |
Unified EL: |
EL 3.0 |
Stand-alone use ( |
EL 4.0 (Jakarta EE 9) |
The transition from the |
EL 5.0 (Jakarta EE 10) |
Java 11 minimum; |
EL 6.0 (Jakarta EE 11) |
Java 17 minimum; deprecated code removed; the |
Pages 3.1 (Jakarta EE 10) |
|
Pages 4.0 (Jakarta EE 11) |
The deprecated classes of |
EL 6.1 and Pages 4.1 (Jakarta EE 12, planned) |
Elvis operator, |
Where the Books Are Outdated
The JSP and JSTL books consulted for this section (listed in the section bibliography) predate the unified expression language, so what they teach about EL is either absent or a different mechanism.
| The books' era | Today (Pages 4.0, EL 6.0) |
|---|---|
No expression language in JSP pages (JSP 1.2); every dynamic value is a scriptlet or an |
EL in template text and attribute values of every page, evaluated by the container; scripting can be disabled with |
EL only inside the attributes of JSTL 1.0 tags, evaluated by the tag library itself (the |
A single language evaluated by the container for every tag; the twin libraries are gone |
Custom tags that evaluate EL themselves, with the Jakarta Taglibs |
A |
|
Servlet 2.4+ descriptors evaluate by default; |
Missing values silently print nothing, and nothing in the book says it can be changed |
|
Extending variable resolution with a custom |
|
EL cannot be used outside a page; testing a rule means a request to a container |
|
|
|
References
-
Jakarta Pages 4.0 — 2.10 Resolution of Variables and their Properties
-
Jakarta Pages 4.0 — 3.3.8 Deferred Syntax (character sequence #{)
-
Jakarta Expression Language 6.0 — A.1 Changes between 6.0 and 5.0
-
Jakarta Expression Language 6.0 — A.2 Changes between 5.0 and 4.0
-
Jakarta Expression Language 6.0 API — ELManager.addBeanNameResolver
-
Eclipse Expressly, the Jakarta Expression Language implementation of GlassFish