Skip to content

集合投影 (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.Iterablejava.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 处理。

Based on Spring Framework.