diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index f234f86591..da2650dcec 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -60,6 +60,7 @@ **** xref:core/expressions/language-ref/constructors.adoc[] **** xref:core/expressions/language-ref/variables.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/operator-ternary.adoc[] **** xref:core/expressions/language-ref/operator-elvis.adoc[] diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc index 66c1d56ba7..4057f7943a 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/constructors.adoc @@ -3,8 +3,10 @@ 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 -(`Integer`, `Float`, `String`, and so on). The following example shows how to use the -`new` operator to invoke constructors: +(`Integer`, `Float`, `String`, and so on). +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] ====== @@ -38,4 +40,3 @@ Kotlin:: ====== - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc index c4a23012e2..00f3cb56fd 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/functions.adoc @@ -2,8 +2,12 @@ = Functions 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 -variables in `EvaluationContext` implementations via the `setVariable()` method. +expressions by using the `#functionName(...)` syntax, and like with standard 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] ==== @@ -111,7 +115,8 @@ been fully bound prior to registration; however, partially bound handles are als supported. 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 example shows: @@ -203,4 +208,3 @@ Kotlin:: ====== - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc index 4c9d151bab..6e6f26e173 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/methods.adoc @@ -1,9 +1,11 @@ [[expressions-methods]] = Methods -You can invoke methods by using typical Java programming syntax. You can also invoke methods -on literals. Variable arguments are also supported. The following examples show how to -invoke methods: +You can invoke methods by using the typical Java programming syntax. You can also invoke +methods directly on literals such as strings or numbers. +xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well. + +The following examples show how to invoke methods. [tabs] ====== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/varargs.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/varargs.adoc new file mode 100644 index 0000000000..8b7240a13e --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/varargs.adoc @@ -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) +---- +====== + diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index 55605ea3c2..57cc2168ea 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -21,10 +21,13 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import example.Color; 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 class TernaryOperator { @@ -906,6 +962,12 @@ class SpelDocumentationTests extends AbstractExpressionTests { public static String reverseString(String input) { return new StringBuilder(input).reverse().toString(); } + + public static String reverseStrings(String... strings) { + List list = Arrays.asList(strings); + Collections.reverse(list); + return list.stream().collect(Collectors.joining(", ")); + } } private static class ListConcatenation implements OperatorOverloader {