Skip to content

安全引用运算符 (Safe Navigation Operator)

安全引用运算符 (?.) 用于避免 NullPointerException,其灵感源自 Groovy 语言。

通常情况下,当你引用一个对象时,你可能需要在访问该对象的方法或属性之前验证它是否不为 null。为了简化这一逻辑,安全引用运算符在遇到 null 时会直接返回 null,而不会抛出异常。

警告:复合表达式中的求值规则

当复合表达式中的某个安全操作求值为 null 时,该复合表达式的剩余部分仍会被执行评估

这意味着你需要对整条链条应用安全引用,详见复合表达式中的空安全操作

安全属性和方法访问

以下示例演示了如何使用安全引用运算符访问属性 (?.):

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

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

// 结果为 "Smiljan"
String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);

// 将属性设为 null
tesla.setPlaceOfBirth(null);

// 结果为 null,而不会抛出 NullPointerException
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
kotlin
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

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

// 结果为 "Smiljan"
var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java)

tesla.setPlaceOfBirth(null)

// 结果为 null
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java)

注意

安全引用运算符同样适用于对象上的方法调用。 例如,如果变量 #calculator 未在上下文中配置,表达式 #calculator?.max(4, 2) 将返回 null,而不会报错。

安全索引访问

Spring Framework 6.2 起,SpEL 支持对以下结构的索引访问进行安全导航:

以下示例演示了如何对列表使用安全索引运算符 (?.[]):

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

// 正常访问,结果为 Inventor("Nikola Tesla")
Inventor inventor = parser.parseExpression("members?.[0]").getValue(context, Inventor.class);

// 将列表设为 null
society.members = null;

// 结果为 null,而不会抛出异常
inventor = parser.parseExpression("members?.[0]").getValue(context, Inventor.class);
kotlin
val parser = SpelExpressionParser()
val society = IEEE()
val context = StandardEvaluationContext(society)

// 结果为 Inventor("Nikola Tesla")
var inventor = parser.parseExpression("members?.[0]").getValue(context, Inventor::class.java)

society.members = null

// 结果为 null
inventor = parser.parseExpression("members?.[0]").getValue(context, Inventor::class.java)

安全集合选择与投影

SpEL 支持在集合选择集合投影中使用安全导航:

  • 空安全选择?.?
  • 空安全选择首项?.^
  • 空安全选择末项?.$
  • 空安全投影?.!

以下是以“安全选择首项 (?.^)”为例的说明:

java
String expression = "members?.^[nationality == 'Serbian']";

// 如果 members 为 null,下式返回 null 而非异常
Inventor inventor = parser.parseExpression(expression).getValue(context, Inventor.class);
kotlin
val expression = "members?.^[nationality == 'Serbian']"

// 返回 null 或匹配的第一个发明家
val inventor = parser.parseExpression(expression).getValue(context, Inventor::class.java)

对 Optional 的空安全操作 (Spring 7.0+)

Spring Framework 7.0 起,安全操作支持 java.util.Optional 实例,具有透明解包语义

具体而言,当安全运算符(如 ?.?.[])应用于空的 Optional 时,它会被视同为 null,随后的操作结果为 null。而当它应用于非空的 Optional 时,后续操作将作用于该 Optional 中包含的对象,即实现了解包。

例如,如果 user 的类型是 Optional<User>

  • user?.name:如果 usernull 或空的 Optional,结果为 null
  • 否则,结果等价于 user.get().getName()

提示

即便在空的 Optional 上,你仍然可以调用 Optional API 本身的方法。例如,如果 nameOptional<String>name?.orElse('Unknown')name 为空时将正常返回 "Unknown"

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

正如本章开头提到的,如果复合表达式中的某个安全操作返回了 null,表达式的后续部分仍会被链式执行。因此,为了彻底避免 NPE,你必须在整条复合表达式链中应用安全操作。

错误示例#person?.address.city 如果 #personnull#person?.address 会安全地返回 null。但接下来表达式会尝试访问 null.city,这会导致 NullPointerException

正确做法#person?.address?.city 该表达式在 #person#person.address 任何一级为 null 时都会安全地返回 null

以下是一个结合了集合选择与属性访问的复合示例:

java
// members 可能为 null,选出的第一个发明家也可能为 null,其 name 属性访问也需保护
String expression = "members?.^[nationality == 'Serbian']?.name";

// 即使 society.members 为 null,结果也会是 null
String name = parser.parseExpression(expression).getValue(context, String.class);
kotlin
val expression = "members?.^[nationality == 'Serbian']?.name"

val name = parser.parseExpression(expression).getValue(context, String::class.java)

补充教学

1. 为什么 SpEL 坚持要“全链条保护”?

这与 Kotlin 或 Groovy 的行为略有不同。在某些语言中,一旦链条中出现空值,后续所有操作都会隐式短路。 但 SpEL 的设计更加显式:

  • 灵活性:它允许你在链条中间引入像 ?.orElse('default') 这样的逻辑,如果全部自动短路,就无法实现这种“捕获空值并转换”的功能。
  • 开发建议:始终坚持 “只要前面加了问号,后面所有点都要带问号” 的原则,除非你后面接的是处理空值的逻辑。

2. Spring 6.2 索引保护带来的变革

在 Spring 6.2 之前,list?.[0] 是不合法的语法。你必须写成复杂的三元运算,如 list != null ? list[0] : null。 现在有了 ?.[Index],这大大的简化了:

  • 前端展示:在 Thymeleaf 模板中使用 ${user?.roles?.[0]?.name}
  • 配置解析:从环境变量解析列表属性时,避免索引越界或基础集合丢失导致的挂掉。

3. Spring 7.0 Optional 解包的深意

Spring 7 对 Optional 的支持实际上是将 SpEL 从传统的“Java 反射调用器”向“现代化语言解析器”进化的体现。 它让 Optional 从之前的“必须要手动调用 .get() 的累赘”变成了“可以被 SpEL 自动感知并处理的安全容器”。

4. 性能与安全性平衡

虽然 ?. 很好用,但它会掩盖程序中的数据缺失问题。

  • 调试建议:如果你发现表达式一直返回 null 却不知道哪一级出的问题,暂时去掉 ?. 观察异常栈。
  • 实战忠告:在核心业务计算(如计费金额转换)中,与其使用 ?. 得到一个错误的 0 或 null,不如让它抛出异常,提前发现上游数据不一致的问题。

Based on Spring Framework.