安全导航操作符

安全导航操作符(?.)用于避免 NullPointerException,它源自 Groovy 语言。通常,当你持有一个对象的引用时,在访问该对象的方法或属性之前,可能需要先验证它是否不为 null。为了避免这种情况,安全导航操作符会在特定的空安全操作中返回 null,而不是抛出异常。spring-doc.cadn.net.cn

当安全导航操作符在复合表达式中的某个空安全操作求值为 null 时,该复合表达式的其余部分仍会被继续求值。spring-doc.cadn.net.cn

安全的属性和方法访问

以下示例展示了如何使用安全导航运算符进行属性访问(?.)。spring-doc.cadn.net.cn

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

// evaluates to "Smiljan"
String city = parser.parseExpression("placeOfBirth?.city") (1)
		.getValue(context, tesla, String.class);

tesla.setPlaceOfBirth(null);

// evaluates to null - does not throw NullPointerException
city = parser.parseExpression("placeOfBirth?.city") (2)
		.getValue(context, tesla, String.class);
1 在非空的 placeOfBirth 属性上使用安全导航运算符
2 对为空的 placeOfBirth 属性使用安全导航操作符
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

val tesla = Inventor("Nikola Tesla", "Serbian")
tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan"))

// evaluates to "Smiljan"
var city = parser.parseExpression("placeOfBirth?.city") (1)
		.getValue(context, tesla, String::class.java)

tesla.setPlaceOfBirth(null)

// evaluates to null - does not throw NullPointerException
city = parser.parseExpression("placeOfBirth?.city") (2)
		.getValue(context, tesla, String::class.java)
1 在非空的 placeOfBirth 属性上使用安全导航运算符
2 对为空的 placeOfBirth 属性使用安全导航操作符

安全导航运算符也适用于对象上的方法调用。spring-doc.cadn.net.cn

例如,表达式 #calculator?.max(4, 2) 在上下文中未配置 null 变量时,其求值结果为 #calculator。否则,将对 max(int, int) 调用 #calculator 方法。spring-doc.cadn.net.cn

安全索引访问

自 Spring Framework 6.2 起,Spring 表达式语言支持对以下类型的结构进行索引时使用安全导航。spring-doc.cadn.net.cn

以下示例展示了如何使用安全导航运算符对列表进行索引(?.[])。spring-doc.cadn.net.cn

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 在非空的 members 列表上使用空安全的索引操作符
2 在为 null 的 members 列表上使用空安全的索引操作符
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 在非空的 members 列表上使用空安全的索引操作符
2 在为 null 的 members 列表上使用空安全的索引操作符

安全的集合选择与投影

Spring 表达式语言通过以下操作符支持安全导航,用于 集合选择集合投影spring-doc.cadn.net.cn

以下示例展示了如何在集合选择中使用安全导航运算符(?.?)。spring-doc.cadn.net.cn

ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression = "members?.?[nationality == 'Serbian']"; (1)

// evaluates to [Inventor("Nikola Tesla")]
List<Inventor> list = (List<Inventor>) parser.parseExpression(expression)
		.getValue(context);

society.members = null;

// evaluates to null - does not throw a NullPointerException
list = (List<Inventor>) parser.parseExpression(expression)
		.getValue(context);
1 在可能为 null 的 members 列表上使用空安全的选择运算符
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
val expression = "members?.?[nationality == 'Serbian']" (1)

// evaluates to [Inventor("Nikola Tesla")]
var list = parser.parseExpression(expression)
		.getValue(context) as List<Inventor>

society.members = null

// evaluates to null - does not throw a NullPointerException
list = parser.parseExpression(expression)
		.getValue(context) as List<Inventor>
1 在可能为 null 的 members 列表上使用空安全的选择运算符

以下示例展示了如何对集合使用“空安全选择第一个”操作符(?.^)。spring-doc.cadn.net.cn

ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression =
	"members?.^[nationality == 'Serbian' || nationality == 'Idvor']"; (1)

// evaluates to Inventor("Nikola Tesla")
Inventor inventor = parser.parseExpression(expression)
		.getValue(context, Inventor.class);

society.members = null;

// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
		.getValue(context, Inventor.class);
1 在可能为 null 的 members 列表上使用“空安全的选择第一个”操作符
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
val expression =
	"members?.^[nationality == 'Serbian' || nationality == 'Idvor']" (1)

// evaluates to Inventor("Nikola Tesla")
var inventor = parser.parseExpression(expression)
		.getValue(context, Inventor::class.java)

society.members = null

// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
		.getValue(context, Inventor::class.java)
1 在可能为 null 的 members 列表上使用“空安全的选择第一个”操作符

以下示例展示了如何对集合使用“空安全的选择最后一个”操作符(?.$)。spring-doc.cadn.net.cn

ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);
String expression =
	"members?.$[nationality == 'Serbian' || nationality == 'Idvor']"; (1)

// evaluates to Inventor("Pupin")
Inventor inventor = parser.parseExpression(expression)
		.getValue(context, Inventor.class);

