Skip to content

Commit 807e1e6

Browse files
committed
Document support for varargs invocations in SpEL
Closes gh-33332
1 parent 5ac56bd commit 807e1e6

File tree

6 files changed

+231
-10
lines changed

6 files changed

+231
-10
lines changed

Diff for: framework-docs/modules/ROOT/nav.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
**** xref:core/expressions/language-ref/constructors.adoc[]
6161
**** xref:core/expressions/language-ref/variables.adoc[]
6262
**** xref:core/expressions/language-ref/functions.adoc[]
63+
**** xref:core/expressions/language-ref/varargs.adoc[]
6364
**** xref:core/expressions/language-ref/bean-references.adoc[]
6465
**** xref:core/expressions/language-ref/operator-ternary.adoc[]
6566
**** xref:core/expressions/language-ref/operator-elvis.adoc[]

Diff for: framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
You can invoke constructors by using the `new` operator. You should use the fully
55
qualified class name for all types except those located in the `java.lang` package
6-
(`Integer`, `Float`, `String`, and so on). The following example shows how to use the
7-
`new` operator to invoke constructors:
6+
(`Integer`, `Float`, `String`, and so on).
7+
xref:core/expressions/language-ref/varargs.adoc[Varargs] are also supported.
8+
9+
The following example shows how to use the `new` operator to invoke constructors.
810

911
[tabs]
1012
======
@@ -38,4 +40,3 @@ Kotlin::
3840
======
3941

4042

41-

Diff for: framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
= Functions
33

44
You can extend SpEL by registering user-defined functions that can be called within
5-
expressions by using the `#functionName(...)` syntax. Functions can be registered as
6-
variables in `EvaluationContext` implementations via the `setVariable()` method.
5+
expressions by using the `#functionName(...)` syntax, and like with standard method
6+
invocations, xref:core/expressions/language-ref/varargs.adoc[varargs] are also supported
7+
for function invocations.
8+
9+
Functions can be registered as _variables_ in `EvaluationContext` implementations via the
10+
`setVariable()` method.
711

812
[TIP]
913
====
@@ -111,7 +115,8 @@ been fully bound prior to registration; however, partially bound handles are als
111115
supported.
112116

113117
Consider the `String#formatted(Object...)` instance method, which produces a message
114-
according to a template and a variable number of arguments.
118+
according to a template and a variable number of arguments
119+
(xref:core/expressions/language-ref/varargs.adoc[varargs]).
115120

116121
You can register and use the `formatted` method as a `MethodHandle`, as the following
117122
example shows:
@@ -203,4 +208,3 @@ Kotlin::
203208
======
204209

205210

206-

Diff for: framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[[expressions-methods]]
22
= Methods
33

4-
You can invoke methods by using typical Java programming syntax. You can also invoke methods
5-
on literals. Variable arguments are also supported. The following examples show how to
6-
invoke methods:
4+
You can invoke methods by using the typical Java programming syntax. You can also invoke
5+
methods directly on literals such as strings or numbers.
6+
xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well.
7+
8+
The following examples show how to invoke methods.
79

