Skip to content

Commit

Permalink
Support MethodHandle function invocation with varargs array in SpEL
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Jul 10, 2024
1 parent dc16f3c commit 83ca2c0
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,29 @@ else if (spelParamCount != declaredParamCount) {
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
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
methodHandle.type().parameterArray(), functionArgs);
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -106,22 +105,6 @@ void functionWithVarargs() {
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)
Expand All @@ -138,6 +121,14 @@ void functionWithVarargsViaMethodHandle() {
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);
Expand Down

0 comments on commit 83ca2c0

Please sign in to comment.