Appearance
Spring Expression Language (SpEL) 语言参考指南 🚀
什么是 SpEL?为什么需要它?
想象一下,你正在开发一个电商系统,需要根据用户的会员等级、购买金额、促销活动等多种条件来动态计算折扣。传统的做法是写一堆 if-else 语句,但这样的代码既难维护又不够灵活。
Spring Expression Language (SpEL) 就是为了解决这个问题而生的!它是 Spring 框架提供的一种强大的表达式语言,让我们能够在运行时动态地计算表达式,就像在代码中嵌入了一个小型的计算器和逻辑处理器。
NOTE
SpEL 的核心价值在于将硬编码的逻辑转换为可配置的表达式,大大提升了应用的灵活性和可维护性。
SpEL 的设计哲学与核心原理
SpEL 的设计遵循以下几个核心原则:
- 表达式即配置:将复杂的业务逻辑以表达式的形式外化
- 运行时求值:支持在程序运行时动态计算表达式结果
- 类型安全:提供强类型支持,避免运行时类型错误
- Spring 生态集成:与 Spring 框架深度集成,可以访问 Bean、属性等
1. 字面量表达式 (Literal Expressions)
字面量是 SpEL 中最基础的表达式类型,直接表示具体的值。
kotlin
@Component
class PriceCalculator {
// 字符串字面量
@Value("#{`Hello World`}")
private lateinit var greeting: String
// 数字字面量
@Value("#{42}")
private var meaningOfLife: Int = 0
// 布尔字面量
@Value("#{true}")
private var isEnabled: Boolean = false
// null 字面量
@Value("#{null}")
private var optionalValue: String? = null
fun demonstrateLiterals() {
println("字符串: $greeting") // Hello World
println("数字: $meaningOfLife") // 42
println("布尔值: $isEnabled") // true
println("空值: $optionalValue") // null
}
}
kotlin
@Service
class ConfigService {
// 科学计数法
@Value("#{1.2E+2}") // 120.0
private var scientificNumber: Double = 0.0
// 十六进制
@Value("#{0xFF}") // 255
private var hexNumber: Int = 0
// 字符串中的特殊字符
@Value("#{`包含'单引号'的字符串`}")
private lateinit var stringWithQuotes: String
fun showAdvancedLiterals() {
println("科学计数法: $scientificNumber")
println("十六进制: $hexNumber")
println("特殊字符串: $stringWithQuotes")
}
}
TIP
在 SpEL 中,字符串字面量需要用反引号 `
包围,这样可以避免与 Spring 的属性占位符 ${}
产生冲突。
2. 属性、数组、列表、映射和索引器
SpEL 提供了强大的数据访问能力,可以轻松访问对象的属性、集合元素等。
kotlin
// 用户数据模型
data class User(
val id: Long,
val name: String,
val email: String,
val level: String,
val orders: List<Order> = emptyList(),
val preferences: Map<String, Any> = emptyMap()
)
data class Order(
val id: String,
val amount: Double,
val status: String
)
@Service
class UserService {
private val sampleUser = User(
id = 1L,
name = "张三",
email = "[email protected]",
level = "VIP",
orders = listOf(
Order("O001", 299.99, "COMPLETED"),
Order("O002", 199.99, "PENDING")
),
preferences = mapOf(
"theme" to "dark",
"language" to "zh-CN",
"notifications" to true
)
)
fun demonstratePropertyAccess() {
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// 设置根对象
val rootContext = StandardEvaluationContext(sampleUser)
// 访问简单属性
val userName = parser.parseExpression("name").getValue(rootContext, String::class.java)
println("用户名: $userName") // 张三
// 访问嵌套属性 - 第一个订单的金额
val firstOrderAmount = parser.parseExpression("orders[0].amount")
.getValue(rootContext, Double::class.java)
println("第一个订单金额: $firstOrderAmount") // 299.99
// 访问 Map 中的值
val theme = parser.parseExpression("preferences['theme']")
.getValue(rootContext, String::class.java)
println("主题设置: $theme") // dark
// 使用点号访问 Map(当 key 是有效标识符时)
val language = parser.parseExpression("preferences.language")
.getValue(rootContext, String::class.java)
println("语言设置: $language") // zh-CN
}
}
IMPORTANT
访问集合元素时,SpEL 使用方括号 []
语法,支持数字索引(List/Array)和字符串键(Map)。
3. 内联列表与映射
SpEL 支持直接在表达式中创建列表和映射,这在动态配置中非常有用。
kotlin
@Component
class CollectionDemo {
fun demonstrateInlineLists() {
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
// 创建内联列表
val numberList = parser.parseExpression("{1, 2, 3, 4, 5}")
.getValue(context, List::class.java)
println("数字列表: $numberList")
// 混合类型列表
val mixedList = parser.parseExpression("{`张三`, 25, true, null}")
.getValue(context, List::class.java)
println("混合列表: $mixedList")
// 嵌套列表
val nestedList = parser.parseExpression("{{1, 2}, {3, 4}, {5, 6}}")
.getValue(context, List::class.java)
println("嵌套列表: $nestedList")
// 访问列表元素
val secondElement = parser.parseExpression("{10, 20, 30, 40}[1]")
.getValue(context, Int::class.java)
println("第二个元素: $secondElement") // 20
}
}
kotlin
@Component
class MapDemo {
fun demonstrateInlineMaps() {
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
// 创建内联映射
val userInfo = parser.parseExpression(
"{name: `李四`, age: 28, city: `北京`}"
).getValue(context, Map::class.java)
println("用户信息: $userInfo")
// 访问映射值
val userName = parser.parseExpression(
"{name: `李四`, age: 28}[`name`]"
).getValue(context, String::class.java)
println("用户名: $userName") // 李四
// 复杂映射结构
val complexMap = parser.parseExpression("""
{
user: {name: `王五`, level: `VIP`},
settings: {theme: `light`, lang: `en`},
scores: {math: 95, english: 88}
}
""".trimIndent()).getValue(context, Map::class.java)
// 访问嵌套映射
val userLevel = parser.parseExpression(
"{user: {level: `VIP`}}.user.level"
).getValue(context, String::class.java)
println("用户等级: $userLevel") // VIP
}
}
4. 方法调用
SpEL 支持调用对象的方法,包括静态方法和实例方法。
kotlin
@Service
class MethodCallDemo {
fun demonstrateMethodCalls() {
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
// 字符串方法调用
val upperCase = parser.parseExpression("`hello world`.toUpperCase()")
.getValue(context, String::class.java)
println("大写转换: $upperCase") // HELLO WORLD
// 链式方法调用
val processed = parser.parseExpression(
"` Spring Boot `.trim().replace(` `, `-`).toLowerCase()"
).getValue(context, String::class.java)
println("处理后: $processed") // spring-boot
// 数学计算方法
val mathResult = parser.parseExpression("T(Math).max(10, 20)")
.getValue(context, Int::class.java)
println("最大值: $mathResult") // 20
// 集合方法调用
val listSize = parser.parseExpression("{1, 2, 3, 4, 5}.size()")
.getValue(context, Int::class.java)
println("列表大小: $listSize") // 5
}
// 自定义方法供 SpEL 调用
fun calculateDiscount(level: String, amount: Double): Double {
return when (level) {
"VIP" -> amount * 0.8
"GOLD" -> amount * 0.9
else -> amount
}
}
fun demonstrateCustomMethodCall() {
val parser = SpelExpressionParser()
val context = StandardEvaluationContext(this)
// 调用自定义方法
val discountPrice = parser.parseExpression("calculateDiscount(`VIP`, 100.0)")
.getValue(context, Double::class.java)
println("VIP 折扣价: $discountPrice") // 80.0
}
}
TIP
使用 T()
操作符可以访问类的静态方法和常量,例如 T(Math).PI
或 T(System).currentTimeMillis()
。
5. 运算符详解
SpEL 支持丰富的运算符,包括算术、比较、逻辑和特殊运算符。
kotlin
@Component
class OperatorDemo {
fun demonstrateArithmeticOperators() {
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
// 算术运算符
println("加法: ${parser.parseExpression("10 + 5").getValue(context)}") // 15
println("减法: ${parser.parseExpression("10 - 3").getValue(context)}") // 7
println("乘法: ${parser.parseExpression("4 * 6").getValue(context)}") // 24
println("除法: ${parser.parseExpression("15 / 3").getValue(context)}") // 5
println("取模: ${parser.parseExpression("17 % 5").getValue(context)}") // 2
println("幂运算: ${parser.parseExpression("2 ^ 3").getValue(context)}") // 8
// 比较运算符
println("等于: ${parser.parseExpression("5 == 5").getValue(context)}") // true
println("不等于: ${parser.parseExpression("5 != 3").getValue(context)}") // true
println("大于: ${parser.parseExpression("10 > 5").getValue(context)}") // true
println("小于等于: ${parser.parseExpression("3 <= 5").getValue(context)}") // true
}
}
kotlin
@Component
class LogicalOperatorDemo {
fun demonstrateLogicalOperators() {
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
// 逻辑运算符
println("逻辑与: ${parser.parseExpression("true and false").getValue(context)}") // false
println("逻辑或: ${parser.parseExpression("true or false").getValue(context)}") // true
println("逻辑非: ${parser.parseExpression("not true").getValue(context)}") // false
// instanceof 运算符
context.setVariable("str", "Hello")
println("类型检查: ${parser.parseExpression("#str instanceof T(String)").getValue(context)}") // true
// matches 正则表达式运算符
println("正则匹配: ${parser.parseExpression("`123-456-7890` matches `\\d{3}-\\d{3}-\\d{4}`").getValue(context)}") // true
// between 范围运算符
println("范围检查: ${parser.parseExpression("5 between {1, 10}").getValue(context)}") // true
}
}
6. 三元运算符与 Elvis 运算符
这两个运算符是处理条件逻辑和空值的利器。
kotlin
@Service
class ConditionalOperatorDemo {
data class Product(
val name: String,
val price: Double?,
val discount: Double? = null,
val category: String? = null
)
fun demonstrateTernaryOperator() {
val parser = SpelExpressionParser()
val product = Product("笔记本电脑", 5999.0, 0.1)
val context = StandardEvaluationContext(product)
// 三元运算符 (condition ? trueValue : falseValue)
val finalPrice = parser.parseExpression(
"discount != null ? price * (1 - discount) : price"
).getValue(context, Double::class.java)
println("最终价格: $finalPrice") // 5399.1
// 嵌套三元运算符
val priceLevel = parser.parseExpression(
"price > 10000 ? `高端` : (price > 5000 ? `中端` : `入门`)"
).getValue(context, String::class.java)
println("价格等级: $priceLevel") // 中端
}
fun demonstrateElvisOperator() {
val parser = SpelExpressionParser()
val product = Product("手机", 3999.0, category = null)
val context = StandardEvaluationContext(product)
// Elvis 运算符 (?:) - 提供默认值
val categoryName = parser.parseExpression("category ?: `未分类`")
.getValue(context, String::class.java)
println("商品分类: $categoryName") // 未分类
// 链式 Elvis 运算符
val displayName = parser.parseExpression("category ?: name ?: `未知商品`")
.getValue(context, String::class.java)
println("显示名称: $displayName") // 手机
}
}
NOTE
Elvis 运算符 ?:
是三元运算符的简化形式,当左侧表达式为 null 时返回右侧的默认值,非常适合处理空值场景。
7. 安全导航运算符
在处理可能为 null 的对象链时,安全导航运算符可以避免 NullPointerException。
kotlin
@Service
class SafeNavigationDemo {
data class Address(val city: String?, val street: String?)
data class Company(val name: String?, val address: Address?)
data class Employee(val name: String, val company: Company?)
fun demonstrateSafeNavigation() {
val parser = SpelExpressionParser()
// 完整对象链
val employee1 = Employee(
"张三",
Company("科技公司", Address("北京", "中关村大街"))
)
// 部分为 null 的对象链
val employee2 = Employee("李四", null)
val employee3 = Employee("王五", Company("咨询公司", null))
listOf(employee1, employee2, employee3).forEachIndexed { index, employee ->
val context = StandardEvaluationContext(employee)
// 不安全的访问方式(可能抛出异常)
try {
val unsafeCity = parser.parseExpression("company.address.city")
.getValue(context, String::class.java)
println("员工${index + 1} 城市 (不安全): $unsafeCity")
} catch (e: Exception) {
println("员工${index + 1} 城市访问失败: ${e.message}")
}
// 安全导航运算符 (?.)
val safeCity = parser.parseExpression("company?.address?.city")
.getValue(context, String::class.java)
println("员工${index + 1} 城市 (安全): ${safeCity ?: "未知"}")
// 结合 Elvis 运算符使用
val cityWithDefault = parser.parseExpression("company?.address?.city ?: `总部`")
.getValue(context, String::class.java)
println("员工${index + 1} 城市 (带默认值): $cityWithDefault")
println("---")
}
}
}
8. 集合选择与投影
SpEL 提供了强大的集合操作能力,可以进行筛选和转换操作。
kotlin
@Service
class CollectionSelectionDemo {
data class Order(
val id: String,
val amount: Double,
val status: String,
val customerLevel: String
)
fun demonstrateCollectionSelection() {
val orders = listOf(
Order("O001", 299.99, "COMPLETED", "VIP"),
Order("O002", 199.99, "PENDING", "NORMAL"),
Order("O003", 599.99, "COMPLETED", "VIP"),
Order("O004", 99.99, "CANCELLED", "NORMAL"),
Order("O005", 399.99, "COMPLETED", "GOLD")
)
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setVariable("orders", orders)
// 选择所有已完成的订单
val completedOrders = parser.parseExpression(
"#orders.?[status == 'COMPLETED']"
).getValue(context, List::class.java)
println("已完成订单数量: ${completedOrders?.size}") // 3
// 选择 VIP 客户的高价值订单
val vipHighValueOrders = parser.parseExpression(
"#orders.?[customerLevel == 'VIP' and amount > 300]"
).getValue(context, List::class.java)
println("VIP 高价值订单: ${vipHighValueOrders?.size}") // 1
// 选择第一个符合条件的订单
val firstPendingOrder = parser.parseExpression(
"#orders.^[status == 'PENDING']"
).getValue(context, Order::class.java)
println("第一个待处理订单: ${firstPendingOrder?.id}") // O002
// 选择最后一个符合条件的订单
val lastCompletedOrder = parser.parseExpression(
"#orders.$[status == 'COMPLETED']"
).getValue(context, Order::class.java)
println("最后一个完成订单: ${lastCompletedOrder?.id}") // O005
}
}
kotlin
@Service
class CollectionProjectionDemo {
data class Product(
val id: String,
val name: String,
val price: Double,
val tags: List<String>
)
fun demonstrateCollectionProjection() {
val products = listOf(
Product("P001", "iPhone 15", 6999.0, listOf("手机", "苹果", "5G")),
Product("P002", "MacBook Pro", 12999.0, listOf("笔记本", "苹果", "M3")),
Product("P003", "AirPods Pro", 1999.0, listOf("耳机", "苹果", "降噪"))
)
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setVariable("products", products)
// 投影:提取所有产品名称
val productNames = parser.parseExpression(
"#products.![name]"
).getValue(context, List::class.java)
println("产品名称列表: $productNames") // [iPhone 15, MacBook Pro, AirPods Pro]
// 投影:提取所有产品价格
val productPrices = parser.parseExpression(
"#products.![price]"
).getValue(context, List::class.java)
println("价格列表: $productPrices")
// 复杂投影:格式化产品信息
val formattedInfo = parser.parseExpression(
"#products.![name + ' - ¥' + price]"
).getValue(context, List::class.java)
println("格式化信息: $formattedInfo") // [iPhone 15 - ¥6999.0, ...]
// 结合选择和投影:获取高价产品的名称
val expensiveProductNames = parser.parseExpression(
"#products.?[price > 5000].![name]"
).getValue(context, List::class.java)
println("高价产品名称: $expensiveProductNames") // [iPhone 15, MacBook Pro]
}
}
IMPORTANT
集合操作符说明:
?[]
: 选择操作,筛选符合条件的元素^[]
: 选择第一个符合条件的元素$[]
: 选择最后一个符合条件的元素![]
: 投影操作,转换集合中的每个元素
9. 表达式模板
表达式模板允许在字符串中嵌入 SpEL 表达式,非常适合动态生成文本内容。
kotlin
@Service
class ExpressionTemplateDemo {
data class User(
val name: String,
val level: String,
val points: Int,
val lastLoginDays: Int
)
fun demonstrateExpressionTemplating() {
val user = User("张三", "VIP", 8500, 3)
val parser = SpelExpressionParser()
val context = StandardEvaluationContext(user)
// 基础模板
val welcomeMessage = parser.parseExpression(
"欢迎回来,#{name}!您是我们的#{level}会员。",
ParserContext { true } // 启用模板模式
).getValue(context, String::class.java)
println(welcomeMessage) // 欢迎回来,张三!您是我们的VIP会员。
// 复杂模板with条件逻辑
val statusMessage = parser.parseExpression("""
亲爱的#{name},
您当前积分:#{points}分
#{points >= 10000 ? '恭喜您达到钻石会员标准!' :
(points >= 5000 ? '您已是金牌会员,继续加油!' : '努力积累积分,升级会员等级!')}
#{lastLoginDays > 7 ? '好久不见,我们想念您!' : '感谢您的持续关注!'}
""".trimIndent(),
ParserContext { true }
).getValue(context, String::class.java)
println(statusMessage)
// 邮件模板示例
val emailTemplate = parser.parseExpression("""
<!DOCTYPE html>
<html>
<body>
<h2>#{level}会员专享优惠</h2>
<p>尊敬的#{name},</p>
<p>您的当前积分:<strong>#{points}</strong></p>
<p>#{level == 'VIP' ? '享受8折优惠' : (level == 'GOLD' ? '享受9折优惠' : '享受9.5折优惠')}</p>
</body>
</html>
""".trimIndent(),
ParserContext { true }
).getValue(context, String::class.java)
println("邮件模板:")
println(emailTemplate)
}
}
10. 实际业务场景应用示例
让我们通过一个完整的电商折扣计算系统来展示 SpEL 的实际应用价值。
完整的电商折扣系统示例
kotlin
// 业务模型
data class Customer(
val id: String,
val name: String,
val level: CustomerLevel,
val totalSpent: Double,
val joinDate: LocalDate,
val birthday: LocalDate?
)
enum class CustomerLevel {
BRONZE, SILVER, GOLD, PLATINUM, DIAMOND
}
data class Product(
val id: String,
val name: String,
val price: Double,
val category: String,
val isNewProduct: Boolean = false
)
data class DiscountRule(
val name: String,
val condition: String, // SpEL 表达式
val discount: String, // SpEL 表达式
val priority: Int
)
@Service
class DiscountCalculationService {
private val parser = SpelExpressionParser()
// 预定义的折扣规则
private val discountRules = listOf(
DiscountRule(
"生日特惠",
"customer.birthday != null and T(java.time.LocalDate).now().monthValue == customer.birthday.monthValue",
"0.8", // 8折
1
),
DiscountRule(
"VIP会员折扣",
"customer.level.name() matches '(GOLD|PLATINUM|DIAMOND)'",
"customer.level == T(CustomerLevel).DIAMOND ? 0.7 : (customer.level == T(CustomerLevel).PLATINUM ? 0.75 : 0.85)",
2
),
DiscountRule(
"新品推广",
"product.isNewProduct and product.category == 'Electronics'",
"0.9",
3
),
DiscountRule(
"高消费客户优惠",
"customer.totalSpent > 10000 and product.price > 1000",
"0.88",
4
),
DiscountRule(
"老客户回馈",
"T(java.time.Period).between(customer.joinDate, T(java.time.LocalDate).now()).years >= 2",
"0.92",
5
)
)
fun calculateDiscount(customer: Customer, product: Product): DiscountResult {
val context = StandardEvaluationContext().apply {
setVariable("customer", customer)
setVariable("product", product)
}
val applicableRules = mutableListOf<AppliedRule>()
var finalDiscount = 1.0
// 按优先级应用折扣规则
discountRules.sortedBy { it.priority }.forEach { rule ->
try {
val conditionResult = parser.parseExpression(rule.condition)
.getValue(context, Boolean::class.java) ?: false