810
[tabs]
911
======
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
[[expressions-varargs]]
2+
= Varargs Invocations
3+
4+
The Spring Expression Language supports
5+
https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html[varargs]
6+
invocations for xref:core/expressions/language-ref/constructors.adoc[constructors],
7+
xref:core/expressions/language-ref/methods.adoc[methods], and user-defined
8+
xref:core/expressions/language-ref/functions.adoc[functions].
9+
10+
The following example shows how to invoke the `java.lang.String#formatted(Object...)`
11+
_varargs_ method within an expression by supplying the variable argument list as separate
12+
arguments (`'blue', 1`).
13+
14+
[tabs]
15+
======
16+
Java::
17+
+
18+
[source,java,indent=0,subs="verbatim,quotes"]
19+
----
20+
// evaluates to "blue is color #1"
21+
String expression = "'%s is color #%d'.formatted('blue', 1)";
22+
String message = parser.parseExpression(expression).getValue(String.class);
23+
----
24+
25+
Kotlin::
26+
+
27+
[source,kotlin,indent=0,subs="verbatim,quotes"]
28+
----
29+
// evaluates to "blue is color #1"
30+
val expression = "'%s is color #%d'.formatted('blue', 1)"
31+
val message = parser.parseExpression(expression).getValue(String::class.java)
32+
----
33+
======
34+
35+
A variable argument list can also be supplied as an array, as demonstrated in the
36+
following example (`new Object[] {'blue', 1}`).
37+
38+
[tabs]
39+
======
40+
Java::
41+
+
42+
[source,java,indent=0,subs="verbatim,quotes"]
43+
----
44+
// evaluates to "blue is color #1"
45+
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
46+
String message = parser.parseExpression(expression).getValue(String.class);
47+
----
48+
49+
Kotlin::
50+
+
51+
[source,kotlin,indent=0,subs="verbatim,quotes"]
52+
----
53+
// evaluates to "blue is color #1"
54+
val expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})"
55+
val message = parser.parseExpression(expression).getValue(String::class.java)
56+
----
57+
======
58+
59+
As an alternative, a variable argument list can be supplied as a `java.util.List` – for
60+
example, as an xref:core/expressions/language-ref/inline-lists.adoc[inline list]
61+
(`{'blue', 1}`). The following example shows how to do that.
62+
63+
[tabs]
64+
======
65+
Java::
66+
+
67+
[source,java,indent=0,subs="verbatim,quotes"]
68+
----
69+
// evaluates to "blue is color #1"
70+
String expression = "'%s is color #%d'.formatted({'blue', 1})";
71+
String message = parser.parseExpression(expression).getValue(String.class);
72+
----
73+
74+
Kotlin::
75+
+
76+
[source,kotlin,indent=0,subs="verbatim,quotes"]
77+
----
78+
// evaluates to "blue is color #1"
79+
val expression = "'%s is color #%d'.formatted({'blue', 1})"
80+
val message = parser.parseExpression(expression).getValue(String::class.java)
81+
----
82+
======
83+
84+
[[expressions-varargs-type-conversion]]
85+
== Varargs Type Conversion
86+
87+
In contrast to the standard support for varargs invocations in Java,
88+
xref:core/expressions/evaluation.adoc#expressions-type-conversion[type conversion] may be
89+
applied to the individual arguments when invoking varargs constructors, methods, or
90+
functions in SpEL.
91+
92+
For example, if we have registered a custom
93+
xref:core/expressions/language-ref/functions.adoc[function] in the `EvaluationContext`
94+
under the name `#reverseStrings` for a method with the signature
95+
`String reverseStrings(String... strings)`, we can invoke that function within a SpEL
96+
expression with any argument that can be converted to a `String`, as demonstrated in the
97+
following example.
98+
99+
[tabs]
100+
======
101+
Java::
102+
+
103+
[source,java,indent=0,subs="verbatim,quotes"]
104+
----
105+
// evaluates to "3.0, 2.0, 1, SpEL"
106+
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
107+
String message = parser.parseExpression(expression)
108+
.getValue(evaluationContext, String.class);
109+
----
110+
111+
Kotlin::
112+
+
113+
[source,kotlin,indent=0,subs="verbatim,quotes"]
114+
----
115+
// evaluates to "3.0, 2.0, 1, SpEL"
116+
val expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)"
117+
val message = parser.parseExpression(expression)
118+
.getValue(evaluationContext, String::class.java)
119+
----
120+
======
121+
122+
Similarly, any array whose component type is a subtype of the required varargs type can
123+
be supplied as the variable argument list for a varargs invocation. For example, a
124+
`String[]` array can be supplied to a varargs invocation that accepts an `Object...`
125+
argument list.
126+
127+
The following listing demonstrates that we can supply a `String[]` array to the
128+
`java.lang.String#formatted(Object...)` _varargs_ method. It also highlights that `1`
129+
will be automatically converted to `"1"`.
130+
131+
[tabs]
132+
======
133+
Java::
134+
+
135+
[source,java,indent=0,subs="verbatim,quotes"]
136+
----
137+
// evaluates to "blue is color #1"
138+
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
139+
String message = parser.parseExpression(expression).getValue(String.class);
140+
----
141+
142+
Kotlin::
143+
+
144+
[source,kotlin,indent=0,subs="verbatim,quotes"]
145+
----
146+
// evaluates to "blue is color #1"
147+
val expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})"
148+
val message = parser.parseExpression(expression).getValue(String::class.java)
149+
----
150+
======
151+

