bigywater.tech
Spring · 14

SpEL:表达式在哪些地方被求值,代价是什么

SpEL 能写什么、在哪里被求值?表达式里出错会是什么样?用户输入的表达式为什么危险?每次解析和复用解析结果差多少?这篇在 Spring Framework 7.0.9 上逐项验证。

约 13 倍
每次 parseExpression 与解析一次后反复求值的耗时比值(1315 对 96 ns,算出的比值)
EL1005E
受限的 SimpleEvaluationContext 下执行 T(java.lang.Runtime) 得到的错误
5 到 7 ns
编译模式(SpelCompilerMode.IMMEDIATE)每次求值的耗时

@Value 里的表达式

实测 · 本机执行输出
  upper=ALICE | adult=成年 | second=b | math+10=100 | lang=en | T(Math).max=7 | user?.friend?.name=null | ${missing:#{user.name}}=alice

可以引用 Bean、调用方法与静态方法,用 ?. 避免空指针,也能和 ${} 占位符组合。

语法与错误

实测 · 本机执行输出
  name                                           → alice
  name.length()                                  → 5
  age * 2 + 1                                    → 61
  tags.size() > 2                                → true
  tags[0] + tags[2]                              → ac
  score['math']                                  → 90
  hello('bob')                                   → hello bob from alice
  friend?.name                                   → null
  friend.name                                    → ✗ SpelEvaluationException: EL1007E: Property or field 'name' cannot be found on null
  new String('x').toUpperCase()                  → X
  T(java.lang.System).currentTimeMillis() > 0    → true
  tags.![toUpperCase()]                          → [A, B, C]
  tags.?[#this != 'b']                           → [a, c]
  'abc' matches '[a-c]+'                         → true
  #root.age                                      → 30
  secret                                         → ✗ SpelEvaluationException: EL1008E: Property or field 'secret' cannot be found on object of type 'demo.Main$User' - maybe not p…
  nope                                           → ✗ SpelEvaluationException: EL1008E: Property or field 'nope' cannot be found on object of type 'demo.Main$User' - maybe not pub…

集合投影与过滤、matches、三元;在 null 上取属性、取不到的字段都有明确的错误码。

求值上下文

实测 · 本机执行输出
  StandardEvaluationContext 下 T(java.lang.Runtime).getRuntime().availableProcessors() → 10(能调任意类的静态方法)
  SimpleEvaluationContext.forReadOnlyDataBinding() 下同一个表达式 → ✗ SpelEvaluationException: EL1005E: Type cannot be found 'java.lang.Runtime'
  SimpleEvaluationContext 读 name → alice
  只读上下文里赋值 name = 'x' → ✗ SpelEvaluationException: EL1068E: The expression component 'name='x'' is not assignable

Standard 上下文能访问任意类型,Simple 上下文把类型和写入都限制住。

成本

实测 · 本机执行输出
  每次 parseExpression 加求值:1315 ns/次;解析一次后反复求值:96 ns/次;编译模式(IMMEDIATE):6.5 ns/次;直接写 Java:1.99 ns/次(s=61000000 防止被优化掉)
  每次 parseExpression 加求值:1227 ns/次;解析一次后反复求值:96 ns/次;编译模式(IMMEDIATE):5.0 ns/次;直接写 Java:2.01 ns/次(s=61000000 防止被优化掉)
  每次 parseExpression 加求值:1219 ns/次;解析一次后反复求值:96 ns/次;编译模式(IMMEDIATE):6.6 ns/次;直接写 Java:1.73 ns/次(s=61000000 防止被优化掉)

解析是大头,解析一次复用能省一个数量级,编译模式再省一个数量级。

分步演示

SpEL 表达式
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用不带 Boot 的 Spring Framework 7.0.9;耗时是简单循环计时(预热之后),没有用 JMH,直接写 Java 的那一项在循环里可能被 JIT 部分优化,只能当量级参考;availableProcessors 返回的 10 是这台机器的核数
    • 没有验证:SpEL 在 @Cacheable、@PreAuthorize、@ConditionalOnExpression 里的使用;MethodBasedEvaluationContext;自定义函数与 PropertyAccessor;编译模式对非 public 类的限制;SpEL 在 AOT 下的支持;表达式注入的实际利用方式(只验证了受限上下文会拒绝)
    京ICP备2026067502号-1京公网安备11010502063487号