Skip to content

Spring Expression Language (SpEL) - Functions 自定义函数详解 ⚙️

概述

Spring Expression Language (SpEL) 提供了强大的自定义函数功能,允许开发者扩展表达式的能力。通过注册用户定义的函数,我们可以在 SpEL 表达式中调用自定义的业务逻辑,使表达式更加灵活和强大。

IMPORTANT

自定义函数是 SpEL 的核心扩展机制之一,它让我们能够将复杂的业务逻辑封装成可重用的函数,在表达式中直接调用。

为什么需要自定义函数? 🤔

在实际开发中,我们经常遇到以下场景:

  1. 复杂的数据处理逻辑:需要在表达式中进行复杂的字符串处理、数学计算等
  2. 业务规则封装:将特定的业务规则封装成函数,便于复用
  3. 第三方库集成:将第三方库的功能暴露给 SpEL 表达式使用
  4. 性能优化:将重复的计算逻辑提取为函数,避免重复执行

设计哲学

SpEL 自定义函数的设计哲学是"可扩展性"和"简洁性"的完美结合。它允许开发者在保持表达式简洁的同时,扩展其功能边界。

函数注册机制

基本注册方式

自定义函数通过 EvaluationContextsetVariable() 方法注册:

kotlin
// 定义工具函数
fun reverseString(input: String): String {
    return StringBuilder(input).reverse().toString()
}

// 注册函数
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// 将函数注册为变量
context.setVariable("reverseString", ::reverseString.javaMethod) 

// 使用函数
val result = parser.parseExpression("#reverseString('hello')")
    .getValue(context, String::class.java)
// 结果: "olleh"
kotlin
// Java 风格的静态方法
object StringUtils {
    @JvmStatic
    fun reverseString(input: String): String {
        return StringBuilder(input).reverse().toString()
    }
}

// 注册方式
context.setVariable("reverseString", 
    StringUtils::class.java.getMethod("reverseString", String::class.java))

高级注册方式 - MethodHandle

对于更高性能的需求,可以使用 MethodHandle

kotlin
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// 使用 MethodHandle 注册 String.formatted 方法
val mh = MethodHandles.lookup().findVirtual(
    String::class.java, 
    "formatted",
    MethodType.methodType(String::class.java, Array<Any>::class.java)
)
context.setVariable("message", mh) 

// 使用函数(支持可变参数)
val message = parser.parseExpression(
    "#message('Hello %s, you have %d messages', 'John', 5)"
).getValue(context, String::class.java)
// 结果: "Hello John, you have 5 messages"

TIP

MethodHandle 方式在性能上通常比反射更优,特别是在高频调用场景下。

实际业务场景示例

场景1:订单状态验证函数

kotlin
// 定义订单状态枚举
enum class OrderStatus {
    PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}

// 定义订单数据类
data class Order(
    val id: String,
    val status: OrderStatus,
    val amount: Double,
    val createTime: LocalDateTime
)

// 业务函数:检查订单是否可以取消
fun canCancelOrder(order: Order): Boolean {
    val hoursSinceCreation = Duration.between(order.createTime, LocalDateTime.now()).toHours()
    return order.status in listOf(OrderStatus.PENDING, OrderStatus.PAID) && hoursSinceCreation < 24
}

// 业务函数:计算订单优先级
fun calculateOrderPriority(order: Order): Int {
    return when {
        order.amount > 1000 -> 1  // 高价值订单
        order.status == OrderStatus.PENDING -> 2  // 待处理订单
        else -> 3  // 普通订单
    }
}

场景2:在 Spring Boot 服务中使用

kotlin
@Service
class OrderService {
    
    private val spelParser = SpelExpressionParser()
    private val evaluationContext: EvaluationContext
    
    init {
        evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build()
        // 注册业务函数
        evaluationContext.setVariable("canCancel", ::canCancelOrder.javaMethod) 
        evaluationContext.setVariable("priority", ::calculateOrderPriority.javaMethod) 
    }
    
    fun processOrderWithRules(order: Order): String {
        // 设置根对象
        evaluationContext.setVariable("order", order)
        
        // 使用 SpEL 表达式进行业务判断
        val canCancel = spelParser.parseExpression("#canCancel(#order)")
            .getValue(evaluationContext, Boolean::class.java)
            
        val priority = spelParser.parseExpression("#priority(#order)")
            .getValue(evaluationContext, Int::class.java)
            
        return when {
            canCancel -> "订单可以取消,优先级: $priority"
            priority == 1 -> "高价值订单,需要特殊处理"
            else -> "普通订单处理流程"
        }
    }
}

