集合投影 (Collection Projection)
投影(Projection)允许集合驱动一个子表达式的评估,评估结果会构成一个新的集合。
投影的语法为 .![projectionExpression]。例如,假设我们有一个发明家列表,但我们想要的是他们出生城市的列表。实际上,我们希望对发明家列表中的每一项评估 placeOfBirth.city。以下示例使用投影实现了这一点:
java
// 结果为 ["Smiljan", "Idvor"]
List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]")
.getValue(societyContext, List.class);kotlin
// 结果为 ["Smiljan", "Idvor"]
val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]")
.getValue(societyContext) as List<*>投影支持数组以及任何实现了 java.lang.Iterable 或 java.util.Map 的对象。
当使用 Map 驱动投影时,投影表达式会针对 Map 中的每个条目(表示为 Java 的 Map.Entry)进行评估。对 Map 进行投影的结果是一个列表 (List),其中包含了对每个 Map 条目评估投影表达式所得的结果。
注意
Spring 表达式语言同样支持集合投影的安全导航。语法为 ?.!,当源集合为 null 时直接返回 null 而不抛出异常。
补充教学
1. SpEL 投影 vs Java Stream map
SpEL 的投影操作逻辑上等同于 Java Stream API 的 map 操作。
- SpEL:
list.![name] - Java:
list.stream().map(e -> e.getName()).toList()它提供了一种在表达式层面实现“属性提取”或“数据转换”的极简手段。
2. Map 投影的结果陷阱
这是一个常见的误区:对 Map 进行投影后,返回的结果类型不再是 Map,而是一个 List。 因为投影是对每个条目(Entry)进行转换,SpEL 无法自动推断出转换后的对象应如何作为新的 Key 或 Value,因此统一将结果收集到列表中。
- 示例:
#map.![value]将提取 Map 中所有的 Value 到一个列表中。
3. 黄金组合:先筛选,后投影
在实际业务(特别是 Spring Data JPA 或安全策略)中,我们经常需要“先过滤符合条件的条目,再提取特定字段”。在 SpEL 中你可以直接链式调用:
java
// 语义:获取所有“塞尔维亚”籍发明家的“名字”列表
"members.?[nationality == 'Serbian'].![name]"这种链式调用非常强大,能在一行字符串内完成复杂的集合处理逻辑,具有极高的声明性。
4. 健壮性与性能
- 空指针风险:由于投影是对每个元素执行表达式,如果集合中某个元素为
null,而投影表达式试图访问其属性(如list.![name]),会抛出 NPE。- 应对方案:建议结合安全属性访问使用:
list.![#this?.name]。
- 应对方案:建议结合安全属性访问使用:
- 结果动态性:投影表达式不限于提取属性,还可以是计算逻辑:java
// 提取所有发明家的年龄,并统一加 1 "members.![age + 1]" - 反射开销:投影在大集合上运行会有一定的反射或索引解析开销。如果是对性能极度敏感的代码块,建议在 Java 层使用 Stream 处理。