评估

本节介绍 SpEL 接口及其表达式语言的编程使用方式。 完整的语言参考请参见 语言参考spring-doc.cadn.net.cn

以下代码演示了如何使用 SpEL API 来求值字面量字符串表达式 Hello Worldspring-doc.cadn.net.cn

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.supportspring-doc.cadn.net.cn

ExpressionParser 接口负责解析表达式字符串。在前面的示例中,表达式字符串是一个由单引号包围的字符串字面量。Expression 接口负责对已定义的表达式字符串进行求值。调用 parser.parseExpression(…​)exp.getValue(…​) 时可能分别抛出 ParseExceptionEvaluationException 两种异常。spring-doc.cadn.net.cn

SpEL 支持多种功能,例如调用方法、访问属性以及调用构造函数。spring-doc.cadn.net.cn

在下面的方法调用示例中,我们在字符串字面量 concat 上调用了 Hello World 方法。spring-doc.cadn.net.cn

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!"

以下示例演示了如何访问字符串字面量 BytesHello World JavaBean 属性。spring-doc.cadn.net.cn

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)来访问嵌套属性,以及相应地设置属性值。 也可以访问公共字段。spring-doc.cadn.net.cn

以下示例展示了如何使用点号表示法来获取字符串字面量的长度。spring-doc.cadn.net.cn

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 的构造函数,而不使用字符串字面量,如下例所示。spring-doc.cadn.net.cn

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)。 使用此方法可以避免将表达式的值强制转换为所需的结果类型。 如果该值无法转换为类型 EvaluationException,或者无法通过已注册的类型转换器进行转换,则会抛出 T 异常。spring-doc.cadn.net.cn

SpEL 更常见的用法是提供一个表达式字符串,该字符串针对特定的对象实例(称为根对象)进行求值。以下示例展示了如何从 name 类的一个实例中获取 Inventor 属性,以及如何在布尔表达式中引用 name 属性。spring-doc.cadn.net.cn

// 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 提供了两种实现。spring-doc.cadn.net.cn

SimpleEvaluationContext

公开 SpEL 语言的一组核心特性和配置选项的子集,适用于那些不需要完整 SpEL 语言语法、且应进行有意义限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。spring-doc.cadn.net.cn

StandardEvaluationContext

暴露完整的 SpEL 语言特性和配置选项。您可以使用它来指定默认的根对象,并配置所有可用的与表达式求值相关的策略。spring-doc.cadn.net.cn

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

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

类型转换

默认情况下,SpEL 使用 Spring Core 中提供的转换服务(org.springframework.core.convert.ConversionService)。该转换服务内置了许多用于常见类型转换的转换器,同时也完全可扩展,允许你添加自定义的类型转换。此外,它还支持泛型。这意味着,当你在表达式中使用泛型类型时,SpEL 会尝试进行类型转换,以确保所遇到的任何对象都保持类型正确性。spring-doc.cadn.net.cn

这在实践中意味着什么?假设正在使用 setValue() 方法进行赋值,以设置一个 List 属性。该属性的实际类型是 List<Boolean>。SpEL 能够识别出列表中的元素在放入列表之前需要转换为 Boolean 类型。以下示例展示了如何实现这一点。spring-doc.cadn.net.cn

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 将会在指定索引处保留在集合中。下面的例子演示了如何 自动扩展一个 Listspring-doc.cadn.net.cn

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 属性)。spring-doc.cadn.net.cn

SpEL 编译

Spring 为 SpEL 表达式提供了一个基本的编译器。表达式通常以解释方式执行,这在求值过程中提供了很大的动态灵活性,但无法达到最佳性能。对于偶尔使用的表达式来说,这没有问题;然而,当被其他组件(例如 Spring Integration)使用时,性能可能变得非常重要,而此时实际上并不需要这种动态性。spring-doc.cadn.net.cn

SpEL 编译器正是为满足这一需求而设计的。在表达式求值过程中,编译器会生成一个 Java 类,该类在运行时体现表达式的行为,并利用该类实现更快的表达式求值。由于表达式本身缺乏类型信息,编译器在编译时会利用解释执行阶段收集到的表达式求值信息。例如,仅从表达式本身无法得知某个属性引用的具体类型,但在首次解释执行求值时,编译器就能确定其实际类型。当然,如果表达式中各个元素的类型随时间发生变化,那么基于此类推导出的信息进行编译可能会在后续引发问题。因此,编译最适合用于那些在重复求值过程中类型信息不会发生变化的表达式。spring-doc.cadn.net.cn

考虑以下基本表达式。spring-doc.cadn.net.cn

someArray[0].someProperty.someOtherProperty < 0.1

由于上述表达式涉及数组访问、某些属性解引用以及数值运算,因此性能提升可能非常明显。在一个包含 50,000 次迭代的微基准测试示例中,使用解释器求值耗时 75 毫秒,而使用编译后的表达式版本仅需 3 毫秒。spring-doc.cadn.net.cn

编译器配置

编译器默认未启用,但您可以通过以下两种方式之一来启用它。您可以使用解析器配置过程(如前所述)来启用,或者在 SpEL 被嵌入到另一个组件中使用时,通过设置一个 Spring 属性来启用。本节将讨论这两种选项。spring-doc.cadn.net.cn

编译器可以以三种模式之一运行,这些模式在 org.springframework.expression.spel.SpelCompilerMode 枚举中定义。这些模式如下所示。spring-doc.cadn.net.cn

OFF

编译器已关闭,所有表达式将以解释模式进行求值。这是默认模式。spring-doc.cadn.net.cn

IMMEDIATE

在即时模式下,表达式会尽快被编译,通常是在首次解释执行之后。如果编译后的表达式求值失败(例如,由于前面所述的类型发生变化),调用表达式求值的方法将收到一个异常。如果各种表达式元素的类型会随时间变化,请考虑切换到 MIXED 模式或关闭编译器。spring-doc.cadn.net.cn

MIXED

在混合模式下,表达式求值会随着时间的推移在解释执行编译执行之间静默切换。经过若干次成功的解释执行后,该表达式会被编译。如果编译后的表达式求值失败(例如,由于类型发生变化),该失败会被内部捕获,系统将针对该表达式重新切换回解释模式。基本上,在IMMEDIATE模式下由调用方接收到的异常,此时会在内部被处理掉。稍后,编译器可能会再次生成一个新的编译形式并切换到该形式。这种在解释模式和编译模式之间的切换循环将持续进行,直到系统判定继续尝试已无意义为止——例如,当达到某个失败阈值时——此时系统将对该表达式永久切换到解释模式。spring-doc.cadn.net.cn

IMMEDIATE 模式存在的原因是:MIXED 模式可能会对具有副作用的表达式造成问题。如果一个已编译的表达式在部分执行成功后发生异常,它可能已经执行了某些操作并影响了系统的状态。在这种情况下,调用者可能不希望该表达式在解释模式下静默地重新运行,因为表达式的部分内容可能会被执行两次。spring-doc.cadn.net.cn

选择模式后,使用 SpelParserConfiguration 来配置解析器。以下示例展示了如何进行此操作。spring-doc.cadn.net.cn

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(通常是执行表达式求值的线程的上下文7)。spring-doc.cadn.net.cn

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

编译器限制

Spring 并不支持编译所有类型的表达式。其主要关注点在于那些可能在性能敏感场景中常用的表达式。以下类型的表达式无法被编译。spring-doc.cadn.net.cn

将来可能会支持更多种类表达式的编译。spring-doc.cadn.net.cn