society.members = null;

// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
		.getValue(context, Inventor.class);
1 在可能为 null 的 members 列表上使用“空安全的选择最后一个”操作符
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)
val expression =
	"members?.$[nationality == 'Serbian' || nationality == 'Idvor']" (1)

// evaluates to Inventor("Pupin")
var inventor = parser.parseExpression(expression)
		.getValue(context, Inventor::class.java)

society.members = null

// evaluates to null - does not throw a NullPointerException
inventor = parser.parseExpression(expression)
		.getValue(context, Inventor::class.java)
1 在可能为 null 的 members 列表上使用“空安全的选择最后一个”操作符

以下示例展示了如何使用安全导航运算符进行集合投影(?.!)。spring-doc.cadn.net.cn

ExpressionParser parser = new SpelExpressionParser();
IEEE society = new IEEE();
StandardEvaluationContext context = new StandardEvaluationContext(society);

// evaluates to ["Smiljan", "Idvor"]
List placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") (1)
		.getValue(context, List.class);

society.members = null;

// evaluates to null - does not throw a NullPointerException
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") (2)
		.getValue(context, List.class);
1 在非空的 members 列表上使用空安全投影运算符
2 在为 null 的 members 列表上使用空安全投影操作符
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)

// evaluates to ["Smiljan", "Idvor"]
var placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") (1)
		.getValue(context, List::class.java)

society.members = null

// evaluates to null - does not throw a NullPointerException
placesOfBirth = parser.parseExpression("members?.![placeOfBirth.city]") (2)
		.getValue(context, List::class.java)
1 在非空的 members 列表上使用空安全投影运算符
2 在为 null 的 members 列表上使用空安全投影操作符

对空安全操作Optional

从 Spring Framework 7.0 起,支持对 java.util.Optional 实例进行空安全操作,并具有透明的解包语义。spring-doc.cadn.net.cn

具体来说,当对一个emptyOptional应用空安全操作符时,它将被视为Optionalnull,后续操作的结果也将是null。然而,如果对一个非空的Optional应用空安全操作符,则后续操作将作用于Optional中包含的对象,从而有效地解包该Optionalspring-doc.cadn.net.cn

例如,如果 user 的类型为 Optional<User>,那么当 usernull 或一个emptyOptional 时,表达式 user?.name 的计算结果将为 null;否则,其计算结果将为 username,对于属性或字段访问而言,这实际上分别等同于 user.get().getName()user.get().namespring-doc.cadn.net.cn

emptyOptional 上,仍然支持调用在 Optional API 中定义的方法。例如,如果 name 的类型为 Optional<String>,那么当 name 是一个空的 Optional 时,表达式 name?.orElse('Unknown') 的计算结果将为 "Unknown";否则,如果 name 是一个非空的 Optional,该表达式将计算为包含在 Optional 中的 String,这实际上相当于 name.get()spring-doc.cadn.net.cn

同样地,如果 names 的类型为 Optional<List<String>>,表达式 names?.?⁠[#this.length > 5]nullnames 或一个emptynull 时将求值为 Optional;否则,它将求值为一个包含长度大于 5 的名称的序列,其效果等同于 names.get().stream().filter(s → s.length() > 5).toList()spring-doc.cadn.net.cn

本章前面提到的所有空安全操作符均适用相同的语义。spring-doc.cadn.net.cn

有关更多详细信息和示例,请参阅以下操作符的 Javadoc。spring-doc.cadn.net.cn

复合表达式中的空安全操作

如本节开头所述,当安全导航操作符在复合表达式中的某个空安全操作处求值为null时,该复合表达式的其余部分仍会被继续求值。这意味着,为了避免出现任何意外的NullPointerException,必须在整个复合表达式中都使用安全导航操作符。spring-doc.cadn.net.cn

给定表达式 #person?.address.city,如果 #personnull,则安全导航运算符(?.)可确保在尝试访问 #personaddress 属性时不会抛出异常。然而,由于 #person?.address 的计算结果为 null,因此在尝试访问 nullcity 属性时将抛出 NullPointerException。为解决此问题,您可以在整个复合表达式中应用空安全导航,如 #person?.address?.city 所示。该表达式将在 #person#person?.address 的计算结果为 null 时安全地求值为 nullspring-doc.cadn.net.cn

以下示例演示了如何在复合表达式中,对集合使用“空安全选择第一个”操作符(?.^)并结合空安全属性访问(?.)。如果 membersnull,则“空安全选择第一个”操作符(members?.^[nationality == 'Serbian'])的求值结果为 null,而进一步使用安全导航操作符(?.name)可确保整个复合表达式的求值结果为 null,而不是抛出异常。spring-doc.cadn.net.cn

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 在复合表达式中使用“空安全的选择第一个”操作符和空安全的属性访问操作符。
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 在复合表达式中使用“空安全的选择第一个”操作符和空安全的属性访问操作符。