Browse Source

Support MethodHandle function invocation with varargs array in SpEL

Prior to this commit, the Spring Expression Language (SpEL) could not
invoke a varargs MethodHandle function with an array containing the
variable arguments, although that is supported for a varargs Method
function. Attempting to do so resulted in the array being supplied as a
single argument to the MethodHandle.

This commit addresses this by updating the
executeFunctionViaMethodHandle(...) method in FunctionReference as
follows when the user supplies the varargs already packaged in an array.

- Creates a new array large enough to hold the non-varargs arguments
  and the unpackaged varargs arguments.

- Adds the non-varargs arguments to the beginning of that array and
  adds the unpackaged varargs arguments to the end of that array.

- Invokes the MethodHandle with the new arguments array.

Closes gh-33191
pull/33350/head
Sam Brannen 1 year ago
parent
commit
83ca2c0cff
  1. 23
      spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java
  2. 25
      spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java

23
spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java

@ -225,11 +225,30 @@ public class FunctionReference extends SpelNodeImpl { @@ -225,11 +225,30 @@ public class FunctionReference extends SpelNodeImpl {
TypeConverter converter = state.getEvaluationContext().getTypeConverter();
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
if (isSuspectedVarargs && declaredParamCount == 1) {
// we only repack the varargs if it is the ONLY argument
if (isSuspectedVarargs) {
if (declaredParamCount == 1) {
// We only repackage the varargs if it is the ONLY argument -- for example,
// when we are dealing with a bound MethodHandle.
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
methodHandle.type().parameterArray(), functionArgs);
}
else if (spelParamCount == declaredParamCount) {
// If the varargs were supplied already packaged in an array, we have to create
// a new array, add the non-varargs arguments to the beginning of that array,
// and add the unpackaged varargs arguments to the end of that array. The reason
// is that MethodHandle.invokeWithArguments(Object...) does not expect varargs
// to be packaged in an array, in contrast to how method invocation works with
// reflection.
int actualVarargsIndex = functionArgs.length - 1;
if (actualVarargsIndex >= 0 && functionArgs[actualVarargsIndex].getClass().isArray()) {
Object[] argsToUnpack = (Object[]) functionArgs[actualVarargsIndex];
Object[] newArgs = new Object[actualVarargsIndex + argsToUnpack.length];
System.arraycopy(functionArgs, 0, newArgs, 0, actualVarargsIndex);
System.arraycopy(argsToUnpack, 0, newArgs, actualVarargsIndex, argsToUnpack.length);
functionArgs = newArgs;
}
}
}
try {
return new TypedValue(methodHandle.invokeWithArguments(functionArgs));

25
spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java

@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
package org.springframework.expression.spel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -106,22 +105,6 @@ class VariableAndFunctionTests extends AbstractExpressionTests { @@ -106,22 +105,6 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
}
@Disabled("Disabled until bugs are reported and fixed")
@Test
void functionWithVarargsViaMethodHandle_CurrentlyFailing() {
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
// No conversion necessary
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
}
@Test // gh-33013
void functionWithVarargsViaMethodHandle() {
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
@ -138,6 +121,14 @@ class VariableAndFunctionTests extends AbstractExpressionTests { @@ -138,6 +121,14 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class);
evaluate("#formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class);
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
// Conversion necessary
evaluate("#add('2', 5.0)", 7, Integer.class);

Loading…
Cancel
Save