Skip to content

Spring Expression Language (SpEL) 语言参考指南 🚀

什么是 SpEL?为什么需要它?

想象一下,你正在开发一个电商系统,需要根据用户的会员等级、购买金额、促销活动等多种条件来动态计算折扣。传统的做法是写一堆 if-else 语句,但这样的代码既难维护又不够灵活。

Spring Expression Language (SpEL) 就是为了解决这个问题而生的!它是 Spring 框架提供的一种强大的表达式语言,让我们能够在运行时动态地计算表达式,就像在代码中嵌入了一个小型的计算器和逻辑处理器。

NOTE

SpEL 的核心价值在于将硬编码的逻辑转换为可配置的表达式,大大提升了应用的灵活性和可维护性。

SpEL 的设计哲学与核心原理

SpEL 的设计遵循以下几个核心原则:

  1. 表达式即配置:将复杂的业务逻辑以表达式的形式外化
  2. 运行时求值:支持在程序运行时动态计算表达式结果
  3. 类型安全:提供强类型支持,避免运行时类型错误
  4. 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).PIT(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