Evaluation

本部分介绍了 SpEL 接口及其表达式语言的编程使用。完整的语言参考可在此找到Language Reference. 下面的代码演示如何使用 SpEL API 来评估文字字符串表达式“Hello World”。

Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 message 变量的值是 "Hello World"
Kotlin
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
2 message 变量的值是 "Hello World"

你最可能使用的 SpEL 类和接口位于 org.springframework.expression 包及其子包(例如 spel.support)中。 ExpressionParser 接口负责解析表达式字符串。在前面的示例中,表达式字符串是一个文字字符串,由周围的单引号表示。Expression 接口负责评估已定义的表达式字符串。调用 parser.parseExpression(…​) 和 exp.getValue(…​) 时可能抛出的两种类型的异常分别为 ParseException 和 EvaluationException。 SpEL 支持广泛的功能,例如调用方法、访问属性和调用构造函数。 在下面的方法调用示例中,我们对字符串文字“Hello World”调用 concat 方法。

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

以下示例演示如何访问字符串文字“Hello World”的 JavaBean 属性“Bytes”。

Java
ExpressionParser parser = new SpelExpressionParser();

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

// invokes 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") (1)
val bytes = exp.value as ByteArray
2 此行将文本转换为字节数组。

SpEL 还支持使用标准点表示法(例如 prop1.prop2.prop3)以及相应的属性值设置来访问嵌套属性。还可以访问公有字段。 以下示例演示如何使用点表示法获取字符串文字的长度。

Java
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
1 ’Hello World'.bytes.length`给出文本的长度。
Kotlin
val parser = SpelExpressionParser()

// invokes 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") (1)
val length = exp.value as Int
2 ’Hello World'.bytes.length`给出文本的长度。

可以调用字符串的构造函数,而不是像以下示例所展示那样使用字符串文本。

Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
1 根据文本构造一个新的 `String`并将其转换为大写。
Kotlin
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()")  (1)
val message = exp.getValue(String::class.java)
2 根据文本构造一个新的 `String`并将其转换为大写。

请注意使用泛型方法:public <T> T getValue(Class<T> desiredResultType)。使用此方法消除了将表达式的值强制转换为所需结果类型的需要。如果该值无法强制转换为类型 T 或无法使用已注册的类型转换器进行转换,则会抛出 EvaluationException。 更常见的 SpEL 用法是提供针对特定对象实例(称为根对象)进行评估的表达式字符串。以下示例演示如何从 Inventor 类的实例检索 name 属性,以及如何在布尔表达式中引用 name 属性。

  • Java

  • Kotlin

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
// Create and set a calendar
val c = GregorianCalendar()
c.set(1856, 7, 9)

// The constructor arguments are name, birthday, and nationality.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")

val parser = SpelExpressionParser()

var exp = parser.parseExpression("name") // Parse name as an expression
val name = exp.getValue(tesla) as String
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true

Understanding EvaluationContext

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

  • SimpleEvaluationContext:为不需要 SpEL 语言语法完全扩展且应有意义地受到限制的表达式类型公开 SpEL 语言特性的一个本质子集和配置选项。示例包括但不限于数据绑定表达式和基于属性的过滤器。

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

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

  • 只有自定义 PropertyAccessor(无反射)

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

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

Type Conversion

默认情况下,SpEL 使用 Spring Core 中提供的转换服务 (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" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b is 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" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")

// b is false
val b = simple.booleanList[0]

Parser Configuration

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

  • Java

  • Kotlin

class Demo {
	public List<String> list;
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
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 will now be a real collection of 4 entries
// Each entry is a new empty String
class Demo {
	var list: List<String>? = null
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
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 will now be a real collection of 4 entries
// Each entry is a new empty String

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

SpEL Compilation

Spring 为 SpEL 表达式提供了一个基本编译器。表达式通常是解释的,这在评估期间提供了很多动态灵活性,但是不能提供最佳性能。对于偶尔的表达式使用,这很好,但是,当被 Spring Integration 等其他组件使用时,性能就非常重要了,并且实际上不需要动态性。

SpEL 编译器旨在满足此需求。在评估期间,编译器会生成一个 Java 类,该类体现了运行时的表达式行为,并利用该类实现更快速的表达式评估。由于缺乏有关表达式的类型化,因此编译器在执行编译时使用在解释评估表达式期间收集的信息。例如,它不会从表达式中完全了解属性引用的类型,但在首次解释评估期间,它会找出是什么。当然,如果各种表达式元素的类型随时间变化,那么基于此派生信息进行编译可能会在以后造成麻烦。因此,编译最适合表达式类型信息不会在重复评估中发生更改。

考虑以下基本表达式。

someArray[0].someProperty.someOtherProperty < 0.1

由于前面的表达式涉及数组访问、某些属性取消引用和数值运算,因此性能增益会很明显。在一个运行 50,000 次迭代的示例微基准中,使用解释器评估需要 75 毫秒,而使用编译版本表达式只需 3 毫秒。

Compiler Configuration

该编译器默认未开启,但是你可以采用两种不同方式中的任何一种开启它。你可以通过使用解析器配置流程 (discussed earlier) 或在 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)。

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

Compiler Limitations

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

  • Expressions involving assignment

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

  • Expressions using custom resolvers

  • Expressions using overloaded operators

  • 使用数组构造语法

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

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