Skip to content

属性、数组、列表、Map 和索引器 (Properties, Arrays, Lists, Maps, and Indexers)

Spring 表达式语言支持导航对象图以及对各种结构进行索引操作。

提示

数值索引值是从 0 开始的,就像在 Java 中访问数组的第 n 个元素一样。

建议

有关如何使用空安全操作符导航对象图和索引各种结构的详细信息,请参阅安全导航操作符部分。

属性导航 (Property Navigation)

你可以通过使用点(.)来表示嵌套的属性值,从而在对象图中导航属性引用。Inventor 类的实例 pupintesla 已使用示例中使用的类部分列出的数据进行了填充。为了向下导航对象图并获取 Tesla 的出生年份和 Pupin 的出生城市,我们使用以下表达式:

java
// 评估结果为 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);

// 评估结果为 "Smiljan"
String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);
kotlin
// 评估结果为 1856
val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int

// 评估结果为 "Smiljan"
val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String

注意

属性名称的首字母允许不区分大小写。因此,上述示例中的表达式可以分别写为 Birthdate.Year + 1900PlaceOfBirth.City。此外,属性可以选择通过方法调用来访问——例如,使用 getPlaceOfBirth().getCity() 代替 placeOfBirth.city

对数组和集合进行索引 (Indexing into Arrays and Collections)

可以使用方括号标记法获取数组或集合(例如 SetList)的第 n 个元素,如下例所示。

说明

  • 如果被索引的集合是 java.util.List,则将通过 list.get(n) 直接访问第 n 个元素。
  • 对于任何其他类型的 Collection,将通过使用其 Iterator 遍历集合并返回遇到的第 n 个元素来访问。
java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions 数组
// 评估结果为 "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
		context, tesla, String.class);

// Members 列表
// 评估结果为 "Nikola Tesla"
String name = parser.parseExpression("members[0].name").getValue(
		context, ieee, String.class);

// 列表和数组混合索引
// 评估结果为 "Wireless communication"
String invention = parser.parseExpression("members[0].inventions[6]").getValue(
		context, ieee, String.class);
kotlin
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// Inventions 数组
// 评估结果为 "Induction motor"
val invention = parser.parseExpression("inventions[3]").getValue(
		context, tesla, String::class.java)

// Members 列表
// 评估结果为 "Nikola Tesla"
val name = parser.parseExpression("members[0].name").getValue(
		context, ieee, String::class.java)

// 列表和数组混合索引
// 评估结果为 "Wireless communication"
val invention = parser.parseExpression("members[0].inventions[6]").getValue(
		context, ieee, String::class.java)

对字符串进行索引 (Indexing into Strings)

可以通过在方括号内指定索引来获取字符串的第 n 个字符,如下例所示。

注意

字符串的第 n 个字符的评估结果将是一个 java.lang.String,而不是 java.lang.Character

java
// 评估结果为 "T" ("Nikola Tesla" 的第 8 个字母)
String character = parser.parseExpression("members[0].name[7]")
		.getValue(societyContext, String.class);
kotlin
// 评估结果为 "T" ("Nikola Tesla" 的第 8 个字母)
val character = parser.parseExpression("members[0].name[7]")
		.getValue(societyContext, String::class.java)

对 Map 进行索引 (Indexing into Maps)

通过在方括号内指定键值来获取 Map 的内容。在下例中,由于 officers Map 的键是字符串,我们可以指定字符串字面量(如 'president'):

java
// Officer's Map
// 评估结果为 Inventor("Pupin")
Inventor pupin = parser.parseExpression("officers['president']")
		.getValue(societyContext, Inventor.class);

// 评估结果为 "Idvor"
String city = parser.parseExpression("officers['president'].placeOfBirth.city")
		.getValue(societyContext, String.class);

String countryExpression = "officers['advisors'][0].placeOfBirth.country";

// 设置值
parser.parseExpression(countryExpression)
		.setValue(societyContext, "Croatia");

// 评估结果为 "Croatia"
String country = parser.parseExpression(countryExpression)
		.getValue(societyContext, String.class);
kotlin
// Officer's Map
// 评估结果为 Inventor("Pupin")
val pupin = parser.parseExpression("officers['president']")
		.getValue(societyContext, Inventor::class.java)

// 评估结果为 "Idvor"
val city = parser.parseExpression("officers['president'].placeOfBirth.city")
		.getValue(societyContext, String::class.java)

val countryExpression = "officers['advisors'][0].placeOfBirth.country"

// 设置值
parser.parseExpression(countryExpression)
		.setValue(societyContext, "Croatia")

// 评估结果为 "Croatia"
val country = parser.parseExpression(countryExpression)
		.getValue(societyContext, String::class.java)

对对象进行索引 (Indexing into Objects)

可以通过在方括号内指定属性名称来获取对象的属性。这类似于基于键访问 Map 的值。下例演示了如何对对象进行“索引”以检索特定属性。

java
// 创建一个发明家作为根上下文对象。
Inventor tesla = new Inventor("Nikola Tesla");

// 评估结果为 "Nikola Tesla"
String name = parser.parseExpression("#root['name']")
		.getValue(context, tesla, String.class);
kotlin
// 创建一个发明家作为根上下文对象。
val tesla = Inventor("Nikola Tesla")

