评估

本节介绍了SpEL接口及其表达式语言的编程用法。完整的语言参考可以在语言参考中找到。

以下代码演示了如何使用SpEL API来评估字面字符串表达式Hello World

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 消息变量的值为"Hello World"
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
1 消息变量的值为"Hello World"

您最可能使用的SpEL类和接口位于org.springframework.expression包及其子包中,例如spel.support

ExpressionParser接口负责解析表达式字符串。在上面的示例中,表达式字符串是由单引号括起来的字符串字面量。Expression接口负责评估定义的表达式字符串。调用parser.parseExpression(…​)exp.getValue(…​)时可能抛出的两种异常类型分别是ParseExceptionEvaluationException

SpEL支持许多功能,例如调用方法、访问属性和调用构造函数。

在以下方法调用示例中,我们在字符串字面量Hello World上调用concat方法。

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
1 message的值现在是"Hello World!"
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") (1)
val message = exp.value as String
1 message的值现在是"Hello World!"

以下示例演示了如何访问字符串字面量Hello WorldBytes JavaBean属性。

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();

// 调用'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
1 此行将字面量转换为字节数组。
val parser = SpelExpressionParser()

// 调用'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") (1)
val bytes = exp.value as ByteArray
1 此行将字面量转换为字节数组。

SpEL还支持使用标准点表示法(例如prop1.prop2.prop3)访问嵌套属性以及相应的设置属性值。还可以访问公共字段。

以下示例展示了如何使用点表示法获取字符串字面量的长度。

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();

// 调用'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
1 'Hello World'.bytes.length给出了字面量的长度。
val parser = SpelExpressionParser()

// 调用'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") (1)
val length = exp.value as Int
1 'Hello World'.bytes.length给出了字面量的长度。

可以调用字符串的构造函数,而不是使用字符串字面量,如下例所示。

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
1 从字面量构造一个新的String并将其转换为大写。
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()")  (1)
val message = exp.getValue(String::class.java)
1 从字面量构造一个新的String并将其转换为大写。

请注意使用泛型方法:public <T> T getValue(Class<T> desiredResultType)。使用此方法可以避免将表达式的值强制转换为所需的结果类型。如果值无法转换为类型T或通过注册的类型转换器进行转换,则会抛出EvaluationException

SpEL的更常见用法是提供针对特定对象实例(称为根对象)评估的表达式字符串。以下示例显示了如何从Inventor类的实例中检索name属性以及如何在布尔表达式中引用name属性。

  • Java

  • Kotlin

// 创建并设置日历
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// 构造函数参数分别为姓名、生日和国籍。
Inventor tesla = new Inventor("尼古拉·特斯拉", c.getTime(), "塞尔维亚人");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); // 将姓名解析为表达式
String name = (String) exp.getValue(tesla);
// name == "尼古拉·特斯拉"

