Document support for varargs invocations in SpEL

Closes gh-33332
This commit is contained in:
Sam Brannen 2024-11-15 17:38:08 +01:00
parent 5ac56bda87
commit 807e1e6126
6 changed files with 231 additions and 10 deletions

View File

@ -60,6 +60,7 @@
**** xref:core/expressions/language-ref/constructors.adoc[] **** xref:core/expressions/language-ref/constructors.adoc[]
**** xref:core/expressions/language-ref/variables.adoc[] **** xref:core/expressions/language-ref/variables.adoc[]
**** xref:core/expressions/language-ref/functions.adoc[] **** xref:core/expressions/language-ref/functions.adoc[]
**** xref:core/expressions/language-ref/varargs.adoc[]
**** xref:core/expressions/language-ref/bean-references.adoc[] **** xref:core/expressions/language-ref/bean-references.adoc[]
**** xref:core/expressions/language-ref/operator-ternary.adoc[] **** xref:core/expressions/language-ref/operator-ternary.adoc[]
**** xref:core/expressions/language-ref/operator-elvis.adoc[] **** xref:core/expressions/language-ref/operator-elvis.adoc[]

View File

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

View File

@ -2,8 +2,12 @@
= Functions = Functions
You can extend SpEL by registering user-defined functions that can be called within You can extend SpEL by registering user-defined functions that can be called within
expressions by using the `#functionName(...)` syntax. Functions can be registered as expressions by using the `#functionName(...)` syntax, and like with standard method
variables in `EvaluationContext` implementations via the `setVariable()` method. invocations, xref:core/expressions/language-ref/varargs.adoc[varargs] are also supported
for function invocations.
Functions can be registered as _variables_ in `EvaluationContext` implementations via the
`setVariable()` method.
[TIP] [TIP]
==== ====
@ -111,7 +115,8 @@ been fully bound prior to registration; however, partially bound handles are als
supported. supported.
Consider the `String#formatted(Object...)` instance method, which produces a message Consider the `String#formatted(Object...)` instance method, which produces a message
according to a template and a variable number of arguments. according to a template and a variable number of arguments
(xref:core/expressions/language-ref/varargs.adoc[varargs]).
You can register and use the `formatted` method as a `MethodHandle`, as the following You can register and use the `formatted` method as a `MethodHandle`, as the following
example shows: example shows:
@ -203,4 +208,3 @@ Kotlin::
====== ======

View File

@ -1,9 +1,11 @@
[[expressions-methods]] [[expressions-methods]]
= Methods = Methods
You can invoke methods by using typical Java programming syntax. You can also invoke methods You can invoke methods by using the typical Java programming syntax. You can also invoke
on literals. Variable arguments are also supported. The following examples show how to methods directly on literals such as strings or numbers.
invoke methods: xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well.
The following examples show how to invoke methods.
[tabs] [tabs]
====== ======

View File

@ -0,0 +1,151 @@
[[expressions-varargs]]
= Varargs Invocations
The Spring Expression Language supports
https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html[varargs]
invocations for xref:core/expressions/language-ref/constructors.adoc[constructors],
xref:core/expressions/language-ref/methods.adoc[methods], and user-defined
xref:core/expressions/language-ref/functions.adoc[functions].
The following example shows how to invoke the `java.lang.String#formatted(Object...)`
_varargs_ method within an expression by supplying the variable argument list as separate
arguments (`'blue', 1`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted('blue', 1)";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%d'.formatted('blue', 1)"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
A variable argument list can also be supplied as an array, as demonstrated in the
following example (`new Object[] {'blue', 1}`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
As an alternative, a variable argument list can be supplied as a `java.util.List` for
example, as an xref:core/expressions/language-ref/inline-lists.adoc[inline list]
(`{'blue', 1}`). The following example shows how to do that.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted({'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%d'.formatted({'blue', 1})"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
[[expressions-varargs-type-conversion]]
== Varargs Type Conversion
In contrast to the standard support for varargs invocations in Java,
xref:core/expressions/evaluation.adoc#expressions-type-conversion[type conversion] may be
applied to the individual arguments when invoking varargs constructors, methods, or
functions in SpEL.
For example, if we have registered a custom
xref:core/expressions/language-ref/functions.adoc[function] in the `EvaluationContext`
under the name `#reverseStrings` for a method with the signature
`String reverseStrings(String... strings)`, we can invoke that function within a SpEL
expression with any argument that can be converted to a `String`, as demonstrated in the
following example.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "3.0, 2.0, 1, SpEL"
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
String message = parser.parseExpression(expression)
.getValue(evaluationContext, String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "3.0, 2.0, 1, SpEL"
val expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)"
val message = parser.parseExpression(expression)
.getValue(evaluationContext, String::class.java)
----
======
Similarly, any array whose component type is a subtype of the required varargs type can
be supplied as the variable argument list for a varargs invocation. For example, a
`String[]` array can be supplied to a varargs invocation that accepts an `Object...`
argument list.
The following listing demonstrates that we can supply a `String[]` array to the
`java.lang.String#formatted(Object...)` _varargs_ method. It also highlights that `1`
will be automatically converted to `"1"`.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======

View File

@ -21,10 +21,13 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType; import java.lang.invoke.MethodType;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import example.Color; import example.Color;
import example.FruitMap; import example.FruitMap;
@ -667,6 +670,59 @@ class SpelDocumentationTests extends AbstractExpressionTests {
} }
} }
@Nested
class Varargs {
@Test
void varargsMethodInvocationWithIndividualArguments() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted('blue', 1)";
String message = parser.parseExpression(expression)
.getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
@Test
void varargsMethodInvocationWithArgumentsAsObjectArray() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
String message = parser.parseExpression(expression)
.getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
@Test
void varargsMethodInvocationWithArgumentsAsInlineList() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted({'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
@Test
void varargsMethodInvocationWithTypeConversion() {
Method reverseStringsMethod = ReflectionUtils.findMethod(StringUtils.class, "reverseStrings", String[].class);
SimpleEvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
evaluationContext.setVariable("reverseStrings", reverseStringsMethod);
// String reverseStrings(String... strings)
// evaluates to "3.0, 2.0, 1.0, SpEL"
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
String message = parser.parseExpression(expression)
.getValue(evaluationContext, String.class);
assertThat(message).isEqualTo("3.0, 2.0, 1, SpEL");
}
@Test
void varargsMethodInvocationWithArgumentsAsStringArray() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
}
@Nested @Nested
class TernaryOperator { class TernaryOperator {
@ -906,6 +962,12 @@ class SpelDocumentationTests extends AbstractExpressionTests {
public static String reverseString(String input) { public static String reverseString(String input) {
return new StringBuilder(input).reverse().toString(); return new StringBuilder(input).reverse().toString();
} }
public static String reverseStrings(String... strings) {
List<String> list = Arrays.asList(strings);
Collections.reverse(list);
return list.stream().collect(Collectors.joining(", "));
}
} }
private static class ListConcatenation implements OperatorOverloader { private static class ListConcatenation implements OperatorOverloader {