// 评估结果为 "Nikola Tesla"
val name = parser.parseExpression("#root['name']")
		.getValue(context, tesla, String::class.java)

对自定义结构进行索引 (Indexing into Custom Structures)

从 Spring Framework 6.2 开始,SpEL 支持通过允许开发者实现并向 EvaluationContext 注册 IndexAccessor 来对自定义结构进行索引。如果你希望支持依赖于自定义索引访问器的表达式编译,该索引访问器必须实现 CompilableIndexAccessor SPI。

为了支持常见用例,Spring 提供了一个内置的 ReflectiveIndexAccessor。这是一个灵活的 IndexAccessor,它使用反射来读取(以及可选地写入)目标对象的索引结构。索引结构可以通过 public 读取方法(读取时)或 public 写入方法(写入时)进行访问。读取方法和写入方法之间的关系基于适用于索引结构典型实现的约定。

说明

ReflectiveIndexAccessor 还实现了 CompilableIndexAccessor,以支持针对读取访问编译为字节码。但请注意,配置的读取方法必须通过 public 类或 public 接口可调用,编译才能成功。

以下代码定义了一个 Color 枚举和 FruitMap 类型,其行为类似于映射,但未实现 java.util.Map 接口。因此,如果你想在 SpEL 表达式中对 FruitMap 进行索引,你需要注册一个 IndexAccessor

java
public enum Color {
	RED, ORANGE, YELLOW
}

public class FruitMap {

	private final Map<Color, String> map = new HashMap<>();

	public FruitMap() {
		this.map.put(Color.RED, "cherry");
		this.map.put(Color.ORANGE, "orange");
		this.map.put(Color.YELLOW, "banana");
	}

	public String getFruit(Color color) {
		return this.map.get(color);
	}

	public void setFruit(Color color, String fruit) {
		this.map.put(color, fruit);
	}
}

可以通过 new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit")FruitMap 创建只读的 IndexAccessor。注册该访问器并将 FruitMap 注册为名为 #fruitMap 的变量后,SpEL 表达式 #fruitMap[T(example.Color).RED] 的评估结果将为 "cherry"

可以通过 new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")FruitMap 创建读写 IndexAccessor。注册该访问器并将 FruitMap 注册为名为 #fruitMap 的变量后,可以使用 SpEL 表达式 #fruitMap[T(example.Color).RED] = 'strawberry' 将红色对应的水果映射从 "cherry" 更改为 "strawberry"

下例演示了如何注册 ReflectiveIndexAccessor 以对 FruitMap 进行索引。

java
// 为 FruitMap 创建一个 ReflectiveIndexAccessor
IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor(
		FruitMap.class, Color.class, "getFruit", "setFruit");

// 为 FruitMap 注册 IndexAccessor
context.addIndexAccessor(fruitMapAccessor);

// 注册 fruitMap 变量
context.setVariable("fruitMap", new FruitMap());

// 评估结果为 "cherry"
String fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
		.getValue(context, String.class);
kotlin
// 为 FruitMap 创建一个 ReflectiveIndexAccessor
val fruitMapAccessor = ReflectiveIndexAccessor(
		FruitMap::class.java, Color::class.java, "getFruit", "setFruit")

// 为 FruitMap 注册 IndexAccessor
context.addIndexAccessor(fruitMapAccessor)

// 注册 fruitMap 变量
context.setVariable("fruitMap", FruitMap())

// 评估结果为 "cherry"
val fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
	.getValue(context, String::class.java)

补充教学

1. 属性访问的灵活性

SpEL 在属性导航上非常宽容:

  • 首字母大小写不敏感:这主要是为了兼容不同风格的 JavaBean 命名规范(虽然现代开发大都遵循 camelCase)。
  • 字段 vs 方法:SpEL 会首先尝试查找 getter 方法(如 getCity()),如果找不到,再尝试直接访问公共字段。这种机制对于开发者来说是透明且友好的。

3. Map 索引的常见陷阱

在对 Map 进行索引时,一定要注意键的类型:

  • 字符串键:必须加引号,如 officers['president']。如果漏掉引号写成 officers[president],SpEL 会尝试寻找一个名为 president 的变量。
  • 数字键:如果是 Map<Integer, Value>,应直接写 map[123]
  • 动态键:你可以结合变量使用,如 officers[#dynamicKey]

3. Spring 6.2 的重大增强:IndexAccessor

在以前的版本中,如果你有一个类似 Map 的自定义类(比如 MessageContext 或特殊的 Registry),你想用 obj['key'] 这种语法访问它是行不通的,你只能调用 obj.get('key')

  • 语法糖IndexAccessor 的引入让自定义结构也能享受和原生 Map 一样的快捷语法。
  • 应用场景:这对构建领域特定语言 (DSL) 或处理高度抽象的配置对象非常有用。它让表达式更简洁,更符合人类阅读习惯。

4. 性能与编译

既然提到了 ReflectiveIndexAccessor,就不得不提它的性能: 通过反射访问通常比直接代码调用慢。但是,因为它实现了 CompilableIndexAccessor,Spring 的编译器可以在“通过解释执行摸清规律”后,将该反射访问生成为直接的方法调用代码并缓存为字节码。在高性能要求的循环处理中,这是提升 SpEL 运行效率的关键。

Based on Spring Framework.