Skip to content

变量 (Variables)

你可以在表达式中使用 #variableName 语法来引用变量。变量是通过 EvaluationContext 实现类中的 setVariable() 方法设置的。

变量命名规则

  • 变量名必须以字母(定义如下)、下划线(_)或美元符号($)开头。
  • 变量名必须由一个或多个受支持的字符组成:
    • 字母:任何 java.lang.Character.isLetter(char) 返回 true 的字符。
      • 这包括 AZazüñé 等字母,以及来自其他字符集(如中文、日文、西里尔文等)的字母。
    • 数字09
    • 下划线_
    • 美元符号$

建议

EvaluationContext 中设置变量或根上下文对象时,建议将变量或根上下文对象的类型设置为 public

否则,涉及非 public 类型的变量或根对象的某些 SpEL 表达式可能会在评估或编译时失败。

警告

由于变量与评估上下文中的函数共享公共命名空间,因此必须小心确保变量名和函数名不会发生冲突。

以下示例演示了如何使用变量:

java
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
// 设置变量 "newName"
context.setVariable("newName", "Mike Tesla");

// 使用 #newName 引用变量并赋值给 name 属性
parser.parseExpression("name = #newName").getValue(context, tesla);
System.out.println(tesla.getName());  // "Mike Tesla"
kotlin
val tesla = Inventor("Nikola Tesla", "Serbian")

val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
// 设置变量 "newName"
context.setVariable("newName", "Mike Tesla")

// 使用 #newName 引用变量
parser.parseExpression("name = #newName").getValue(context, tesla)
println(tesla.name)  // "Mike Tesla"

#this 和 #root 变量

#this 变量始终有定义,它指向当前的评估对象(用于解析非限定引用的对象)。#root 变量也始终有定义,它指向根上下文对象。虽然 #this 可能会随着表达式组件的评估而改变,但 #root 始终指向根对象。

以下示例展示了如何结合集合选择 (Collection Selection)使用 #this 变量。

java
// 创建质数列表
List<Integer> primes = List.of(2, 3, 5, 7, 11, 13, 17);

// 创建解析器并设置变量 'primes'
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("primes", primes);

// 从列表中选择所有大于 10 的质数(使用选择语法 .?[...])
// 在选择闭包内部,#this 指向当前遍历的元素
String expression = "#primes.?[#this > 10]";

// 评估结果为包含 [11, 13, 17] 的列表
List<Integer> primesGreaterThanTen =
		parser.parseExpression(expression).getValue(context, List.class);
kotlin
// 创建质数列表
val primes = listOf(2, 3, 5, 7, 11, 13, 17)

// 创建解析器并设置变量 'primes'
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
context.setVariable("primes", primes)

// 选择所有大于 10 的质数
val expression = "#primes.?[#this > 10]"

// 评估结果为列表 [11, 13, 17]
val primesGreaterThanTen = parser.parseExpression(expression)
		.getValue(context) as List<Int>

以下示例展示了如何结合集合投影 (Collection Projection)同时使用 #this#root 变量。

java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

// 创建一个发明家作为根上下文对象
Inventor tesla = new Inventor("Nikola Tesla");
tesla.setInventions("Telephone repeater", "Tesla coil transformer");

// 遍历根对象(#root)的所有发明(inventions),
// 生成格式为 "<发明家姓名> invented the <发明名称>." 的字符串列表。
// 在投影闭包内部:
// #root 指向 tesla 对象
// #this 指向当前正在处理的单个发明字符串
String expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']";

// 评估结果列表包含:
// "Nikola Tesla invented the Telephone repeater."
// "Nikola Tesla invented the Tesla coil transformer."
List<String> results = parser.parseExpression(expression)
		.getValue(context, tesla, List.class);
kotlin
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()

val tesla = Inventor("Nikola Tesla")
tesla.setInventions("Telephone repeater", "Tesla coil transformer")

// 使用投影语法 ![...]
val expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']"

val results = parser.parseExpression(expression)
		.getValue(context, tesla, List::class.java)

补充教学

1. #this vs #root:深度理解

这是 SpEL 中最核心的概念之一,容易混淆。你可以把它们类比为:

  • #root:类似于整个程序的“全局根节点”,无论你进入多深的嵌套结构,它永远不变。
  • #this:类似于“当前上下文句柄”。当你进入一个集合的某种操作(如筛选 ?[] 或投影 ![])时,#this 会自动切换为当前正在处理的那个元素。

2. 为什么变量名要加 #

这是为了区分 属性访问变量访问

  • name:SpEL 会去根对象中寻找名为 name 的属性(调用 getName())。
  • #name:SpEL 会去 EvaluationContext 的变量池中寻找名为 name 的变量。 这种明确的区分让表达式在处理复杂逻辑时不会产生歧义。

3. 多语言支持的魅力

正如文档提到的,SpEL 支持 Unicode 变量名。虽然在生产环境中不建议过度使用,但在某些本地化场景下,它允许你写出非常易读的代码:

java
context.setVariable("用户名", "张三");
parser.parseExpression("'你好,' + #用户名").getValue(context); // 你好,张三

4. 共享命名空间带来的冲突风险

由于变量和自定义函数(见下一章)共享同一个存储空间,如果你注册了一个名为 check 的函数,又设置了一个名为 #check 的变量,后设置的内容可能会覆盖前者。 最佳实践

  • 函数建议使用动词开头(如 #isAdult)。
  • 变量建议使用名词(如 #user)。

5. 常见预定义变量

除了 #this#root,在 Spring 的不同子项目中,框架会为你预定义很多有用的变量:

  • Spring Security:提供 #principal(当前用户)、#authentication(认证信息)。
  • Spring Data:提供 #entityName(实体名)等。 了解这些预定义变量能让你在配置这些框架时如鱼得水。

Based on Spring Framework.