对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

评估

本节介绍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 消息变量的值是 '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.supportspring-doc.cadn.net.cn

ExpressionParser 接口负责解析表达式字符串。在前面的示例中,表达式字符串是一个由周围单引号表示的字符串字面量。Expression 接口负责评估之前定义的表达式字符串。在调用parser.parseExpressionexp.getValue时可能抛出的两个异常,分别是ParseExceptionEvaluationExceptionspring-doc.cadn.net.cn

SpEL支持广泛的功能,例如调用方法、访问属性和调用构造函数。spring-doc.cadn.net.cn

在下面的方法调用示例中,我们在字符串字面量上调用 concat 方法: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!'。

以下调用JavaBean属性的示例调用了 String 属性 Bytes :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 并将其转换为大写。

注意通用方法的使用:<code>0</code>。 使用此方法可以避免将表达式的值强制转换为所需的结果类型。如果无法将值转换为类型<code>2</code>或通过注册的类型转换器进行转换,则会抛出<code>1</code>。spring-doc.cadn.net.cn

SpEL更常见的用法是提供一个表达式字符串,该字符串针对特定对象实例(称为根对象)进行求值。下面的示例显示了如何从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

The EvaluationContext interface 用于在评估表达式时解析属性、方法或字段,并帮助执行类型转换。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 引用。它还要求您显式选择表达式中属性和方法的支持级别。 默认情况下,create() 静态工厂方法仅启用对属性的只读访问。 您还可以获取一个构建器来配置所需的确切支持级别,针对以下一项或多项组合:spring-doc.cadn.net.cn

类型转换

默认情况下,SpEL 使用 Spring 核心中的转换服务(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将保留在数组或列表中的指定索引处。下面的示例演示了如何自动扩展列表:spring-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编译

Spring Framework 4.1 包含一个基本的表达式编译器。表达式通常是被解释执行的,这在求值过程中提供了很多动态灵活性,但不会提供最佳性能。对于偶尔使用的表达式来说,这没有问题,但是当其他组件(如 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

由于前面的表达式涉及数组访问、一些属性解引用和数值运算,性能提升可能非常显著。在一个50000次迭代的微基准测试中,使用解释器评估花费了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: 在立即模式下,表达式会尽快编译。这通常是在第一次解释执行之后。如果编译后的表达式失败(通常是由于类型发生变化,如前所述),则表达式评估的调用者会收到异常。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)

当指定编译器模式时,也可以指定一个类加载器(传递null是允许的)。 已编译的表达式是在任何提供的类加载器下创建的子类加载器中定义的。 确保如果指定了类加载器,它可以看到表达式求值过程中涉及的所有类型是很重要的。如果您没有指定类加载器,则使用默认的类加载器(通常是表达式求值期间运行的线程的上下文类加载器)。spring-doc.cadn.net.cn

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

编译器限制

自 Spring Framework 4.1 以来,基本的编译框架已经就绪。然而,该框架尚未支持编译所有类型的表达式。最初的重点是那些可能在性能关键上下文中使用的常见表达式。目前无法编译以下类型的表达式:spring-doc.cadn.net.cn

未来将支持更多类型的表达式。spring-doc.cadn.net.cn