|
此版本仍在开发中,尚未稳定。如需最新的稳定版本,请使用 Spring Framework 7.0.6! |
评估
本节介绍以编程方式使用 SpEL 的接口及其表达式语言。 完整的语言参考可在 语言参考 中找到。
以下代码演示了如何使用 SpEL API 来计算字面字符串表达式 Hello World。
-
Java
-
Kotlin
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.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 并将其转换为大写。 |
注意通用方法的使用:<code>0</code>。 使用此方法可以避免将表达式的值强制转换为所需的结果类型。如果无法将值转换为类型<code>2</code>或通过注册的类型转换器进行转换,则会抛出<code>1</code>。
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 等中),可以将名为 spring.context.expression.maxLength 的 JVM 系统属性或 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)。
配置编译器的第二种方法适用于 SpEL 嵌入在其他组件中,并且可能无法通过配置对象进行配置的情况。在这种情况下,可以通过 JVM 系统属性(或通过
SpringProperties 机制)将 spring.expression.compiler.mode
属性设置为 SpelCompilerMode 枚举值之一(off、immediate 或 mixed)。