Diff for: spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java

+62
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121
import java.lang.invoke.MethodType;
2222
import java.lang.reflect.Method;
2323
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.Collections;
2426
import java.util.Date;
2527
import java.util.GregorianCalendar;
2628
import java.util.List;
2729
import java.util.Map;
30+
import java.util.stream.Collectors;
2831

2932
import example.Color;
3033
import example.FruitMap;
@@ -667,6 +670,59 @@ void registerFunctionViaMethodHandleFullyBound() throws Exception {
667670
}
668671
}
669672

673+
@Nested
674+
class Varargs {
675+
676+
@Test
677+
void varargsMethodInvocationWithIndividualArguments() {
678+
// evaluates to "blue is color #1"
679+
String expression = "'%s is color #%d'.formatted('blue', 1)";
680+
String message = parser.parseExpression(expression)
681+
.getValue(String.class);
682+
assertThat(message).isEqualTo("blue is color #1");
683+
}
684+
685+
@Test
686+
void varargsMethodInvocationWithArgumentsAsObjectArray() {
687+
// evaluates to "blue is color #1"
688+
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
689+
String message = parser.parseExpression(expression)
690+
.getValue(String.class);
691+
assertThat(message).isEqualTo("blue is color #1");
692+
}
693+
694+
@Test
695+
void varargsMethodInvocationWithArgumentsAsInlineList() {
696+
// evaluates to "blue is color #1"
697+
String expression = "'%s is color #%d'.formatted({'blue', 1})";
698+
String message = parser.parseExpression(expression).getValue(String.class);
699+
assertThat(message).isEqualTo("blue is color #1");
700+
}
701+
702+
@Test
703+
void varargsMethodInvocationWithTypeConversion() {
704+
Method reverseStringsMethod = ReflectionUtils.findMethod(StringUtils.class, "reverseStrings", String[].class);
705+
SimpleEvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
706+
evaluationContext.setVariable("reverseStrings", reverseStringsMethod);
707+
708+
// String reverseStrings(String... strings)
709+
// evaluates to "3.0, 2.0, 1.0, SpEL"
710+
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
711+
String message = parser.parseExpression(expression)
712+
.getValue(evaluationContext, String.class);
713+
assertThat(message).isEqualTo("3.0, 2.0, 1, SpEL");
714+
}
715+
716+
@Test
717+
void varargsMethodInvocationWithArgumentsAsStringArray() {
718+
// evaluates to "blue is color #1"
719+
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
720+
String message = parser.parseExpression(expression).getValue(String.class);
721+
assertThat(message).isEqualTo("blue is color #1");
722+
}
723+
724+
}
725+
670726
@Nested
671727
class TernaryOperator {
672728

@@ -906,6 +962,12 @@ static class StringUtils {
906962
public static String reverseString(String input) {
907963
return new StringBuilder(input).reverse().toString();
908964
}
965+
966+
public static String reverseStrings(String... strings) {
967+
List<String> list = Arrays.asList(strings);
968+
Collections.reverse(list);
969+
return list.stream().collect(Collectors.joining(", "));
970+
}
909971
}
910972

911973
private static class ListConcatenation implements OperatorOverloader {

0 commit comments

Comments
 (0)