评估
本节介绍了 SpEL 接口及其表达式语言的编程使用。完整的语言参考可以在语言参考中找到。
以下代码演示了如何使用 SpEL API 来计算字面字符串表达式 Hello World
。
- Java
- Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); 1
String message = (String) exp.getValue();
变量
message
的值是"Hello World"
。
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") 1
val message = exp.value as String
变量
message
的值是"Hello World"
。
你最有可能使用的 SpEL 类和接口位于 org.springframework.expression
包及其子包中,例如 spel.support
。
ExpressionParser
接口负责解析表达式字符串。在前面的例子中,表达式字符串是由周围的单引号表示的字符串字面量。Expression
接口负责评估定义的表达式字符串。在调用 parser.parseExpression(…)
和 exp.getValue(…)
时可能抛出的两种异常分别是 ParseException
和 EvaluationException
。
SpEL 支持多种功能,例如调用方法、访问属性和调用构造函数。
在下面的方法调用示例中,我们在字符串字面量 Hello World
上调用 concat
方法。
- Java
- Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); 1
String message = (String) exp.getValue();
message
的值现在是"Hello World!"
。
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") 1
val message = exp.value as String
message
的值现在是"Hello World!"
。
下面的示例演示了如何访问字符串字面量 Hello World
的 Bytes
JavaBean 属性。
- Java
- Kotlin
ExpressionParser parser = new SpelExpressionParser();
// 调用 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); 1
byte[] bytes = (byte[]) exp.getValue();
这一行将字面量转换为字节数组。
val parser = SpelExpressionParser()
// 调用 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") 1
val bytes = exp.value as ByteArray
这一行将字面量转换为字节数组。
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();
'Hello World'.bytes.length
获取字面量的长度。
val parser = SpelExpressionParser()
// 调用 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") 1
val length = exp.value as Int
'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);
从字面量构造一个新的
String
并将其转换为大写。
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()") 1
val message = exp.getValue(String::class.java)
从字面量构造一个新的
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
,可以在创建提供给 SpelExpressionParser
的 SpelParserConfiguration
时指定自定义的 maxExpressionLength
。如果您希望为在 ApplicationContext
中解析 SpEL 表达式设置 maxExpressionLength
— 例如,在 XML bean 定义、@Value
等中 — 可以设置一个 JVM 系统属性或 Spring 属性,名为 spring.context.expression.maxLength
,以满足您的应用程序所需的最大表达式长度(请参阅 Supported Spring Properties)。
SpEL 编译
Spring 提供了一个用于 SpEL 表达式的基本编译器。表达式通常是解释执行的,这在评估期间提供了很大的动态灵活性,但并不能提供最佳性能。对于偶尔的表达式使用,这没有问题,但当被其他组件(如 Spring Integration)使用时,性能可能非常重要,而且实际上并不需要动态性。
SpEL 编译器旨在解决这一需求。在评估过程中,编译器生成一个 Java 类,该类在运行时体现表达式行为,并使用该类实现更快的表达式评估。由于表达式缺乏类型化,编译器在执行编译时使用在解释评估过程中收集的信息。例如,它仅通过表达式无法知道属性引用的类型,但在第一次解释评估期间,它会发现属性的类型。当然,如果各种表达式元素的类型随着时间的推移而改变,基于这种派生信息进行编译可能会导致问题。因此,编译最适合用于类型信息在重复评估时不会改变的表达式。
考虑以下基本表达式。
someArray[0].someProperty.someOtherProperty < 0.1
由于前面的表达式涉及数组访问、一些属性解引用和数值运算,性能提升可能非常明显。在一个 50,000 次迭代的微基准测试示例中,使用解释器评估耗时 75ms,而使用编译版本的表达式仅耗时 3ms。
编译器配置
编译器默认未开启,但你可以通过两种不同的方式来开启它。你可以通过使用解析器配置过程(前面讨论过)来开启它,或者在 SpEL 用法嵌入到另一个组件中时,通过使用 Spring 属性来开启它。本节讨论这两种选项。
编译器可以在三种模式之一下运行,这些模式在 org.springframework.expression.spel.SpelCompilerMode
枚举中被捕获。模式如下。
OFF
编译器已关闭,所有表达式将以解释模式进行求值。这是默认模式。
IMMEDIATE
在立即模式下,表达式会尽快被编译,通常是在第一次解释执行之后。如果编译后的表达式执行失败(例如,由于类型变化,如前所述),表达式求值的调用者会收到一个异常。如果各种表达式元素的类型随着时间变化,考虑切换到 MIXED
模式或关闭编译器。
<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
枚举值之一(off
、immediate
或 mixed
)。
编译器限制
Spring 不支持编译所有类型的表达式。主要关注的是可能在性能关键的上下文中使用的常见表达式。以下类型的表达式无法编译。
-
包含赋值的表达式
-
依赖转换服务的表达式
-
使用自定义解析器的表达式
-
使用重载运算符的表达式
-
使用数组构造语法的表达式
-
使用选择或投影的表达式
-
使用 bean 引用的表达式
将来可能会支持其他类型表达式的编译。