https://bz.apache.org/bugzilla/show_bug.cgi?id=70297
Bug ID: 70297
Summary: EL function call evaluates the argument that alone
fills the varargs parameter twice
Product: Tomcat 11
Version: 11.0.26
Hardware: PC
OS: Linux
Status: NEW
Severity: normal
Priority: P2
Component: EL
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: -------
When an EL function whose Java method is varargs is called with exactly one
argument for the varargs parameter,
AstFunction.getValue evaluates that argument expression twice: once for the
isArray(...) check and once more for
the value (java/org/apache/el/parser/AstFunction.java, the inputParameterCount
== methodParameterCount &&
isArray(parameters.jjtGetChild(i).getValue(ctx)) branch, and again in the else
branch when the value is not an
array). The check was added by the fix for bug 66419 (commit 8c6f2972be).
Consequences:
- An argument with side effects runs twice, and the function receives the
result of the second evaluation, so the
result can differ from a single evaluation (reproducer below: 2 instead of
1).
- An expensive argument (a function that queries a database, for example) costs
double on every evaluation.
- With two or more varargs values each one is evaluated once, so the cost
depends on the argument count.
The caller cannot avoid it: a function mapper maps one Method per name, and the
single-array case
(fn:format('%s-%s', 'one,two'.split(','))) has to keep working.
Reproduced with 9.0.123 and 11.0.26. The code is unchanged on the heads of
main, 11.0.x, 10.1.x and 9.0.x
(2026-10-29, after the 2026-10-18 varargs changes f3bdd779b3 and 1d672d78c8).
The same problem was fixed for method
references on 2026-10-18 (246b894299, "Evaluate each expression once for EL
method references"), but not for functions.
Reproducer
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.el.ELContext;
import jakarta.el.ExpressionFactory;
import jakarta.el.StandardELContext;
public class VarargsDoubleEvaluation {
private static final AtomicInteger CALLS = new AtomicInteger();
public static int tick() {
return CALLS.incrementAndGet();
}
public static void main(String[] args) throws Exception {
ExpressionFactory factory = ExpressionFactory.newInstance();
ELContext context = new StandardELContext(factory);
context.getFunctionMapper().mapFunction("fn", "format",
String.class.getMethod("format", String.class,
Object[].class));
context.getFunctionMapper().mapFunction("fn", "tick",
VarargsDoubleEvaluation.class.getMethod("tick"));
Object value = factory.createValueExpression(context,
"${fn:format('%s', fn:tick())}", String.class)
.getValue(context);
System.out.println(value + ", tick() calls: " + CALLS.get());
}
}
Expected 1, tick() calls: 1. Actual: 2, tick() calls: 2. With two values
(${fn:format('%s %s', fn:tick(), fn:tick())}) it is 1 2 and 2 calls, as
expected. (On 9.0.x, use javax.el.)
Proposed fix
Evaluate the argument once and branch on the value:
--- a/java/org/apache/el/parser/AstFunction.java
+++ b/java/org/apache/el/parser/AstFunction.java
@@ -170,9 +170,16 @@
// Java semantics: an empty array is passed when
no
// varargs elements are supplied
params[i] = new Object[0];
- } else if (inputParameterCount == methodParameterCount
&&
-
isArray(parameters.jjtGetChild(i).getValue(ctx))) {
- params[i] =
parameters.jjtGetChild(i).getValue(ctx);
+ } else if (inputParameterCount ==
methodParameterCount) {
+ // Evaluate the single argument once: it is either
+ // the varargs array itself or its only element
+ Object value =
parameters.jjtGetChild(i).getValue(ctx);
+ if (isArray(value)) {
+ params[i] = value;
+ } else {
+ params[i] = new Object[] {
ELSupport.coerceToType(ctx, value,
+ paramTypes[i].getComponentType()) };
+ }
} else {
Object[] varargs = new Object[inputParameterCount
- methodParameterCount + 1];
Class<?> target =
paramTypes[i].getComponentType();
The patch applies to main and 11.0.x as is, and to 10.1.x and 9.0.x with an
offset of 5-6 lines. With the patch on the
11.0.x head AstFunction (run on 11.0.26), the reproducer gives 1 and 1 call.
The two-value case and both array
cases (fn:format('%s-%s', 'one,two'.split(',')), a single-parameter varargs
function with 'a,b'.split(',')) give
the same results as before.
A test for test/org/apache/el/parser/TestAstFunction.java, next to
testVarargMethod (needs
import java.util.concurrent.atomic.AtomicInteger;):
private static final AtomicInteger NEXT = new AtomicInteger();
public static int next() {
return NEXT.incrementAndGet();
}
@Test
public void testVarargMethodSingleArgumentEvaluatedOnce() throws
NoSuchMethodException {
// The argument that alone fills the varargs parameter must be
evaluated
// once, like any other argument
ExpressionFactory factory = ExpressionFactory.newInstance();
ELContext context = new StandardELContext(factory);
context.getFunctionMapper().mapFunction("fn", "format",
String.class.getMethod("format", String.class,
Object[].class));
context.getFunctionMapper().mapFunction("fn", "next",
TestAstFunction.class.getMethod("next"));
NEXT.set(0);
Object result = factory.createValueExpression(context,
"${fn:format('%s', fn:next())}", String.class)
.getValue(context);
Assert.assertEquals("1", result);
Assert.assertEquals(1, NEXT.get());
}
--
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]