评估
本节介绍 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(…)
时可能抛出的两种异常分别是 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();
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 World
的 Bytes
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(…)
配置自定义访问器,可以禁用赋值,并通过 builder 可选地激活方法解析和/或类型转换器。
类型转换
默认情况下,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]
解析器配置
可以使用解析器配置对象 (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.context.expression.maxLength
的 Spring 属性为您应用所需的表达式最大长度(参阅支持的 Spring 属性)。
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
-
在混合模式下,表达式评估会在 解释执行 和 编译 模式之间随着时间静默切换。经过一定数量的成功解释执行后,表达式会被编译。如果编译后的表达式评估失败(例如,由于类型改变),该失败将在内部捕获,并且系统将针对给定表达式切换回解释执行模式。基本上,调用者在
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
(通常是表达式评估期间运行线程的上下文 ClassLoader
)。
配置编译器的第二种方法适用于 SpEL 嵌入在其他组件中,且无法通过配置对象进行配置的情况。在这种情况下,可以通过 JVM 系统属性(或通过 SpringProperties
机制)将 spring.expression.compiler.mode
属性设置为 SpelCompilerMode
枚举值(off
、immediate
或 mixed
)之一。