Skip to content

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 } 
}

最佳实践建议

使用建议

  1. 小到中等规模的集合:SpEL 投影提供了很好的可读性和灵活性
  2. 大规模数据集:考虑使用 Kotlin 原生的 mapfilter 等操作
  3. 动态表达式:当投影表达式需要在运行时动态构建时,SpEL 是最佳选择
  4. 配置驱动:在配置文件中定义投影表达式时,SpEL 非常有用

总结 📚

集合投影是 SpEL 中一个强大而优雅的功能,它提供了:

声明式编程风格:用表达式描述"要什么"而不是"怎么做"
简洁的语法:一行代码完成复杂的集合转换
类型安全:支持安全导航,避免空指针异常
灵活性:支持复杂表达式和嵌套投影
广泛支持:数组、List、Map 等多种集合类型

NOTE

SpEL 集合投影特别适合在配置文件、模板引擎、动态查询等场景中使用,它让我们能够以声明式的方式处理集合数据,代码更加简洁和易读。

记住投影的核心语法:.![projectionExpression],让集合驱动表达式求值,创造新的集合! 🎯