Skip to content

Commit fcc99a6

Browse files
committed
Support lists for varargs invocations in SpEL
The changes made in conjunction with gh-33013 resulted in a regression for varargs support in SpEL expressions. Specifically, before gh-33013 one could supply a list -- for example, an "inline list" -- as the varargs array when invoking a varargs constructor, method, or function within a SpEL expression. However, after gh-33013 an inline list (or collection in general) is no longer converted to an array for varargs invocations. Instead, the list is supplied as a single argument of the resulting varargs array which breaks applications that depend on the previous behavior. Although it was never intended that one could supply a collection as the set of varargs, we concede that this is a regression in existing behavior, and this commit therefore restores support for supplying a java.util.List as the varargs "array". In addition, this commit introduces the same "list to array" conversion support for MethodHandle-based functions that accept varargs. Note, however, that this commit does not restore support for converting arbitrary single objects to an array for varargs invocations if the single object is already an instance of the varargs array's component type. See gh-33013 Closes gh-33315
1 parent 8afff33 commit fcc99a6

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,14 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe
314314
// convert it or wrap it in an array. For example, using StringToArrayConverter to convert
315315
// a String containing a comma would result in the String being split and repackaged in an
316316
// array when it should be used as-is. Similarly, if the argument is an array that is
317-
// assignable to the varargs array type, there is no need to convert it.
317+
// assignable to the varargs array type, there is no need to convert it. However, if the
318+
// argument is a java.util.List, we let the TypeConverter convert the list to an array.
318319
else if (!sourceType.isAssignableTo(componentTypeDesc) ||
319-
(sourceType.isArray() && !sourceType.isAssignableTo(targetType))) {
320+
(sourceType.isArray() && !sourceType.isAssignableTo(targetType)) ||
321+
(argument instanceof List)) {
320322

321-
TypeDescriptor targetTypeToUse = (sourceType.isArray() ? targetType : componentTypeDesc);
323+
TypeDescriptor targetTypeToUse =
324+
(sourceType.isArray() || argument instanceof List ? targetType : componentTypeDesc);
322325
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
323326
}
324327
// Possible outcomes of the above if-else block:
@@ -413,11 +416,14 @@ public static boolean convertAllMethodHandleArguments(TypeConverter converter, O
413416
// convert it. For example, using StringToArrayConverter to convert a String containing a
414417
// comma would result in the String being split and repackaged in an array when it should
415418
// be used as-is. Similarly, if the argument is an array that is assignable to the varargs
416-
// array type, there is no need to convert it.
419+
// array type, there is no need to convert it. However, if the argument is a java.util.List,
420+
// we let the TypeConverter convert the list to an array.
417421
else if (!sourceType.isAssignableTo(varargsComponentType) ||
418-
(sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType))) {
422+
(sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType)) ||
423+
(argument instanceof List)) {
419424

420-
TypeDescriptor targetTypeToUse = (sourceType.isArray() ? varargsArrayType : varargsComponentType);
425+
TypeDescriptor targetTypeToUse =
426+
(sourceType.isArray() || argument instanceof List ? varargsArrayType : varargsComponentType);
421427
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
422428
}
423429
// Possible outcomes of the above if-else block:

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.expression.spel.standard.SpelExpression;
3333
import org.springframework.expression.spel.standard.SpelExpressionParser;
3434
import org.springframework.expression.spel.support.StandardEvaluationContext;
35+
import org.springframework.expression.spel.support.StandardTypeLocator;
3536
import org.springframework.expression.spel.testresources.Inventor;
3637
import org.springframework.expression.spel.testresources.PlaceOfBirth;
3738

@@ -364,6 +365,21 @@ void varargsWithPrimitiveArrayToObjectArrayConversion() {
364365
evaluate("formatObjectVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to Object[]
365366
}
366367

368+
@Test // gh-33315
369+
void varargsWithListConvertedToVarargsArray() {
370+
((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util");
371+
372+
// Calling 'public String aVarargsMethod(String... strings)' -> Arrays.toString(strings)
373+
String expected = "[a, b, c]";
374+
evaluate("aVarargsMethod(T(List).of('a', 'b', 'c'))", expected, String.class);
375+
evaluate("aVarargsMethod({'a', 'b', 'c'})", expected, String.class);
376+
377+
// Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
378+
expected = "x -> a b c";
379+
evaluate("formatObjectVarargs('x -> %s %s %s', T(List).of('a', 'b', 'c'))", expected, String.class);
380+
evaluate("formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class);
381+
}
382+
367383
@Test
368384
void varargsOptionalInvocation() {
369385
// Calling 'public String optionalVarargsMethod(Optional<String>... values)'

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.expression.spel.standard.SpelExpressionParser;
2222
import org.springframework.expression.spel.support.StandardEvaluationContext;
23+
import org.springframework.expression.spel.support.StandardTypeLocator;
2324

2425
import static org.assertj.core.api.Assertions.assertThat;
2526
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -211,6 +212,33 @@ void functionFromMethodHandleWithVarargsAndPrimitiveArrayToObjectArrayConversion
211212
evaluate("#formatObjectVarargs('x -> %s %s %s', new int[]{1, 2, 3})", "x -> 1 2 3", String.class); // int[] to Object[]
212213
}
213214

215+
@Test // gh-33315
216+
void functionFromMethodWithListConvertedToVarargsArray() {
217+
((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util");
218+
String expected = "[a, b, c]";
219+
220+
evaluate("#varargsFunction(T(List).of('a', 'b', 'c'))", expected, String.class);
221+
evaluate("#varargsFunction({'a', 'b', 'c'})", expected, String.class);
222+
223+
// Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
224+
evaluate("#varargsObjectFunction(T(List).of('a', 'b', 'c'))", expected, String.class);
225+
evaluate("#varargsObjectFunction({'a', 'b', 'c'})", expected, String.class);
226+
}
227+
228+
@Test // gh-33315
229+
void functionFromMethodHandleWithListConvertedToVarargsArray() {
230+
((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util");
231+
String expected = "x -> a b c";
232+
233+
// Calling 'public static String message(String template, String... args)' -> template.formatted((Object[]) args)
234+
evaluate("#message('x -> %s %s %s', T(List).of('a', 'b', 'c'))", expected, String.class);
235+
evaluate("#message('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class);
236+
237+
// Calling 'public static String formatObjectVarargs(String format, Object... args)' -> String.format(format, args)
238+
evaluate("#formatObjectVarargs('x -> %s %s %s', T(List).of('a', 'b', 'c'))", expected, String.class);
239+
evaluate("#formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class);
240+
}
241+
214242
@Test
215243
void functionMethodMustBeStatic() throws Exception {
216244
SpelExpressionParser parser = new SpelExpressionParser();

0 commit comments

Comments
 (0)