exp = parser.parseExpression("name == '尼古拉·特斯拉'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
// 创建并设置日历
val c = GregorianCalendar()
c.set(1856, 7, 9)

// 构造函数参数分别为姓名、生日和国籍。
val tesla = Inventor("尼古拉·特斯拉", c.time, "塞尔维亚人")

val parser = SpelExpressionParser()

var exp = parser.parseExpression("name") // 将姓名解析为表达式
val name = exp.getValue(tesla) as String
// name == "尼古拉·特斯拉"

exp = parser.parseExpression("name == '尼古拉·特斯拉'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true

理解 EvaluationContext

EvaluationContext 接口用于在评估表达式时解析属性、方法或字段,并帮助执行类型转换。Spring 提供了两种实现。

  • SimpleEvaluationContext:公开了一组基本的 SpEL 语言特性和配置选项,适用于不需要 SpEL 语言语法的全部功能并且应该有意义地受限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。

  • StandardEvaluationContext:公开了完整的 SpEL 语言特性和配置选项。您可以使用它指定默认根对象,并配置每个可用的与评估相关的策略。

SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用、构造函数和 bean 引用。它还要求您明确选择表达式中属性和方法的支持级别。默认情况下,create() 静态工厂方法仅启用对属性的读访问。您还可以获取一个构建器来配置所需的支持级别,针对以下一个或多个组合。

  • 仅自定义 PropertyAccessor(无反射)

  • 用于只读访问的数据绑定属性

  • 用于读写的数据绑定属性

类型转换

默认情况下,SpEL 使用 Spring 核心中可用的转换服务(org.springframework.core.convert.ConversionService)。该转换服务提供了许多内置的转换器用于常见的转换,但也是完全可扩展的,以便您可以添加类型之间的自定义转换。此外,它是泛型感知的。这意味着,在表达式中使用泛型类型时,SpEL 会尝试进行转换,以保持任何遇到的对象的类型正确性。

这在实践中意味着什么?假设正在使用 setValue() 进行赋值来设置一个 List 属性。该属性的类型实际上是 List<Boolean>。SpEL 会意识到需要将列表的元素转换为 Boolean,然后再放入其中。以下示例展示了如何实现这一点。

  • Java

  • Kotlin

class Simple {
	public List<Boolean> booleanList = new ArrayList<>();
}

Simple simple = new Simple();
simple.booleanList.add(true);

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// 这里传入的是 "false" 字符串。SpEL 和转换服务
// 会识别需要将其转换为 Boolean 并相应地进行转换。
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b 现在是 false
Boolean b = simple.booleanList.get(0);
class Simple {
	var booleanList: MutableList<Boolean> = ArrayList()
}

val simple = Simple()
simple.booleanList.add(true)

val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

// 这里传入的是 "false" 字符串。SpEL 和转换服务
// 会识别需要将其转换为 Boolean 并相应地进行转换。
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")

// b 现在是 false
val b = simple.booleanList[0]

解析器配置

可以通过使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)来配置 SpEL 表达式解析器。配置对象控制一些表达式组件的行为。例如,如果您索引到数组或集合,并且指定索引处的元素为 null,SpEL 可以自动创建该元素。这在使用由一系列属性引用组成的表达式时非常有用。如果您索引到数组或列表,并指定超出当前数组或列表大小的索引,SpEL 可以自动增长数组或列表以容纳该索引。为了在设置指定索引处的元素之前在数组或列表中添加一个元素,SpEL 将尝试使用元素类型的默认构造函数创建元素,然后设置指定的值。如果元素类型没有默认构造函数,则将在数组或列表中添加 null。如果没有内置或自定义转换器知道如何设置值,则 null 将保留在数组或列表中的指定索引处。以下示例演示了如何自动增长列表。

  • Java

  • Kotlin

class Demo {
	public List<String> list;
}

// 打开:
// - 自动空引用初始化
// - 自动集合增长
SpelParserConfiguration config = new SpelParserConfiguration(true, true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression("list[3]");

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.list 现在将是一个包含 4 个条目的真实集合
// 每个条目都是一个新的空字符串
class Demo {
	var list: List<String>? = null
}

// 打开:
// - 自动空引用初始化
// - 自动集合增长
val config = SpelParserConfiguration(true, true)

val parser = SpelExpressionParser(config)

val expression = parser.parseExpression("list[3]")

val demo = Demo()

val o = expression.getValue(demo)

// demo.list 现在将是一个包含 4 个条目的真实集合
// 每个条目都是一个新的空字符串

默认情况下,SpEL 表达式不能包含超过 10,000 个字符;但是,maxExpressionLength 是可配置的。如果您以编程方式创建 SpelExpressionParser,可以在创建提供给 SpelExpressionParserSpelParserConfiguration 中指定自定义的 maxExpressionLength。如果您希望设置在 ApplicationContext 中解析 SpEL 表达式时使用的 maxExpressionLength,例如在 XML bean 定义、@Value 等中,您可以设置一个 JVM 系统属性或名为 spring.context.expression.maxLength 的 Spring 属性,以设置应用程序所需的最大表达式长度(请参阅支持的 Spring 属性)。

SpEL编译

Spring为SpEL表达式提供了一个基本的编译器。通常情况下,表达式是解释执行的,这在评估过程中提供了很多动态灵活性,但并不提供最佳性能。对于偶尔使用表达式的情况,这是可以接受的,但是当被Spring Integration等其他组件使用时,性能就变得非常重要,而且没有真正需要动态性。

SpEL编译器旨在解决这个需求。在评估过程中,编译器生成一个Java类,该类在运行时体现表达式行为,并使用该类实现更快的表达式评估。由于表达式周围缺乏类型信息,编译器在执行编译时使用了在解释执行表达式期间收集的信息。例如,它无法仅从表达式中知道属性引用的类型,但在第一次解释执行期间,它会找出它的类型。当然,基于这种衍生信息进行编译可能会在以后引起问题,如果各种表达式元素的类型随时间改变。因此,编译最适合于其类型信息在重复评估时不会改变的表达式。

考虑以下基本表达式。

someArray[0].someProperty.someOtherProperty < 0.1

由于前面的表达式涉及数组访问、某些属性解引用和数值操作,性能提升可能非常明显。在一个包含50,000次迭代的示例微基准测试中,使用解释器评估需要75毫秒,而使用编译版本的表达式仅需要3毫秒。

编译器配置

编译器默认情况下是关闭的,但可以通过两种不同的方式打开它。您可以通过使用解析器配置过程(前面讨论过)或者在SpEL用于嵌入到另一个组件中时使用Spring属性来打开它。本节讨论了这两种选项。

编译器可以在三种模式中的一种操作,这些模式在org.springframework.expression.spel.SpelCompilerMode枚举中定义。这些模式如下。

  • OFF(默认):编译器关闭。

  • IMMEDIATE:在即时模式下,表达式尽快编译。通常在第一次解释执行后进行编译。如果编译的表达式失败(通常是由于类型更改,如前面所述),表达式评估的调用者会收到异常。

  • MIXED:在混合模式下,表达式会在解释执行和编译模式之间静默切换。在一些解释运行后,它们会切换到编译形式,如果编译形式出现问题(例如类型更改,如前面所述),表达式会自动切换回解释形式。稍后,它可能生成另一个编译形式并切换到它。基本上,用户在IMMEDIATE模式中收到的异常在这里被内部处理。

IMMEDIATE模式存在的原因是因为MIXED模式可能会对具有副作用的表达式造成问题。如果编译的表达式在部分成功后失败,它可能已经执行了影响系统状态的操作。如果发生了这种情况,调用者可能不希望它在解释模式下静默重新运行,因为表达式的一部分可能会运行两次。

SpelParserConfiguration来配置解析器。以下示例展示了如何进行配置。

  • Java

  • Kotlin

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
		this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);
val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
		this.javaClass.classLoader)

