Skip to content

Commit e088892

Browse files
committed
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
1 parent 152914a commit e088892

File tree

5 files changed

+124
-15
lines changed

5 files changed

+124
-15
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java

+20-12
Original file line numberDiff line numberDiff line change
@@ -384,29 +384,37 @@ public static boolean convertAllMethodHandleArguments(TypeConverter converter, O
384384
conversionOccurred |= (argument != arguments[i]);
385385
}
386386

387-
Class<?> varArgClass = methodHandleType.lastParameterType();
388-
ResolvableType varArgResolvableType = ResolvableType.forClass(varArgClass);
389-
TypeDescriptor targetType = new TypeDescriptor(varArgResolvableType, varArgClass.componentType(), null);
390-
TypeDescriptor componentTypeDesc = targetType.getElementTypeDescriptor();
391-
Assert.state(componentTypeDesc != null, "Component type must not be null for a varargs array");
387+
Class<?> varargsArrayClass = methodHandleType.lastParameterType();
388+
// We use the wrapper type for a primitive varargs array, since we eventually
389+
// need an Object array in order to invoke the MethodHandle in
390+
// FunctionReference#executeFunctionViaMethodHandle().
391+
Class<?> varargsComponentClass = ClassUtils.resolvePrimitiveIfNecessary(varargsArrayClass.componentType());
392+
TypeDescriptor varargsArrayType = TypeDescriptor.array(TypeDescriptor.valueOf(varargsComponentClass));
393+
Assert.state(varargsArrayType != null, "Array type must not be null for a varargs array");
394+
TypeDescriptor varargsComponentType = varargsArrayType.getElementTypeDescriptor();
395+
Assert.state(varargsComponentType != null, "Component type must not be null for a varargs array");
392396

