From 218a148898914955bbb8a0abbc190af51dea43c3 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:24:29 +0100 Subject: [PATCH] Document null-safe index operator in SpEL See gh-29847 --- .../operator-safe-navigation.adoc | 60 ++++++++++++++++++- .../language-ref/properties-arrays.adoc | 4 ++ .../expression/spel/ast/Indexer.java | 7 +++ .../spel/SpelDocumentationTests.java | 18 ++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc index d914a7002965..d8367f70df8d 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc @@ -1,7 +1,7 @@ [[expressions-operator-safe-navigation]] = Safe Navigation Operator -The safe navigation operator (`?`) is used to avoid a `NullPointerException` and comes +The safe navigation operator (`?.`) is used to avoid a `NullPointerException` and comes from the https://www.groovy-lang.org/operators.html#_safe_navigation_operator[Groovy] language. Typically, when you have a reference to an object, you might need to verify that it is not `null` before accessing methods or properties of the object. To avoid @@ -81,6 +81,64 @@ For example, the expression `#calculator?.max(4, 2)` evaluates to `null` if the `max(int, int)` method will be invoked on the `#calculator`. ==== +[[expressions-operator-safe-navigation-indexing]] +== Safe Index Access + +Since Spring Framework 6.2, the Spring Expression Language supports safe navigation for +indexing into the following types of structures. + +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-arrays-and-collections[arrays and collections] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-strings[strings] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-maps[maps] +* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-objects[objects] + +The following example shows how to use the safe navigation operator for indexing into +a list (`?.[]`). + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + EvaluationContext context = new StandardEvaluationContext(society); + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor.class); + + society.members = null; + + // evaluates to null - does not throw an exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor.class); +---- +<1> Use null-safe index operator on a non-null `members` list +<2> Use null-safe index operator on a null `members` list + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + + // evaluates to Inventor("Nikola Tesla") + var inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor::class.java) + + society.members = null + + // evaluates to null - does not throw an exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor::class.java) +---- +<1> Use null-safe index operator on a non-null `members` list +<2> Use null-safe index operator on a null `members` list +====== [[expressions-operator-safe-navigation-selection-and-projection]] == Safe Collection Selection and Projection diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc index 8e8360d1da31..32e6fd54e0e8 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc @@ -7,6 +7,10 @@ into various structures. NOTE: Numerical index values are zero-based, such as when accessing the n^th^ element of an array in Java. +TIP: See the xref:core/expressions/language-ref/operator-safe-navigation.adoc[Safe Navigation Operator] +section for details on how to navigate object graphs and index into various structures +using the null-safe operator. + [[expressions-property-navigation]] == Property Navigation diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 033817bd944f..5ead18c349a1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -58,6 +58,13 @@ *
As of Spring Framework 6.2, null-safe indexing is supported via the {@code '?.'} + * operator. For example, {@code 'colors?.[0]'} will evaluate to {@code null} if + * {@code colors} is {@code null} and will otherwise evaluate to the 0th + * color. + * * @author Andy Clement * @author Phillip Webb * @author Stephane Nicoll 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 07c9bd3b79f7..b512cf545181 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 @@ -688,6 +688,24 @@ void nullSafePropertyAccess() { assertThat(city).isNull(); } + @Test + void nullSafeIndexing() { + IEEE society = new IEEE(); + EvaluationContext context = new StandardEvaluationContext(society); + + // evaluates to Inventor("Nikola Tesla") + Inventor inventor = parser.parseExpression("members?.[0]") // <1> + .getValue(context, Inventor.class); + assertThat(inventor).extracting(Inventor::getName).isEqualTo("Nikola Tesla"); + + society.members = null; + + // evaluates to null - does not throw an Exception + inventor = parser.parseExpression("members?.[0]") // <2> + .getValue(context, Inventor.class); + assertThat(inventor).isNull(); + } + @Test @SuppressWarnings("unchecked") void nullSafeSelection() {