Appearance
Spring Expression Language (SpEL) - Functions 自定义函数详解 ⚙️
概述
Spring Expression Language (SpEL) 提供了强大的自定义函数功能,允许开发者扩展表达式的能力。通过注册用户定义的函数,我们可以在 SpEL 表达式中调用自定义的业务逻辑,使表达式更加灵活和强大。
IMPORTANT
自定义函数是 SpEL 的核心扩展机制之一,它让我们能够将复杂的业务逻辑封装成可重用的函数,在表达式中直接调用。
为什么需要自定义函数? 🤔
在实际开发中,我们经常遇到以下场景:
- 复杂的数据处理逻辑:需要在表达式中进行复杂的字符串处理、数学计算等
- 业务规则封装:将特定的业务规则封装成函数,便于复用
- 第三方库集成:将第三方库的功能暴露给 SpEL 表达式使用
- 性能优化:将重复的计算逻辑提取为函数,避免重复执行
设计哲学
SpEL 自定义函数的设计哲学是"可扩展性"和"简洁性"的完美结合。它允许开发者在保持表达式简洁的同时,扩展其功能边界。
函数注册机制
基本注册方式
自定义函数通过 EvaluationContext
的 setVariable()
方法注册:
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) // 函数
性能优化建议
性能优化
- 预绑定 MethodHandle:对于固定参数的函数,可以预先绑定提高性能
- 函数缓存:将常用函数的 Method 对象缓存起来
- 避免重复注册:在应用启动时一次性注册所有函数
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
在实际项目中,建议创建一个统一的函数注册中心,管理所有的自定义函数,这样既便于维护,也能避免命名冲突等问题。