val parser = SpelExpressionParser(config)

val expr = parser.parseExpression("payload")

val message = MyMessage()

val payload = expr.getValue(message)

当指定编译器模式时,还可以指定一个ClassLoader(允许传递null)。编译的表达式定义在提供的任何ClassLoader下创建的子ClassLoader中。重要的是要确保,如果指定了ClassLoader,它可以看到表达式评估过程中涉及的所有类型。如果不指定ClassLoader,则使用默认的ClassLoader(通常是在表达式评估期间运行的线程的上下文ClassLoader)。

配置编译器的第二种方式是在SpEL嵌入到其他组件中并且可能无法通过配置对象进行配置时使用。在这种情况下,可以通过JVM系统属性(或通过SpringProperties机制)将spring.expression.compiler.mode属性设置为SpelCompilerMode枚举值之一(offimmediatemixed)。

编译器限制

Spring不支持编译每种类型的表达式。主要关注的是在性能关键环境中可能使用的常见表达式。以下类型的表达式无法编译。

  • 涉及赋值的表达式

  • 依赖于转换服务的表达式

  • 使用自定义解析器或访问器的表达式

  • 使用重载运算符的表达式

  • 使用数组构造语法的表达式

  • 使用选择或投影的表达式

将来可能会支持编译其他类型的表达式。