Support MethodHandle invocation with primitive varargs array in SpEL

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

This commit addresses this by updating the
convertAllMethodHandleArguments(...) method in ReflectionHelper as
follows when the user supplies the varargs already packaged in a
primitive array.

- Regarding conversion, use the wrapper type for a primitive varargs
  array, since we eventually need an Object array in order to invoke
  the MethodHandle in FunctionReference#executeFunctionViaMethodHandle().

- When deciding whether to convert a single element passed as varargs,
  we now check if the argument is an array that is assignable to the
  varargs array type.

- When converting an array supplied as the varargs, we now convert that
  array to the varargs array type instead of the varargs component type.

Note, however, that a SpEL expression cannot provide a primitive array
for an Object[] varargs target. This is due to the fact that the
ArrayToArrayConverter used by Spring's ConversionService does not
support conversion from a primitive array to Object[] -- for example,
from int[] to Object[].

See gh-33191
Closes gh-33198
This commit is contained in:
Sam Brannen 2024-07-12 16:25:43 +02:00
parent 152914a752
commit e088892fc1
5 changed files with 124 additions and 15 deletions

View File

@ -384,11 +384,15 @@ public abstract class ReflectionHelper {
conversionOccurred |= (argument != arguments[i]);
}
Class<?> varArgClass = methodHandleType.lastParameterType();
ResolvableType varArgResolvableType = ResolvableType.forClass(varArgClass);
TypeDescriptor targetType = new TypeDescriptor(varArgResolvableType, varArgClass.componentType(), null);
TypeDescriptor componentTypeDesc = targetType.getElementTypeDescriptor();
Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
Class<?> varargsArrayClass = methodHandleType.lastParameterType();
// We use the wrapper type for a primitive varargs array, since we eventually
// need an Object array in order to invoke the MethodHandle in
// FunctionReference#executeFunctionViaMethodHandle().
Class<?> varargsComponentClass = ClassUtils.resolvePrimitiveIfNecessary(varargsArrayClass.componentType());
TypeDescriptor varargsArrayType = TypeDescriptor.array(TypeDescriptor.valueOf(varargsComponentClass));
Assert.state(varargsArrayType != null, "Array type must not be null for a varargs array");
TypeDescriptor varargsComponentType = varargsArrayType.getElementTypeDescriptor();
Assert.state(varargsComponentType != null, "Component type must not be null for a varargs array");
// If the target is varargs and there is just one more argument, then convert it here.
if (varargsPosition == arguments.length - 1) {
@ -396,17 +400,21 @@ public abstract class ReflectionHelper {
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
if (argument == null) {
// Perform the equivalent of GenericConversionService.convertNullSource() for a single argument.
if (componentTypeDesc.getObjectType() == Optional.class) {
if (varargsComponentType.getObjectType() == Optional.class) {
arguments[varargsPosition] = Optional.empty();
conversionOccurred = true;
}
}
// If the argument type is assignable to the varargs component type, there is no need to
// convert it or wrap it in an array. For example, using StringToArrayConverter to
// convert a String containing a comma would result in the String being split and
// repackaged in an array when it should be used as-is.
else if (!sourceType.isAssignableTo(componentTypeDesc)) {
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
// convert it. For example, using StringToArrayConverter to convert a String containing a
// comma would result in the String being split and repackaged in an array when it should
// be used as-is. Similarly, if the argument is an array that is assignable to the varargs
// array type, there is no need to convert it.
else if (!sourceType.isAssignableTo(varargsComponentType) ||
(sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType))) {
TypeDescriptor targetTypeToUse = (sourceType.isArray() ? varargsArrayType : varargsComponentType);
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
}
// Possible outcomes of the above if-else block:
// 1) the input argument was null, and nothing was done.
@ -424,7 +432,7 @@ public abstract class ReflectionHelper {
for (int i = varargsPosition; i < arguments.length; i++) {
Object argument = arguments[i];
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
arguments[i] = converter.convertValue(argument, sourceType, componentTypeDesc);
arguments[i] = converter.convertValue(argument, sourceType, varargsComponentType);
conversionOccurred |= (argument != arguments[i]);
}
}

View File

@ -245,6 +245,7 @@ class MethodInvocationTests extends AbstractExpressionTests {
evaluate("aVarargsMethod(1,'a',3.0d)", "[1, a, 3.0]", String.class); // first and last need conversion
evaluate("aVarargsMethod(new String[]{'a','b','c'})", "[a, b, c]", String.class);
evaluate("aVarargsMethod(new String[]{})", "[]", String.class);
evaluate("aVarargsMethod(new int[]{1, 2, 3})", "[1, 2, 3]", String.class); // needs int[] to String[] conversion
evaluate("aVarargsMethod(null)", "[null]", String.class);
evaluate("aVarargsMethod(null,'a')", "[null, a]", String.class);
evaluate("aVarargsMethod('a',null,'b')", "[a, null, b]", String.class);
@ -320,6 +321,7 @@ class MethodInvocationTests extends AbstractExpressionTests {
// Conversion necessary
evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
evaluate("formatObjectVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class);
// Individual string contains a comma with multiple varargs arguments
evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
@ -333,6 +335,27 @@ class MethodInvocationTests extends AbstractExpressionTests {
evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
}
@Test
void testVarargsWithPrimitiveArrayType() {
// Calling 'public String formatPrimitiveVarargs(String format, int... nums)' -> effectively String.format(format, args)
// No var-args and no conversion necessary
evaluate("formatPrimitiveVarargs(9)", "9", String.class);
// No var-args but conversion necessary
evaluate("formatPrimitiveVarargs('7')", "7", String.class);
// No conversion necessary
evaluate("formatPrimitiveVarargs('x -> %s', 9)", "x -> 9", String.class);
evaluate("formatPrimitiveVarargs('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class);
evaluate("formatPrimitiveVarargs('x -> %s', new int[]{1})", "x -> 1", String.class);
evaluate("formatPrimitiveVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class);
// Conversion necessary
evaluate("formatPrimitiveVarargs('x -> %s %s', '2', '3')", "x -> 2 3", String.class);
evaluate("formatPrimitiveVarargs('x -> %s %s', '2', 3.0d)", "x -> 2 3", String.class);
}
@Test
void testVarargsOptionalInvocation() {
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'

View File

@ -66,6 +66,8 @@ class TestScenarioCreator {
TestScenarioCreator.class.getDeclaredMethod("varargsFunction", String[].class));
testContext.registerFunction("varargsFunction2",
TestScenarioCreator.class.getDeclaredMethod("varargsFunction2", int.class, String[].class));
testContext.registerFunction("varargsObjectFunction",
TestScenarioCreator.class.getDeclaredMethod("varargsObjectFunction", Object[].class));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
@ -106,6 +108,11 @@ class TestScenarioCreator {
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
// #formatObjectVarargs(format, args...)
MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class));
testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs);
// #add(int, int)
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"add", MethodType.methodType(int.class, int.class, int.class));
@ -160,6 +167,10 @@ class TestScenarioCreator {
return i + "-" + Arrays.toString(strings);
}
public static String varargsObjectFunction(Object... args) {
return Arrays.toString(args);
}
public static String message(String template, String... args) {
return template.formatted((Object[]) args);
}
@ -168,6 +179,14 @@ class TestScenarioCreator {
return String.format(format, args);
}
public static String formatPrimitiveVarargs(String format, int... nums) {
Object[] args = new Object[nums.length];
for (int i = 0; i < nums.length; i++) {
args[i] = nums[i];
}
return String.format(format, args);
}
public static int add(int x, int y) {
return x + y;
}

View File

@ -80,9 +80,11 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#varargsFunction(new String[0])", "[]", String.class);
evaluate("#varargsFunction('a')", "[a]", String.class);
evaluate("#varargsFunction('a','b','c')", "[a, b, c]", String.class);
evaluate("#varargsFunction(new String[]{'a','b','c'})", "[a, b, c]", String.class);
// Conversion from int to String
evaluate("#varargsFunction(25)", "[25]", String.class);
evaluate("#varargsFunction('b',25)", "[b, 25]", String.class);
evaluate("#varargsFunction(new int[]{1, 2, 3})", "[1, 2, 3]", String.class);
// Strings that contain a comma
evaluate("#varargsFunction('a,b')", "[a,b]", String.class);
evaluate("#varargsFunction('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
@ -103,6 +105,21 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
// null values
evaluate("#varargsFunction2(9,null)", "9-[null]", String.class);
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
evaluate("#varargsObjectFunction()", "[]", String.class);
evaluate("#varargsObjectFunction(new String[0])", "[]", String.class);
evaluate("#varargsObjectFunction('a')", "[a]", String.class);
evaluate("#varargsObjectFunction('a','b','c')", "[a, b, c]", String.class);
evaluate("#varargsObjectFunction(new String[]{'a','b','c'})", "[a, b, c]", String.class);
// Conversion from int to String
evaluate("#varargsObjectFunction(25)", "[25]", String.class);
evaluate("#varargsObjectFunction('b',25)", "[b, 25]", String.class);
// Strings that contain a comma
evaluate("#varargsObjectFunction('a,b')", "[a,b]", String.class);
evaluate("#varargsObjectFunction('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
// null values
evaluate("#varargsObjectFunction(null)", "[null]", String.class);
evaluate("#varargsObjectFunction('a',null,'b')", "[a, null, b]", String.class);
}
@Test // gh-33013
@ -110,17 +127,25 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
// No var-args and no conversion necessary
evaluate("#message('x')", "x", String.class);
evaluate("#formatObjectVarargs('x')", "x", String.class);
// No var-args but conversion necessary
evaluate("#message(9)", "9", String.class);
evaluate("#formatObjectVarargs(9)", "9", String.class);
// No conversion necessary
evaluate("#add(3, 4)", 7, Integer.class);
evaluate("#message('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
evaluate("#formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
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("#message('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class); // Object[] instanceof Object[]
evaluate("#message('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class); // String[] instanceof Object[]
evaluate("#message('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] instanceof Object[]
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class); // Integer instanceof Object
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0F)", "x -> a 3.0", String.class); // String/Float instanceof Object
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);
@ -131,9 +156,12 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
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);
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
evaluate("#add('2', 5.0)", 7, Integer.class); // String/Double to Integer
evaluate("#messageStatic('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class); // Integer to String
evaluate("#messageStatic('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to String[]
evaluate("#messageStatic('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to String[]
evaluate("#messageStatic('x -> %s %s %s', new short[]{1, 2, 3})", "x -> 1 2 3", String.class); // short[] to String[]
evaluate("#formatObjectVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to String[]
// Individual string contains a comma with multiple varargs arguments
evaluate("#formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
@ -147,6 +175,29 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
}
@Test
void functionWithPrimitiveVarargsViaMethodHandle() {
// Calling 'public String formatPrimitiveVarargs(String format, int... nums)' -> effectively String.format(format, args)
// No var-args and no conversion necessary
evaluate("#formatPrimitiveVarargs(9)", "9", String.class);
// No var-args but conversion necessary
evaluate("#formatPrimitiveVarargs('7')", "7", String.class);
// No conversion necessary
evaluate("#formatPrimitiveVarargs('x -> %s', 9)", "x -> 9", String.class);
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class);
evaluate("#formatPrimitiveVarargs('x -> %s', new int[]{1})", "x -> 1", String.class);
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class);
// Conversion necessary
evaluate("#formatPrimitiveVarargs('x -> %s %s', '2', '3')", "x -> 2 3", String.class); // String to int
evaluate("#formatPrimitiveVarargs('x -> %s %s', '2', 3.0F)", "x -> 2 3", String.class); // String/Float to int
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to int[]
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new String[]{'1', '2', '3'})", "x -> 1 2 3", String.class); // String[] to int[]
}
@Test
void functionMethodMustBeStatic() throws Exception {
SpelExpressionParser parser = new SpelExpressionParser();

View File

@ -221,6 +221,14 @@ public class Inventor {
return String.format(format, args);
}
public String formatPrimitiveVarargs(String format, int... nums) {
Object[] args = new Object[nums.length];
for (int i = 0; i < nums.length; i++) {
args[i] = nums[i];
}
return String.format(format, args);
}
public Inventor(String... strings) {
if (strings.length > 0) {