场景3:配置驱动的业务规则

kotlin
@Component
class BusinessRuleEngine {
    
    private val spelParser = SpelExpressionParser()
    private val context: EvaluationContext
    
    init {
        context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
        registerBusinessFunctions()
    }
    
    private fun registerBusinessFunctions() {
        // 注册各种业务函数
        context.setVariable("isVip", ::isVipCustomer.javaMethod)
        context.setVariable("discount", ::calculateDiscount.javaMethod)
        context.setVariable("isWorkingDay", ::isWorkingDay.javaMethod)
    }
    
    // 业务函数:判断是否为VIP客户
    private fun isVipCustomer(customerId: String): Boolean {
        // 实际业务逻辑
        return customerId.startsWith("VIP")
    }
    
    // 业务函数:计算折扣
    private fun calculateDiscount(amount: Double, customerType: String): Double {
        return when (customerType) {
            "VIP" -> amount * 0.8
            "GOLD" -> amount * 0.9
            else -> amount
        }
    }
    
    // 业务函数:判断是否为工作日
    private fun isWorkingDay(date: LocalDate = LocalDate.now()): Boolean {
        return date.dayOfWeek !in listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
    }
    
    // 执行配置化的业务规则
    fun executeRule(ruleExpression: String, data: Map<String, Any>): Any? {
        // 设置数据上下文
        data.forEach { (key, value) -> 
            context.setVariable(key, value)
        }
        
        return spelParser.parseExpression(ruleExpression)
            .getValue(context)
    }
}

// 使用示例
@RestController
class OrderController(private val ruleEngine: BusinessRuleEngine) {
    
    @PostMapping("/calculate-price")
    fun calculatePrice(@RequestBody request: PriceRequest): PriceResponse {
        val data = mapOf(
            "amount" to request.amount,
            "customerId" to request.customerId,
            "customerType" to request.customerType
        )
        
        // 使用配置化的规则表达式
        val finalAmount = ruleEngine.executeRule(
            "#isVip(#customerId) ? #discount(#amount, #customerType) : #amount",
            data
        ) as Double
        
        return PriceResponse(finalAmount)
    }
}

函数调用流程图

注意事项与最佳实践

命名空间冲突

WARNING

函数与变量共享同一个命名空间,需要避免名称冲突。

kotlin
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// 错误示例:名称冲突
context.setVariable("data", "some value")  // 变量
context.setVariable("data", ::processData.javaMethod)  // 函数 - 会覆盖变量!

// 正确示例:使用不同的命名约定
context.setVariable("userData", "some value")  // 变量
context.setVariable("processData", ::processData.javaMethod)  // 函数

性能优化建议

性能优化

  1. 预绑定 MethodHandle:对于固定参数的函数,可以预先绑定提高性能
  2. 函数缓存:将常用函数的 Method 对象缓存起来
  3. 避免重复注册:在应用启动时一次性注册所有函数
kotlin
// 性能优化示例:预绑定 MethodHandle
class OptimizedFunctionRegistry {
    
    private val boundFunctions = mutableMapOf<String, MethodHandle>()
    
    fun registerPreBoundFunction(name: String, template: String, vararg args: Any) {
        val mh = MethodHandles.lookup()
            .findVirtual(String::class.java, "formatted", 
                MethodType.methodType(String::class.java, Array<Any>::class.java))
            .bindTo(template)
            .bindTo(args)
            
        boundFunctions[name] = mh
    }
    
    fun getContext(): EvaluationContext {
        val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
        boundFunctions.forEach { (name, handle) ->
            context.setVariable(name, handle)
        }
        return context
    }
}

总结

SpEL 自定义函数功能为我们提供了强大的表达式扩展能力:

灵活性:可以将任何 Java/Kotlin 方法注册为 SpEL 函数
性能:支持 MethodHandle 方式,提供更好的性能
可重用性:函数可以在多个表达式中重复使用
业务封装:将复杂业务逻辑封装为简洁的函数调用

通过合理使用自定义函数,我们可以构建出既强大又易维护的表达式系统,特别适用于规则引擎、配置驱动的业务逻辑等场景。

NOTE

在实际项目中,建议创建一个统一的函数注册中心,管理所有的自定义函数,这样既便于维护,也能避免命名冲突等问题。