评估

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

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

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 message变量的值为"Hello World"
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
1 message变量的值为"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();

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

// invokes '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();

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

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

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

  • 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

// 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

理解EvaluationContext

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

SimpleEvaluationContext

公开基本SpEL语言特性和配置选项的子集,用于不需要SpEL语言语法全部范围的表达式类别,并且应该有意义地受到限制。示例包括但不限于数据绑定表达式和基于属性的过滤器。

StandardEvaluationContext

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

SimpleEvaluationContext旨在仅支持SpEL语言语法的子集。例如,它排除了Java类型引用、构造函数和Bean引用。它还要求您明确选择表达式中属性和方法的支持级别。创建SimpleEvaluationContext时,需要选择SpEL表达式中数据绑定所需的支 持级别。

  • 只读访问的数据绑定

  • 读写访问的数据绑定

  • 自定义PropertyAccessor(通常不是基于反射的),可能与DataBindingPropertyAccessor结合使用

方便的是,SimpleEvaluationContext.forReadOnlyDataBinding()通过DataBindingPropertyAccessor启用对属性的只读访问。类似地,SimpleEvaluationContext.forReadWriteDataBinding()启用对属性的读写访问。或者,通过SimpleEvaluationContext.forPropertyAccessors(…​)配置自定义访问器,可能禁用赋值,并可以选择通过构建器激活方法解析和/或类型转换器。

类型转换

默认情况下,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" 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]

解析器配置

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

  • 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中用于解析SpEL表达式的maxExpressionLength——例如,在XML Bean定义、@Value等中——您可以设置一个名为spring.context.expression.maxLength的JVM系统属性或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模式或关闭编译器。

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 不支持编译所有类型的表达式。主要关注点是那些可能在性能关键型上下文中使用的常见表达式。以下类型的表达式无法编译。

  • 涉及赋值的表达式

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

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

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

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

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

  • 使用 Bean 引用的表达式

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