Evaluating Juel Expressions Outside a Container: ELContext, Caching, and Version Pitfalls
Learn how to evaluate Unified Expression Language strings with Juel in plain Java, cache compiled expressions safely, and avoid javax.el versus jakarta.el import errors.
13 Sept 2025, 18:23 UTC

If you need to evaluate a Unified Expression Language string such as #{person.name} in a plain Java process—without a servlet container—the practical answer is to create an ExpressionFactory, parse the expression once, and evaluate it against a fresh ELContext for each request. The compiled expression object is immutable and can be cached; the context is not thread-safe and must be created per evaluation.
The Core Mechanism: Parse Once, Evaluate Per Request
Juel is an established standalone implementation of the Java/Jakarta Unified Expression Language. In a standalone application you still use the standard EL API: obtain an ExpressionFactory, call createValueExpression to parse the string, and resolve the result through an ELContext. The context supplies an ELResolver chain that maps identifiers like person to Java objects and then resolves properties such as name on those objects.
Application beans are not visible automatically. You must either bind them to the context's variable mapper or expose them through a custom resolver. The following illustrative configuration assumes EL 2.2 / Juel 2.x with javax.el imports:
// Assumes Juel 2.x / EL 2.2 (javax.el package).
ExpressionFactory factory = ExpressionFactory.newInstance();
// Minimal context: resolves bean properties and stores variables.
class JuelContext extends ELContext {
private final BeanELResolver resolver = new BeanELResolver();
private final VariableMapper vars = new DefaultVariableMapper();
private final FunctionMapper funcs = new DefaultFunctionMapper();
@Override public ELResolver getELResolver() { return resolver; }
@Override public VariableMapper getVariableMapper() { return vars; }
@Override public FunctionMapper getFunctionMapper() { return funcs; }
}
// Parse once.
ValueExpression expr = factory.createValueExpression(
new JuelContext(), "#{person.name}", String.class
);
// Evaluate in a new context per call.
JuelContext ctx = new JuelContext();
ctx.getVariableMapper().setVariable(
"person",
factory.createValueExpression(new Person("Ada"), Person.class)
);
String name = (String) expr.getValue(ctx);
In this pattern, Person is a simple POJO with a standard getName() accessor. The BeanELResolver resolves the name property on the base object returned by the person variable. The expected check is that name equals the value returned by Person.getName(). Because the code is illustrative, confirm the exact resolver and mapper types against the Juel artifact in your build file.
Version Boundaries: javax.el Versus jakarta.el
The most common standalone Juel failure is an import mismatch. Juel 2.x implements EL 2.2 and uses the javax.el package. Juel 3.x implements the Jakarta EE lineage and uses jakarta.el. Importing the wrong package for the Juel version on your classpath causes ClassNotFoundException or NoClassDefFoundError at runtime, not a helpful compile-time message in all build setups.
| Aspect | EL 2.2 / Juel 2.x | EL 3.x / Juel 3.x |
|---|---|---|
| API package | javax.el | jakarta.el |
| Typical symptom of mismatch | ClassNotFoundException | NoClassDefFoundError |
| Verification step | Check the Juel implementation artifact version in your build file and match every EL import to the corresponding package. |
Caching Rules: Immutable Expressions, Mutable Contexts
Compiled ValueExpression and MethodExpression instances are immutable and safe to cache. In a high-traffic path, parse the expression string once and store the resulting object. Do not apply the same rule to ELContext: it holds request-scoped variable bindings and resolver state, is not thread-safe, and should never be stored in a static field or shared across threads.
- Cache:
ValueExpression,MethodExpression,ExpressionFactory. - Do not cache:
ELContext, variable mappers bound to a request, function mappers with request state.
Method Expressions and Portability Limits
Method expressions such as #{bean.submit(id)} are compiled into MethodExpression objects. You invoke them through getMethodInfo and invoke, and the EL implementation applies parameter coercion rules to convert arguments to the target method signature. However, method-expression argument support varies by EL version and container. For maximum portability across Juel versions and Jakarta EE runtimes, prefer zero-argument actions when the surrounding framework allows it.
Common Mistakes
- Sharing an
ELContext: Creating one context in a static initializer and reusing it across threads leads to race conditions and corrupted variable state. - Package mixing: Importing
javax.el.ExpressionFactorywhile the classpath provides Juel 3.x produces linkage errors. - EL injection: Never concatenate untrusted input into an expression string. An attacker who controls part of the expression can traverse object properties or invoke unintended methods.
- Missing resolver registration: Application beans remain invisible until they are bound to the
ELContextor exposed by a customELResolver.
Practical Verification
- Confirm the Juel artifact version in your build file and match imports to
javax.elfor 2.2 orjakarta.elfor 3.x before compiling. - Write a unit test that parses
#{person.name}, binds a POJO to a newELContext, callsgetValue, and asserts the property result. - Inspect Juel release notes or source for
ExpressionFactorycaching guarantees andELResolverregistration steps before production use.
Because Juel and the EL specification have evolved across Jakarta EE transitions, treat version-specific package names and resolver behavior as items to verify against your current dependency tree. The caching and context-lifecycle rules above reflect well-established EL semantics, but the exact artifact coordinates and API package should be confirmed for your build.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.