Skip to content

Spring Expression Language (SpEL) 表达式模板:让字符串"活"起来 🎭

什么是表达式模板?

想象一下,你正在写一个邮件模板系统。传统的做法是这样的:

kotlin
// 传统方式:字符串拼接的噩梦
val userName = "张三"
val currentTime = Date()
val message = "亲爱的 " + userName + ",当前时间是 " + currentTime + ",欢迎使用我们的系统!"

这种方式不仅代码冗长,而且难以维护。如果模板复杂一点,代码就会变得非常混乱。

表达式模板(Expression Templating) 就是为了解决这个痛点而生的!它允许我们在普通的文本中嵌入可执行的表达式,让字符串变得"智能"起来。

核心理念

表达式模板的设计哲学是:将静态文本与动态计算完美融合,让开发者能够用更自然、更直观的方式构建动态字符串。

表达式模板的工作原理

表达式模板通过特定的分隔符(默认是 #{ })来标识需要计算的表达式部分:

基础用法示例

让我们从最简单的例子开始:

kotlin
import org.springframework.expression.ExpressionParser
import org.springframework.expression.spel.standard.SpelExpressionParser
import org.springframework.expression.common.TemplateParserContext

// 创建SpEL解析器
val parser: ExpressionParser = SpelExpressionParser()

// 使用表达式模板生成随机数消息
val randomPhrase = parser.parseExpression(
    "random number is #{T(java.lang.Math).random()}", 
    TemplateParserContext() 
).getValue(String::class.java)

println(randomPhrase)
// 输出: "random number is 0.7038186818312008"
kotlin
// 传统方式:代码冗长且不直观
val randomNumber = Math.random()
val traditionalPhrase = "random number is $randomNumber"

// 表达式模板方式:更简洁、更强大
val templatePhrase = parser.parseExpression(
    "random number is #{T(java.lang.Math).random()}",
    TemplateParserContext()
).getValue(String::class.java)

关键组件说明

  • ExpressionParser: SpEL表达式解析器的核心接口
  • TemplateParserContext: 模板解析上下文,定义了分隔符规则
  • 分隔符 #{ }: 默认的表达式边界标识符

实际业务场景应用

1. 动态配置消息模板

在实际的SpringBoot项目中,表达式模板特别适用于配置化的消息系统:

kotlin
@Service
class NotificationService {
    
    private val parser = SpelExpressionParser()
    
    // 配置化的消息模板
    private val templates = mapOf(
        "welcome" to "欢迎 #{user.name}!您的账户余额是 #{user.balance} 元",
        "reminder" to "亲爱的 #{user.name},您有 #{tasks.size()} 个待办任务",
        "system" to "系统时间:#{T(java.time.LocalDateTime).now()},在线用户:#{onlineCount}"
    )
    
    fun generateMessage(templateKey: String, context: Map<String, Any>): String {
        val template = templates[templateKey] 
            ?: throw IllegalArgumentException("模板不存在: $templateKey")
        
        // 创建SpEL上下文
        val spelContext = StandardEvaluationContext().apply {
            context.forEach { (key, value) -> setVariable(key, value) }
        }
        
        return parser.parseExpression(template, TemplateParserContext())
            .getValue(spelContext, String::class.java) ?: ""
    }
}

2. 动态SQL构建场景

kotlin
@Repository
class DynamicQueryRepository {
    
    private val parser = SpelExpressionParser()
    
    fun buildQuery(conditions: QueryConditions): String {
        val queryTemplate = """
            SELECT * FROM users 
            WHERE status = 'ACTIVE'
            #{conditions.hasName() ? ' AND name LIKE \'%' + conditions.name + '%\'' : ''}
            #{conditions.hasAge() ? ' AND age >= ' + conditions.minAge : ''}
            #{conditions.hasCity() ? ' AND city = \'' + conditions.city + '\'' : ''}
            ORDER BY created_date DESC
        """.trimIndent()
        
        val context = StandardEvaluationContext(conditions)
        
        return parser.parseExpression(queryTemplate, TemplateParserContext())
            .getValue(context, String::class.java) ?: ""
    }
}

data class QueryConditions(
    val name: String? = null,
    val minAge: Int? = null,
    val city: String? = null
) {
    fun hasName() = !name.isNullOrBlank()
    fun hasAge() = minAge != null
    fun hasCity() = !city.isNullOrBlank()
}

自定义分隔符

有时候默认的 #{ } 分隔符可能与你的文本内容冲突,SpEL允许你自定义分隔符:

kotlin
@Component
class CustomTemplateService {
    
    private val parser = SpelExpressionParser()
    
    // 自定义分隔符的解析上下文
    class CustomParserContext : ParserContext {
        override fun isTemplate() = true
        override fun getExpressionPrefix() = "{{"
        override fun getExpressionSuffix() = "}}"
    }
    
    fun processTemplate(template: String, variables: Map<String, Any>): String {
        val context = StandardEvaluationContext().apply {
            variables.forEach { (key, value) -> setVariable(key, value) }
        }
        
        // 使用自定义分隔符 {{ }}
        val expression = parser.parseExpression(template, CustomParserContext())
        return expression.getValue(context, String::class.java) ?: ""
    }
}

// 使用示例
fun example() {
    val service = CustomTemplateService()
    val result = service.processTemplate(
        "Hello {{#name}}! Today is {{T(java.time.LocalDate).now()}}",
        mapOf("name" to "Alice")
    )
    println(result) // Hello Alice! Today is 2024-01-15
}

高级特性:表达式模板的强大之处

1. 条件渲染

kotlin
@Service
class ConditionalTemplateService {
    
    private val parser = SpelExpressionParser()
    
    fun generateUserGreeting(user: User): String {
        val template = """
            #{user.isVip ? '尊敬的VIP用户' : '亲爱的用户'} #{user.name},
            #{user.unreadMessages > 0 ? '您有 ' + user.unreadMessages + ' 条未读消息' : '暂无新消息'}
            #{user.points >= 1000 ? ',恭喜您成为金牌会员!' : ''}
        """.trimIndent()
        
        val context = StandardEvaluationContext(user)
        return parser.parseExpression(template, TemplateParserContext())
            .getValue(context, String::class.java) ?: ""
    }
}

data class User(
    val name: String,
    val isVip: Boolean,
    val unreadMessages: Int,
    val points: Int
)

2. 集合操作

kotlin
fun generateSummaryReport(orders: List<Order>): String {
    val template = """
        订单统计报告:
        - 总订单数:#{orders.size()}
        - 总金额:#{orders.![amount].sum()} 元
        - 平均金额:#{orders.![amount].sum() / orders.size()} 元
        - 最高金额:#{orders.![amount].max()} 元
        - VIP订单:#{orders.?[customer.isVip].size()} 个
    """.trimIndent()
    
    val context = StandardEvaluationContext().apply {
        setVariable("orders", orders)
    }
    
    return parser.parseExpression(template, TemplateParserContext())
        .getValue(context, String::class.java) ?: ""
}

性能优化建议

性能注意事项

表达式解析是有性能开销的,在高并发场景下需要注意优化。

kotlin
@Service
class OptimizedTemplateService {
    
    private val parser = SpelExpressionParser()
    // 缓存已解析的表达式
    private val expressionCache = ConcurrentHashMap<String, Expression>() 
    
    fun processTemplate(template: String, context: Map<String, Any>): String {
        // 从缓存获取或解析表达式
        val expression = expressionCache.computeIfAbsent(template) { 
            parser.parseExpression(it, TemplateParserContext()) 
        } 
        
        val spelContext = StandardEvaluationContext().apply {
            context.forEach { (key, value) -> setVariable(key, value) }
        }
        
        return expression.getValue(spelContext, String::class.java) ?: ""
    }
}

错误处理最佳实践

kotlin
@Service
class SafeTemplateService {
    
    private val parser = SpelExpressionParser()
    private val logger = LoggerFactory.getLogger(SafeTemplateService::class.java)
    
    fun safeProcessTemplate(template: String, context: Map<String, Any>): String {
        return try {
            val expression = parser.parseExpression(template, TemplateParserContext())
            val spelContext = StandardEvaluationContext().apply {
                context.forEach { (key, value) -> setVariable(key, value) }
            }
            
            expression.getValue(spelContext, String::class.java) ?: template
            
        } catch (ex: ParseException) {
            logger.error("模板解析失败: $template", ex) 
            template // 返回原始模板作为降级方案
        } catch (ex: EvaluationException) {
            logger.error("表达式计算失败: $template", ex) 
            template // 返回原始模板作为降级方案
        }
    }
}

总结

SpEL表达式模板是Spring框架中一个非常实用的特性,它解决了以下核心问题:

字符串拼接的复杂性 - 用声明式的方式替代命令式的字符串操作
模板的可维护性 - 将逻辑和展示分离,提高代码可读性
动态内容生成 - 支持条件渲染、集合操作等复杂逻辑
配置化的灵活性 - 可以将模板外部化,支持运行时修改

关键要点

表达式模板不仅仅是字符串拼接的语法糖,它是一个强大的文本生成引擎,能够处理复杂的业务逻辑,让你的代码更加优雅和可维护。

在实际项目中,合理使用表达式模板可以大大简化动态文本生成的复杂度,特别是在消息推送、报表生成、配置管理等场景中,它的价值尤为突出。记住要注意性能优化和错误处理,让你的应用既强大又稳定! 🚀