评估
本节介绍 SpEL 接口及其表达式语言的编程式使用。 完整的语言参考可以在 语言参考 中找到。
以下代码演示了如何使用 SpEL API 评估字面字符串表达式 Hello World。
-
Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
| 1 | message 变量的值是 "Hello World"。 |
你最可能使用的 SpEL 类和接口位于 infra.expression 包及其子包(如 spel.support)中。
ExpressionParser 接口负责解析表达式字符串。
在前面的例子中,表达式字符串是由周围的单引号表示的字符串字面量。
Expression 接口负责评估定义的表达式字符串。
调用 parser.parseExpression(…) 和 exp.getValue(…) 时可能抛出的两种异常分别是
ParseException 和 EvaluationException。
SpEL 支持广泛的功能,例如调用方法、访问属性和调用构造函数。
在下面的方法调用示例中,我们在字符串字面量 Hello World 上调用 concat 方法。
-
Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
| 1 | message 的值现在是 "Hello World!"。 |
下例演示了如何访问字符串字面量 Hello World 的 Bytes JavaBean 属性。
-
Java
ExpressionParser parser = new SpelExpressionParser();
// 调用 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
| 1 | 此行将字面量转换为字节数组。 |
SpEL 还支持通过使用标准点符号(例如 prop1.prop2.prop3)访问嵌套属性以及相应的属性值设置。
也可以访问公共字段。
下例显示了如何使用点符号来获取字符串字面量的长度。
-
Java
ExpressionParser parser = new SpelExpressionParser();
// 调用 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
| 1 | 'Hello World'.bytes.length 给出字面量的长度。 |
可以调用 String 的构造函数而不是使用字符串字面量,如下例所示。
-
Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
| 1 | 从字面量构造一个新的 String 并将其转换为大写。 |
注意泛型方法的使用:public <T> T getValue(Class<T> desiredResultType)。
使用此方法无需将表达式的值强制转换为所需的结果类型。
如果值不能强制转换为类型 T 或使用已注册的类型转换器进行转换,则抛出 EvaluationException。
SpEL 更常见的用法是提供一个针对特定对象实例(称为根对象)进行评估的表达式字符串。
下例显示了如何从 Inventor 类的实例中检索 name 属性,以及如何在布尔表达式中引用 name 属性。
-
Java
// 创建并设置日历
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// 构造函数参数是姓名、生日和国籍。
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // 将 name 解析为表达式
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
理解 EvaluationContext
EvaluationContext 接口用于在评估表达式以解析属性、方法或字段并帮助执行类型转换时使用。
Infra 提供了两个实现。
-
SimpleEvaluationContext: 针对不需要 SpEL 语言语法的全部范围且应受到有意义限制的表达式类别, 公开 SpEL 语言特性和配置选项的一个子集。 示例包括但不限于数据绑定表达式和基于属性的过滤器。 -
StandardEvaluationContext: 公开全套 SpEL 语言特性和配置选项。 你可以使用它来指定默认根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。
它排除了 Java 类型引用、构造函数和 bean 引用。
它还要求你显式选择表达式中属性和方法的支持级别。
默认情况下,create() 静态工厂方法仅启用对属性的读取访问权限。
你还可以获取构建器以配置所需的确切支持级别,针对以下一项或某种组合。
-
仅自定义
PropertyAccessor(无反射) -
用于只读访问的数据绑定属性
-
用于读写的数据绑定属性
类型转换
默认情况下,SpEL 使用 Infra core 中可用的转换服务
(infra.core.convert.ConversionService)。此转换服务附带了许多用于常见转换的内置转换器,
但也完全可扩展,以便你可以在类型之间添加自定义转换。此外,它还具有泛型感知能力。
这意味着,当你在表达式中使用泛型类型时,SpEL 会尝试转换以保持其遇到的任何对象的类型正确性。
这在实践中意味着什么?假设使用 setValue() 进行赋值来设置 List 属性。
该属性的类型实际上是 List<Boolean>。SpEL 认识到列表的元素在放入之前需要转换为 Boolean。
下例展示了如何做到这一点。
-
Java
class Simple {
public List<Boolean> booleanList = new ArrayList<>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// "false" 作为 String 传入。SpEL 和转换服务
// 将识别出它需要是 Boolean 并相应地进行转换。
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b 是 false
Boolean b = simple.booleanList.get(0);
解析器配置
可以使用解析器配置对象 (infra.expression.spel.SpelParserConfiguration) 配置 SpEL 表达式解析器。
配置对象控制某些表达式组件的行为。例如,如果你对数组或集合进行索引,并且指定索引处的元素为 null,
SpEL 可以自动创建该元素。这在使用由属性引用链组成的表达式时非常有用。
如果你对数组或列表进行索引并指定超出数组或列表当前大小的索引,SpEL 可以自动增加数组或列表以容纳该索引。
为了在指定索引处添加元素,SpEL 将尝试在设置指定值之前使用元素类型的默认构造函数创建元素。
如果元素类型没有默认构造函数,则将 null 添加到数组或列表中。
如果没有内置或自定义转换器知道如何设置值,则 null 将保留在数组或列表中的指定索引处。
下例演示了如何自动增加列表。
-
Java
class Demo {
public List<String> list;
}
// 开启:
// - 自动空引用初始化
// - 自动集合增长
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 现在将是一个包含 4 个条目的真实集合
// 每个条目都是一个新的空字符串
默认情况下,SpEL 表达式不能包含超过 10,000 个字符;但是,maxExpressionLength 是可配置的。
如果你以编程方式创建 SpelExpressionParser,则可以在创建提供给 SpelExpressionParser 的
SpelParserConfiguration 时指定自定义 maxExpressionLength。
如果你希望设置用于在 ApplicationContext 中解析 SpEL 表达式的 maxExpressionLength
(例如,在 XML bean 定义、@Value 等中),你可以将名为 infra.context.expression.maxLength
的 JVM 系统属性或 Infra 属性设置为应用程序所需的最大表达式长度
(请参阅 支持的 Infra 属性)。
SpEL 编译
Infra 提供了一个基本的 SpEL 表达式编译器。表达式通常是被解释的,这在评估期间提供了很大的动态灵活性, 但不能提供最佳性能。对于偶尔的表达式使用,这很好,但是,当被其他组件(如 Infra Integration)使用时, 性能可能非常重要,并且并不真正需要动态性。
SpEL 编译器旨在解决这一需求。在评估期间,编译器生成一个体现运行时表达式行为的 Java 类, 并使用该类来实现更快的表达式评估。由于表达式周围缺乏类型化,编译器在执行编译时使用在表达式的解释评估期间收集的信息。 例如,它不能仅从表达式中知道属性引用的类型,但在第一次解释评估期间,它会找出它是什么。 当然,如果各种表达式元素的类型随时间变化,基于此类派生信息的编译稍后可能会导致问题。 因此,编译最适合那些类型信息在重复评估中不会改变的表达式。
考虑以下基本表达式。
someArray[0].someProperty.someOtherProperty < 0.1
因为前面的表达式涉及数组访问、一些属性解引用和数字运算,所以性能提升可能非常明显。 在 50,000 次迭代的示例微基准测试运行中,使用解释器评估需要 75 毫秒,而使用表达式的编译版本仅需 3 毫秒。
编译器配置
编译器默认未开启,但你可以通过两种不同的方式开启它。 你可以通过使用解析器配置过程(前面已讨论) 开启它,或者当 SpEL 用法嵌入在另一个组件内部时使用 Infra 属性开启它。 本节讨论这两个选项。
编译器可以在三种模式之一下运行,这些模式捕获在 infra.expression.spel.SpelCompilerMode 枚举中。
模式如下。
-
OFF(默认):编译器已关闭。 -
IMMEDIATE:在立即模式下,表达式会尽快编译。这通常是在第一次解释评估之后。 如果编译后的表达式失败(通常是由于类型更改,如前所述),则表达式评估的调用者会收到异常。 -
MIXED:在混合模式下,表达式随着时间的推移在解释模式和编译模式之间静默切换。 经过一定数量的解释运行后,它们切换到编译形式,如果编译形式出现问题(例如类型更改,如前所述), 表达式会自动再次切换回解释形式。稍后,它可能会生成另一种编译形式并切换到它。 基本上,用户在IMMEDIATE模式下获得的异常是在内部处理的。
IMMEDIATE 模式的存在是因为 MIXED 模式可能会导致具有副作用的表达式出现问题。
如果编译后的表达式在部分成功后崩溃,它可能已经做了一些影响系统状态的事情。
如果发生了这种情况,调用者可能不希望它在解释模式下静默重新运行,因为部分表达式可能会运行两次。
选择模式后,使用 SpelParserConfiguration 配置解析器。下例展示了如何做到这一点。
-
Java
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);
指定编译器模式时,还可以指定 ClassLoader(允许传递 null)。
编译后的表达式定义在提供的任何 ClassLoader 下创建的子 ClassLoader 中。
重要的是要确保,如果指定了 ClassLoader,它可以查看表达式评估过程中涉及的所有类型。
如果不指定 ClassLoader,则使用默认的 ClassLoader(通常是表达式评估期间运行的线程的上下文 ClassLoader)。
配置编译器的第二种方法是在 SpEL 嵌入在其他组件内部且可能无法通过配置对象对其进行配置时使用。
在这种情况下,可以通过 JVM 系统属性(或通过
InfraProperties 机制)将 spel.compiler.mode
属性设置为 SpelCompilerMode 枚举值之一(off、immediate 或 mixed)。