安全引用运算符 (Safe Navigation Operator)
安全引用运算符 (?.) 用于避免 NullPointerException,其灵感源自 Groovy 语言。
通常情况下,当你引用一个对象时,你可能需要在访问该对象的方法或属性之前验证它是否不为 null。为了简化这一逻辑,安全引用运算符在遇到 null 时会直接返回 null,而不会抛出异常。
安全属性和方法访问
以下示例演示了如何使用安全引用运算符访问属性 (?.):
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);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 支持对以下结构的索引访问进行安全导航:
以下示例演示了如何对列表使用安全索引运算符 (?.[]):
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);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)安全集合选择与投影
- 空安全选择:
?.? - 空安全选择首项:
?.^ - 空安全选择末项:
?.$ - 空安全投影:
?.!
以下是以“安全选择首项 (?.^)”为例的说明:
String expression = "members?.^[nationality == 'Serbian']";
// 如果 members 为 null,下式返回 null 而非异常
Inventor inventor = parser.parseExpression(expression).getValue(context, Inventor.class);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:如果user为null或空的Optional,结果为null。- 否则,结果等价于
user.get().getName()。
提示
即便在空的 Optional 上,你仍然可以调用 Optional API 本身的方法。例如,如果 name 是 Optional<String>,name?.orElse('Unknown') 在 name 为空时将正常返回 "Unknown"。
复合表达式中的空安全操作
正如本章开头提到的,如果复合表达式中的某个安全操作返回了 null,表达式的后续部分仍会被链式执行。因此,为了彻底避免 NPE,你必须在整条复合表达式链中应用安全操作。
错误示例: #person?.address.city 如果 #person 是 null,#person?.address 会安全地返回 null。但接下来表达式会尝试访问 null.city,这会导致 NullPointerException。
正确做法: #person?.address?.city 该表达式在 #person 或 #person.address 任何一级为 null 时都会安全地返回 null。
以下是一个结合了集合选择与属性访问的复合示例:
// members 可能为 null,选出的第一个发明家也可能为 null,其 name 属性访问也需保护
String expression = "members?.^[nationality == 'Serbian']?.name";
// 即使 society.members 为 null,结果也会是 null
String name = parser.parseExpression(expression).getValue(context, String.class);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,不如让它抛出异常,提前发现上游数据不一致的问题。