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 9974b027db41..d914a7002965 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 @@ -5,7 +5,17 @@ The safe navigation operator (`?`) is used to avoid a `NullPointerException` and 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 -this, the safe navigation operator returns `null` instead of throwing an exception. +this, the safe navigation operator returns `null` for the particular null-safe operation +instead of throwing an exception. + +[WARNING] +==== +When the safe navigation operator evaluates to `null` for a particular null-safe +operation within a compound expression, the remainder of the compound expression will +still be evaluated. + +See <> for details. +==== [[expressions-operator-safe-navigation-property-access]] == Safe Property and Method Access @@ -283,3 +293,71 @@ Kotlin:: ====== +[[expressions-operator-safe-navigation-compound-expressions]] +== Null-safe Operations in Compound Expressions + +As mentioned at the beginning of this section, when the safe navigation operator +evaluates to `null` for a particular null-safe operation within a compound expression, +the remainder of the compound expression will still be evaluated. This means that the +safe navigation operator must be applied throughout a compound expression in order to +avoid any unwanted `NullPointerException`. + +Given the expression `#person?.address.city`, if `#person` is `null` the safe navigation +operator (`?.`) ensures that no exception will be thrown when attempting to access the +`address` property of `#person`. However, since `#person?.address` evaluates to `null`, a +`NullPointerException` will be thrown when attempting to access the `city` property of +`null`. To address that, you can apply null-safe navigation throughout the compound +expression as in `#person?.address?.city`. That expression will safely evaluate to `null` +if either `#person` or `#person?.address` evaluates to `null`. + +The following example demonstrates how to use the "null-safe select first" operator +(`?.^`) on a collection combined with null-safe property access (`?.`) within a compound +expression. If `members` is `null`, the result of the "null-safe select first" operator +(`members?.^[nationality == 'Serbian']`) evaluates to `null`, and the additional use of +the safe navigation operator (`?.name`) ensures that the entire compound expression +evaluates to `null` instead of throwing an exception. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + ExpressionParser parser = new SpelExpressionParser(); + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = "members?.^[nationality == 'Serbian']?.name"; // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String.class); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String.class); +---- +<1> Use "null-safe select first" and null-safe property access operators within compound expression. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + val parser = SpelExpressionParser() + val society = IEEE() + val context = StandardEvaluationContext(society) + val expression = "members?.^[nationality == 'Serbian']?.name" // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String::class.java) + + society.members = null + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String::class.java) +---- +<1> Use "null-safe select first" and null-safe property access operators within compound expression. +====== 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 14d9d60e1987..c223ab257037 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 @@ -689,6 +689,25 @@ void nullSafeSelectFirst() { assertThat(inventor).isNull(); } + @Test + void nullSafeSelectFirstAndPropertyAccess() { + IEEE society = new IEEE(); + StandardEvaluationContext context = new StandardEvaluationContext(society); + String expression = "members?.^[nationality == 'Serbian']?.name"; // <1> + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression(expression) + .getValue(context, String.class); + assertThat(name).isEqualTo("Nikola Tesla"); + + society.members = null; + + // evaluates to null - does not throw a NullPointerException + name = parser.parseExpression(expression) + .getValue(context, String.class); + assertThat(name).isNull(); + } + @Test @SuppressWarnings("unchecked") void nullSafeSelectLast() {