Releases: micronaut-projects/micronaut-jakarta-el
Release list
Micronaut Jakarta EL 1.0.0
The first release of Micronaut Jakarta EL, a new module: a complete implementation of the
Jakarta Expression Language 6.0
specification whose expressions and resolvers are generated at compilation time by an annotation processor,
rather than parsed and resolved reflectively at runtime.
Expressions are declared in source with @ELExpression and @ELMethodExpression, together with the variables,
imports and functions they work with. The processor parses each one at build time and generates a Java class for
it: a property access becomes a getter call, a function call becomes a static call, an operator becomes the Java
operator. Beans are declared with Micronaut's own @Introspected, so a type already introspected for another
reason works in an expression with no further annotation, and is read through the introspection's dispatch table.
What compilation buys
- No parsing at runtime. An application that declares its expressions in source ships no parser — it is a
build-time module the runtime never sees. Looking an expression up by its string through the standard
jakarta.el.ExpressionFactoryis a generatedswitch, not a parse. - No reflection. Declared variables compile to direct invocations, dynamic types resolve through the bean
introspections Micronaut already generates, and a lambda passed to a method becomes a Java lambda implementing
the parameter's functional interface — no argument maps and nojava.lang.reflect.Proxy. The reflective paths
that remain are the ones the specification itself defines in reflective terms, and they are documented. - Nothing to configure for GraalVM. No runtime reflection means no reflection configuration. The Java test
suite and the complete TCK both run compiled into a native image on every build. - Errors at compile time. A property or method a declared type does not have is a compilation error; so is an
argument that cannot be coerced to a function's parameter, and an expression whose type cannot be determined
because an identifier is undeclared. A syntax error reports the expression and the position on the class that
declares it. An omittedexpectedTypeis inferred from the static type of the expression.
Performance
EvaluationBenchmark evaluates the same expressions — from a property read to a stream pipeline with two lambdas —
against compiled Micronaut Jakarta EL, its runtime interpreter, Eclipse Expressly (the reference implementation)
and Apache Tomcat Jasper EL, each with the context it provides by default. The geometric mean of the average
evaluation times over all the benchmarks, on OpenJDK 25.0.2 on Apple Silicon:
| Implementation | Geometric mean | Relative |
|---|---|---|
| Micronaut compiled | 8.63 ns | 1.0x |
| Micronaut interpreted | 82.9 ns | 9.6x |
| Eclipse Expressly | 367 ns | 42.5x |
| Tomcat Jasper EL | 449 ns | 52.0x |
A compiled expression evaluated about 43 times faster than the reference implementation and 52 times faster than
Tomcat's. The gap is narrowest, around 6x, where every implementation has to resolve at runtime, and widest —
a hundredfold and more — wherever the others fall back to reflection: a method call on a List or a String, a
lambda coerced to a functional interface, a stream pipeline. The benchmarks module of the repository holds the
harness and every figure with its error.
A complete implementation of the specification
The whole language is implemented, not a subset: eval-expressions, literal-expressions and composite expressions,
the full operator set with its precedence, identifiers, functions, variables, lambda expressions, enums, arrays,
static field and method references, constructor references, the type conversion rules, and the construction of
sets, lists and maps with the 22 operations on collection objects of chapter 2.
The Jakarta Expression Language 6.0 Technology Compatibility Kit runs on every build: 360 tests, 0 failures, 0
skipped. The signature test is excluded, as it verifies the signatures of the jakarta.el API jar, which this
module consumes unchanged rather than implements. Code written against the standard API is unaffected: the module
registers its ExpressionFactory as a service and returns ordinary ValueExpression and MethodExpression
instances, evaluated against any ELContext.
The annotation processor runs for Java, Groovy and Kotlin and generates the same expressions for all three.
Expressions built at runtime
An expression whose string is only assembled at runtime has no generated implementation, and the module rejects it
rather than falling back silently. Adding the optional micronaut-jakarta-el-interpreter parses and evaluates
those expressions instead. It is not a second implementation of the language: it walks the same syntax tree the
compiler consumes and calls the same runtime the generated code calls, which is why it also stays well ahead of
the other implementations.
Getting started
annotationProcessor("io.micronaut.el:micronaut-jakarta-el-processor")
implementation("io.micronaut.el:micronaut-jakarta-el")Those two are all an application declaring its expressions in source needs. The published artifacts, all under the
io.micronaut.el group, are micronaut-jakarta-el (the runtime), micronaut-jakarta-el-processor (the compiler),
micronaut-jakarta-el-annotations, micronaut-jakarta-el-parser, the optional micronaut-jakarta-el-interpreter
and micronaut-jakarta-el-bom.
See the documentation for the guide and
worked examples in Java, Groovy and Kotlin.
Requirements
Java 25, Micronaut 5.1.12, Jakarta Expression Language API 6.0.1.
Experimental API
The public API, annotations included, is marked @Experimental and can still change between minor versions. This
release stabilises the implementation and its behaviour, not yet the shape of the API.