393397
// If the target is varargs and there is just one more argument, then convert it here.
394398
if (varargsPosition == arguments.length - 1) {
395399
Object argument = arguments[varargsPosition];
396400
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
397401
if (argument == null) {
398402
// Perform the equivalent of GenericConversionService.convertNullSource() for a single argument.
399-
if (componentTypeDesc.getObjectType() == Optional.class) {
403+
if (varargsComponentType.getObjectType() == Optional.class) {
400404
arguments[varargsPosition] = Optional.empty();
401405
conversionOccurred = true;
402406
}
403407
}
404408
// If the argument type is assignable to the varargs component type, there is no need to
405-
// convert it or wrap it in an array. For example, using StringToArrayConverter to
406-
// convert a String containing a comma would result in the String being split and
407-
// repackaged in an array when it should be used as-is.
408-
else if (!sourceType.isAssignableTo(componentTypeDesc)) {
409-
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
409+
// convert it. For example, using StringToArrayConverter to convert a String containing a
410+
// comma would result in the String being split and repackaged in an array when it should
411+
// be used as-is. Similarly, if the argument is an array that is assignable to the varargs
412+
// array type, there is no need to convert it.
413+
else if (!sourceType.isAssignableTo(varargsComponentType) ||
414+
(sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType))) {
415+
416+
TypeDescriptor targetTypeToUse = (sourceType.isArray() ? varargsArrayType : varargsComponentType);
417+
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
410418
}
411419
// Possible outcomes of the above if-else block:
412420
// 1) the input argument was null, and nothing was done.
@@ -424,7 +432,7 @@ else if (!sourceType.isAssignableTo(componentTypeDesc)) {
424432
for (int i = varargsPosition; i < arguments.length; i++) {
425433
Object argument = arguments[i];
426434
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
427-
arguments[i] = converter.convertValue(argument, sourceType, componentTypeDesc);
435+
arguments[i] = converter.convertValue(argument, sourceType, varargsComponentType);
428436
conversionOccurred |= (argument != arguments[i]);
429437
}
430438
}

spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java

+23
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ void testVarargsInvocation01() {
245245
evaluate("aVarargsMethod(1,'a',3.0d)", "[1, a, 3.0]", String.class); // first and last need conversion
246246
evaluate("aVarargsMethod(new String[]{'a','b','c'})", "[a, b, c]", String.class);
247247
evaluate("aVarargsMethod(new String[]{})", "[]", String.class);
248+
evaluate("aVarargsMethod(new int[]{1, 2, 3})", "[1, 2, 3]", String.class); // needs int[] to String[] conversion
248249
evaluate("aVarargsMethod(null)", "[null]", String.class);
249250
evaluate("aVarargsMethod(null,'a')", "[null, a]", String.class);
250251
evaluate("aVarargsMethod('a',null,'b')", "[a, null, b]", String.class);
@@ -320,6 +321,7 @@ void testVarargsWithObjectArrayType() {
320321
// Conversion necessary
321322
evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
322323
evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
324+
evaluate("formatObjectVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class);
323325

324326
// Individual string contains a comma with multiple varargs arguments
325327
evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
@@ -333,6 +335,27 @@ void testVarargsWithObjectArrayType() {
333335
evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
334336
}
335337

338+
@Test
339+
void testVarargsWithPrimitiveArrayType() {
340+
// Calling 'public String formatPrimitiveVarargs(String format, int... nums)' -> effectively String.format(format, args)
341+
342+
// No var-args and no conversion necessary
343+
evaluate("formatPrimitiveVarargs(9)", "9", String.class);
344+
345+
// No var-args but conversion necessary
346+
evaluate("formatPrimitiveVarargs('7')", "7", String.class);
347+
348+
// No conversion necessary
349+
evaluate("formatPrimitiveVarargs('x -> %s', 9)", "x -> 9", String.class);
350+
evaluate("formatPrimitiveVarargs('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class);
351+
evaluate("formatPrimitiveVarargs('x -> %s', new int[]{1})", "x -> 1", String.class);
352+
evaluate("formatPrimitiveVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class);
353+
354+
// Conversion necessary
355+
evaluate("formatPrimitiveVarargs('x -> %s %s', '2', '3')", "x -> 2 3", String.class);
356+
evaluate("formatPrimitiveVarargs('x -> %s %s', '2', 3.0d)", "x -> 2 3", String.class);
357+
}
358+
336359
@Test
337360
void testVarargsOptionalInvocation() {
338361
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'

spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java

+19
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ private static void populateFunctions(StandardEvaluationContext testContext) {
6666
TestScenarioCreator.class.getDeclaredMethod("varargsFunction", String[].class));
6767
testContext.registerFunction("varargsFunction2",
6868
TestScenarioCreator.class.getDeclaredMethod("varargsFunction2", int.class, String[].class));
69+
testContext.registerFunction("varargsObjectFunction",
70+
TestScenarioCreator.class.getDeclaredMethod("varargsObjectFunction", Object[].class));
6971
}
7072
catch (Exception ex) {
7173
throw new IllegalStateException(ex);
@@ -106,6 +108,11 @@ private static void populateMethodHandles(StandardEvaluationContext testContext)
106108
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
107109
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
108110

111+
// #formatObjectVarargs(format, args...)
112+
MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
113+
"formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class));
114+
testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs);
115+
109116
// #add(int, int)
110117
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
111118
"add", MethodType.methodType(int.class, int.class, int.class));
@@ -160,6 +167,10 @@ public static String varargsFunction2(int i, String... strings) {
160167
return i + "-" + Arrays.toString(strings);
161168
}
162169

170+
public static String varargsObjectFunction(Object... args) {
171+
return Arrays.toString(args);
172+
}
173+
163174
public static String message(String template, String... args) {
164175
return template.formatted((Object[]) args);
165176
}
@@ -168,6 +179,14 @@ public static String formatObjectVarargs(String format, Object... args) {
168179
return String.format(format, args);
169180
}
170181

182+
public static String formatPrimitiveVarargs(String format, int... nums) {
183+
Object[] args = new Object[nums.length];
184+
for (int i = 0; i < nums.length; i++) {
185+
args[i] = nums[i];
186+
}
187+
return String.format(format, args);
188+
}
189+
171190
public static int add(int x, int y) {
172191
return x + y;
173192
}

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

+54-3
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ void functionWithVarargs() {
8080
evaluate("#varargsFunction(new String[0])", "[]", String.class);
8181
evaluate("#varargsFunction('a')", "[a]", String.class);
8282
evaluate("#varargsFunction('a','b','c')", "[a, b, c]", String.class);
83+
evaluate("#varargsFunction(new String[]{'a','b','c'})", "[a, b, c]", String.class);
8384
// Conversion from int to String
8485
evaluate("#varargsFunction(25)", "[25]", String.class);
8586
evaluate("#varargsFunction('b',25)", "[b, 25]", String.class);
87+
evaluate("#varargsFunction(new int[]{1, 2, 3})", "[1, 2, 3]", String.class);
8688
// Strings that contain a comma
8789
evaluate("#varargsFunction('a,b')", "[a,b]", String.class);
8890
evaluate("#varargsFunction('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
@@ -103,24 +105,47 @@ void functionWithVarargs() {
103105
// null values
104106
evaluate("#varargsFunction2(9,null)", "9-[null]", String.class);
105107
evaluate("#varargsFunction2(9,'a',null,'b')", "9-[a, null, b]", String.class);
108+
109+
evaluate("#varargsObjectFunction()", "[]", String.class);
110+
evaluate("#varargsObjectFunction(new String[0])", "[]", String.class);
111+
evaluate("#varargsObjectFunction('a')", "[a]", String.class);
112+
evaluate("#varargsObjectFunction('a','b','c')", "[a, b, c]", String.class);
113+
evaluate("#varargsObjectFunction(new String[]{'a','b','c'})", "[a, b, c]", String.class);
114+
// Conversion from int to String
115+
evaluate("#varargsObjectFunction(25)", "[25]", String.class);
116+
evaluate("#varargsObjectFunction('b',25)", "[b, 25]", String.class);
117+
// Strings that contain a comma
118+
evaluate("#varargsObjectFunction('a,b')", "[a,b]", String.class);
119+
evaluate("#varargsObjectFunction('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
120+
// null values
121+
evaluate("#varargsObjectFunction(null)", "[null]", String.class);
122+
evaluate("#varargsObjectFunction('a',null,'b')", "[a, null, b]", String.class);
106123
}
107124

108125
@Test // gh-33013
109126
void functionWithVarargsViaMethodHandle() {
110127
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
111128

112129
// No var-args and no conversion necessary
130+
evaluate("#message('x')", "x", String.class);
113131
evaluate("#formatObjectVarargs('x')", "x", String.class);
114132

115133
// No var-args but conversion necessary
134+
evaluate("#message(9)", "9", String.class);
116135
evaluate("#formatObjectVarargs(9)", "9", String.class);
117136

118137
// No conversion necessary
119138
evaluate("#add(3, 4)", 7, Integer.class);
139+
evaluate("#message('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
120140
evaluate("#formatObjectVarargs('x -> %s', '')", "x -> ", String.class);
121141
evaluate("#formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class);
122142
evaluate("#formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class);
123143
evaluate("#formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class);
144+
evaluate("#message('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class); // Object[] instanceof Object[]
145+
evaluate("#message('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class); // String[] instanceof Object[]
146+
evaluate("#message('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] instanceof Object[]
147+
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class); // Integer instanceof Object
148+
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0F)", "x -> a 3.0", String.class); // String/Float instanceof Object
124149
evaluate("#formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class);
125150
evaluate("#formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class);
126151
evaluate("#formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class);
@@ -131,9 +156,12 @@ void functionWithVarargsViaMethodHandle() {
131156
evaluate("#formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class);
132157

133158
// Conversion necessary
134-
evaluate("#add('2', 5.0)", 7, Integer.class);
135-
evaluate("#formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class);
136-
evaluate("#formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class);
159+
evaluate("#add('2', 5.0)", 7, Integer.class); // String/Double to Integer
160+
evaluate("#messageStatic('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class); // Integer to String
161+
evaluate("#messageStatic('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to String[]
162+
evaluate("#messageStatic('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to String[]
163+
evaluate("#messageStatic('x -> %s %s %s', new short[]{1, 2, 3})", "x -> 1 2 3", String.class); // short[] to String[]
164+
evaluate("#formatObjectVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to String[]
137165

138166
// Individual string contains a comma with multiple varargs arguments
139167
evaluate("#formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class);
@@ -147,6 +175,29 @@ void functionWithVarargsViaMethodHandle() {
147175
evaluate("#formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class);
148176
}
149177

178+
@Test
179+
void functionWithPrimitiveVarargsViaMethodHandle() {
180+
// Calling 'public String formatPrimitiveVarargs(String format, int... nums)' -> effectively String.format(format, args)
181+
182+
// No var-args and no conversion necessary
183+
evaluate("#formatPrimitiveVarargs(9)", "9", String.class);
184+
185+
// No var-args but conversion necessary
186+
evaluate("#formatPrimitiveVarargs('7')", "7", String.class);
187+
188+
// No conversion necessary
189+
evaluate("#formatPrimitiveVarargs('x -> %s', 9)", "x -> 9", String.class);
190+
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', 1, 2, 3)", "x -> 1 2 3", String.class);
191+
evaluate("#formatPrimitiveVarargs('x -> %s', new int[]{1})", "x -> 1", String.class);
192+
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class);
193+
194+
// Conversion necessary
195+
evaluate("#formatPrimitiveVarargs('x -> %s %s', '2', '3')", "x -> 2 3", String.class); // String to int
196+
evaluate("#formatPrimitiveVarargs('x -> %s %s', '2', 3.0F)", "x -> 2 3", String.class); // String/Float to int
197+
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new Integer[]{1, 2, 3})", "x -> 1 2 3", String.class); // Integer[] to int[]
198+
evaluate("#formatPrimitiveVarargs('x -> %s %s %s', new String[]{'1', '2', '3'})", "x -> 1 2 3", String.class); // String[] to int[]
199+
}
200+
150201
@Test
151202
void functionMethodMustBeStatic() throws Exception {
152203
SpelExpressionParser parser = new SpelExpressionParser();

spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java

+8
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ public String formatObjectVarargs(String format, Object... args) {
221221
return String.format(format, args);
222222
}
223223

224+
public String formatPrimitiveVarargs(String format, int... nums) {
225+
Object[] args = new Object[nums.length];
226+
for (int i = 0; i < nums.length; i++) {
227+
args[i] = nums[i];
228+
}
229+
return String.format(format, args);
230+
}
231+
224232

225233
public Inventor(String... strings) {
226234
if (strings.length > 0) {

0 commit comments

Comments
 (0)