Appearance
Spring Expression Language (SpEL) 集合投影详解 🎯
什么是集合投影?
集合投影(Collection Projection)是 Spring Expression Language (SpEL) 中一个强大的功能,它允许我们从集合中提取特定的属性或计算结果,并生成一个新的集合。
NOTE
投影的核心思想是:让集合驱动子表达式的求值,结果是一个新的集合。
为什么需要集合投影? 🤔
在日常开发中,我们经常遇到这样的场景:
- 从用户列表中提取所有用户的姓名
- 从订单列表中获取所有订单的金额
- 从商品列表中提取所有商品的分类信息
kotlin
// 传统的 Java/Kotlin 方式
val users = getUserList()
val userNames = mutableListOf<String>()
for (user in users) {
userNames.add(user.name)
}
// 或者使用 Stream API
val userNames = users.map { it.name }
kotlin
// 使用 SpEL 投影,一行代码搞定
val userNames = parser.parseExpression("![name]")
.getValue(users) as List<String>
TIP
SpEL 投影提供了一种声明式的方式来处理集合转换,代码更简洁、更易读!
投影语法详解 📝
基本语法
.![projectionExpression]
.![]
是投影操作符projectionExpression
是要对每个元素执行的表达式
实际应用示例
让我们通过一个完整的 SpringBoot 示例来理解投影的应用:
kotlin
// 数据模型
data class Inventor(
val name: String,
val placeOfBirth: PlaceOfBirth,
val inventions: List<String>
)
data class PlaceOfBirth(
val city: String,
val country: String
)
data class Society(
val name: String,
val members: List<Inventor>
)
kotlin
@Service
class InventorService {
private val parser = SpelExpressionParser()
fun createSampleData(): Society {
val tesla = Inventor(
name = "Nikola Tesla",
placeOfBirth = PlaceOfBirth("Smiljan", "Croatia"),
inventions = listOf("AC Motor", "Tesla Coil")
)
val pupin = Inventor(
name = "Mihajlo Pupin",
placeOfBirth = PlaceOfBirth("Idvor", "Serbia"),
inventions = listOf("Long Distance Telephony", "X-Ray Photography")
)
return Society("IEEE", listOf(tesla, pupin))
}
// 使用投影获取所有发明家的出生城市
fun getBirthCities(): List<String> {
val society = createSampleData()
val context = StandardEvaluationContext(society)
// SpEL 投影:从 members 集合中提取每个成员的 placeOfBirth.city
return parser.parseExpression("members.![placeOfBirth.city]")
.getValue(context) as List<String>
// 结果: ["Smiljan", "Idvor"]
}
// 投影获取所有发明家的姓名
fun getInventorNames(): List<String> {
val society = createSampleData()
val context = StandardEvaluationContext(society)
return parser.parseExpression("members.![name]")
.getValue(context) as List<String>
// 结果: ["Nikola Tesla", "Mihajlo Pupin"]
}
}
投影的工作原理 ⚙️
让我们通过时序图来理解投影的执行过程:
支持的集合类型 📋
SpEL 投影支持多种集合类型:
1. 数组投影
kotlin
@RestController
class ArrayProjectionController {
private val parser = SpelExpressionParser()
@GetMapping("/array-projection")
fun arrayProjection(): List<String> {
val inventors = arrayOf(
Inventor("Tesla", PlaceOfBirth("Smiljan", "Croatia"), emptyList()),
Inventor("Edison", PlaceOfBirth("Milan", "USA"), emptyList())
)
val context = StandardEvaluationContext()
context.setVariable("inventors", inventors)
// 对数组进行投影
return parser.parseExpression("#inventors.![name]")
.getValue(context) as List<String>
}
}
2. List 投影
kotlin
@Service
class ListProjectionService {
private val parser = SpelExpressionParser()
fun getInventionCounts(): List<Int> {
val inventors = listOf(
Inventor("Tesla", PlaceOfBirth("Smiljan", "Croatia"),
listOf("AC Motor", "Tesla Coil", "Wireless Power")),
Inventor("Edison", PlaceOfBirth("Milan", "USA"),
listOf("Light Bulb", "Phonograph"))
)
val context = StandardEvaluationContext()
context.setVariable("inventors", inventors)
// 投影获取每个发明家的发明数量
return parser.parseExpression("#inventors.![inventions.size()]")
.getValue(context) as List<Int>
// 结果: [3, 2]
}
}
3. Map 投影
kotlin
@Service
class MapProjectionService {
private val parser = SpelExpressionParser()
fun getMapProjection(): List<String> {
val inventorMap = mapOf(
"tesla" to Inventor("Nikola Tesla", PlaceOfBirth("Smiljan", "Croatia"), emptyList()),
"edison" to Inventor("Thomas Edison", PlaceOfBirth("Milan", "USA"), emptyList())
)
val context = StandardEvaluationContext()
context.setVariable("inventorMap", inventorMap)
// Map 投影:对每个 Map.Entry 执行表达式
return parser.parseExpression("#inventorMap.![value.name]")
.getValue(context) as List<String>
// 结果: ["Nikola Tesla", "Thomas Edison"]
}
fun getMapKeys(): List<String> {
val inventorMap = mapOf(
"tesla" to Inventor("Nikola Tesla", PlaceOfBirth("Smiljan", "Croatia"), emptyList()),
"edison" to Inventor("Thomas Edison", PlaceOfBirth("Milan", "USA"), emptyList())
)
val context = StandardEvaluationContext()
context.setVariable("inventorMap", inventorMap)
// 获取所有的 key
return parser.parseExpression("#inventorMap.![key]")
.getValue(context) as List<String>
// 结果: ["tesla", "edison"]
}
}
高级投影技巧 🚀
1. 复杂表达式投影
kotlin
@Service
class AdvancedProjectionService {
private val parser = SpelExpressionParser()
fun getComplexProjection(): List<String> {
val society = createSampleData()
val context = StandardEvaluationContext(society)
// 复杂表达式:组合多个属性
return parser.parseExpression(
"members.![name + ' from ' + placeOfBirth.city]"
).getValue(context) as List<String>
// 结果: ["Nikola Tesla from Smiljan", "Mihajlo Pupin from Idvor"]
}
fun getConditionalProjection(): List<String> {
val society = createSampleData()
val context = StandardEvaluationContext(society)
// 条件表达式投影
return parser.parseExpression(
"members.![inventions.size() > 1 ? name + ' (多项发明)' : name]"
).getValue(context) as List<String>
}
}
2. 嵌套投影
kotlin
@Service
class NestedProjectionService {
private val parser = SpelExpressionParser()
fun getNestedProjection(): List<List<String>> {
val society = createSampleData()
val context = StandardEvaluationContext(society)
// 嵌套投影:获取所有发明家的所有发明
return parser.parseExpression("members.![inventions]")
.getValue(context) as List<List<String>>
}
fun getFlattenedInventions(): List<String> {
val society = createSampleData()
val context = StandardEvaluationContext(society)
// 扁平化投影(需要结合其他操作)
val nestedResult = parser.parseExpression("members.![inventions]")
.getValue(context) as List<List<String>>
return nestedResult.flatten() // Kotlin 的 flatten 操作
}
}
安全导航与投影 🛡️
IMPORTANT
SpEL 还支持安全导航的集合投影,可以避免空指针异常。
kotlin
@Service
class SafeProjectionService {
private val parser = SpelExpressionParser()
fun getSafeProjection(): List<String?> {
val inventorsWithNulls = listOf(
Inventor("Tesla", PlaceOfBirth("Smiljan", "Croatia"), emptyList()),
null, // 可能包含 null 元素
Inventor("Edison", PlaceOfBirth("Milan", "USA"), emptyList())
)
val context = StandardEvaluationContext()
context.setVariable("inventors", inventorsWithNulls)
// 安全导航投影:使用 ?. 操作符
return parser.parseExpression("#inventors.![?.name]")
.getValue(context) as List<String?>
// 结果: ["Tesla", null, "Edison"]
}
}
实际业务场景应用 💼
电商系统中的应用
kotlin
// 电商数据模型
data class Product(
val id: Long,
val name: String,
val price: BigDecimal,
val category: Category,
val tags: List<String>
)
data class Category(
val id: Long,
val name: String
)
data class Order(
val id: Long,
val products: List<Product>,
val customerName: String
)
@Service
class ECommerceProjectionService {
private val parser = SpelExpressionParser()
// 获取订单中所有商品的名称
fun getProductNames(order: Order): List<String> {
val context = StandardEvaluationContext(order)
return parser.parseExpression("products.![name]")
.getValue(context) as List<String>
}
// 获取订单中所有商品的分类名称
fun getCategoryNames(order: Order): List<String> {
val context = StandardEvaluationContext(order)
return parser.parseExpression("products.![category.name]")
.getValue(context) as List<String>
}
// 计算订单总金额(使用投影 + 聚合)
fun calculateTotalAmount(order: Order): BigDecimal {
val context = StandardEvaluationContext(order)
val prices = parser.parseExpression("products.![price]")
.getValue(context) as List<BigDecimal>
return prices.fold(BigDecimal.ZERO) { acc, price -> acc + price }
}
}
性能考虑与最佳实践 ⚡
WARNING
虽然 SpEL 投影很强大,但在大数据集上使用时需要考虑性能影响。
性能对比
kotlin
// SpEL 投影方式
fun getNamesBySpEL(users: List<User>): List<String> {
val context = StandardEvaluationContext()
context.setVariable("users", users)
return parser.parseExpression("#users.![name]")
.getValue(context) as List<String>
}
kotlin
// Kotlin 原生方式(通常更快)
fun getNamesByKotlin(users: List<User>): List<String> {
return users.map { it.name }
}
最佳实践建议
使用建议
- 小到中等规模的集合:SpEL 投影提供了很好的可读性和灵活性
- 大规模数据集:考虑使用 Kotlin 原生的
map
、filter
等操作 - 动态表达式:当投影表达式需要在运行时动态构建时,SpEL 是最佳选择
- 配置驱动:在配置文件中定义投影表达式时,SpEL 非常有用
总结 📚
集合投影是 SpEL 中一个强大而优雅的功能,它提供了:
✅ 声明式编程风格:用表达式描述"要什么"而不是"怎么做"
✅ 简洁的语法:一行代码完成复杂的集合转换
✅ 类型安全:支持安全导航,避免空指针异常
✅ 灵活性:支持复杂表达式和嵌套投影
✅ 广泛支持:数组、List、Map 等多种集合类型
NOTE
SpEL 集合投影特别适合在配置文件、模板引擎、动态查询等场景中使用,它让我们能够以声明式的方式处理集合数据,代码更加简洁和易读。
记住投影的核心语法:.![projectionExpression]
,让集合驱动表达式求值,创造新的集合! 🎯