跳到主要内容
版本:7.0.3

评估

Hunyuan 7b 中英对照 Evaluation

本节介绍了如何通过编程方式使用SpEL的接口及其表达式语言。完整的语言参考可以在语言参考中找到。

以下代码演示了如何使用SpEL API来评估字面字符串表达式Hello World

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); 1
String message = (String) exp.getValue();
  • message 变量的值是 "Hello World"

您最有可能使用的SpEL类和接口位于org.springframework_expression包及其子包中,例如spel.support

ExpressionParser接口负责解析表达式字符串。在前面的例子中,表达式字符串是由单引号括起来的字符串字面量。Expression接口负责计算定义好的表达式字符串。在调用parser.parseExpression(…)exp.getValue(…)时,可能会抛出两种类型的异常,分别是ParseExceptionEvaluationException

SpEL支持多种功能,如调用方法、访问属性以及调用构造函数。

在以下方法调用示例中,我们在字符串字面量“Hello World”上调用了concat方法。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); 1
String message = (String) exp.getValue();
  • message 的值现在是 "Hello World!"

以下示例演示了如何访问字符串字面量“Hello World”的Bytes JavaBean属性。

ExpressionParser parser = new SpelExpressionParser();

// 调用 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); 1
byte[] bytes = (byte[]) exp.getValue();
  • 这一行将字面量转换为字节数组。

SpEL还支持通过标准的点表示法(例如prop1.prop2.prop3)来访问嵌套属性,同时也支持对属性值的相应设置。也可以访问公共字段。

以下示例展示了如何使用点表示法来获取字符串字面量的长度。

ExpressionParser parser = new SpelExpressionParser();

// 调用 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); 1
int length = (Integer) exp.getValue();
  • 'Hello World'.bytes.length 表示字面量的长度。

如以下示例所示,可以调用字符串的构造函数来代替使用字符串字面量。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); 1
String message = exp.getValue(String.class);
  • 从字面量构建一个新的 String 并将其转换为大写。

请注意这里使用的是通用方法:public <T> T getValue(Class<T> desiredResultType)。使用此方法可以省去将表达式的值强制转换为所需结果类型的步骤。如果无法将该值转换为类型 T,或者无法通过已注册的类型转换器进行转换,将会抛出 EvaluationException 异常。

SpEL 更常见的用法是提供一个表达式字符串,该字符串会针对一个特定的对象实例(称为根对象)进行求值。以下示例展示了如何从 Inventor 类的实例中检索 name 属性,以及如何在布尔表达式中引用该 name 属性。

// 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

理解 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还支持泛型机制。这意味着,在表达式中使用泛型类型时,SpEL会自动尝试进行转换,以确保遇到的所有对象都能保持正确的类型。

在实践中这意味着什么?假设使用 setValue() 方法来设置一个 List 类型的属性。该属性的实际类型是 List<Boolean>。SpEL 能识别出列表中的元素在被放入列表之前需要被转换为 Boolean 类型。以下示例展示了如何实现这一点。

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);

解析器配置

可以通过使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)来配置SpEL表达式解析器。该配置对象控制着某些表达式组件的行为。例如,如果你对一个集合进行索引操作,而指定索引处的元素为null,SpEL可以自动创建该元素。这对于使用由一系列属性引用构成的表达式时非常有用。同样地,如果你对一个集合进行索引操作,并指定的索引超出了该集合当前的大小,SpEL可以自动扩展集合以容纳该索引。为了在指定索引处添加一个元素,SpEL会首先尝试使用该元素类型的默认构造函数来创建该元素,然后再设置指定的值。如果该元素类型没有默认构造函数,那么会在集合中添加null。如果没有内置的转换器或自定义转换器能够知道如何设置该值,那么在指定索引处的集合中仍将保留null。以下示例演示了如何自动扩展一个List

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

默认情况下,SpEL表达式不能包含超过10,000个字符;但是,maxExpressionLength是可以配置的。如果您通过编程方式创建SpelExpressionParser,则可以在创建提供给SpelExpressionParserSpelParserConfiguration时指定自定义的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次迭代的微基准测试中,使用解释器进行评估需要75毫秒,而使用编译后的表达式版本仅需3毫秒。

编译器配置

编译器默认是关闭的,但你可以用两种不同的方式来开启它。一种是通过使用解析器配置过程(前面已经讨论过),另一种是在SpEL被嵌入到另一个组件中时,通过使用Spring属性来开启。本节将讨论这两种选项。

编译器可以在三种模式之一下运行,这些模式在org.springframework(expression.spel.SpelCompilerMode枚举中有所定义。模式如下所示。

关闭

编译器已关闭,所有表达式都将在解释模式下进行求值。这是默认模式。

立即的;立刻的

在即时模式下,表达式会尽快被编译,通常在第一次解释性求值之后就进行编译。如果编译后的表达式求值失败(例如,如前所述,由于类型变化),则调用该表达式求值的人员会收到一个异常。如果各个表达式元素的类型会随时间发生变化,可以考虑切换到MIXED模式或关闭编译器。

MIXED

在混合模式下,表达式的求值会随着时间的推移在“解释执行”和“编译执行”之间自动切换。在经过一定次数的成功解释执行后,该表达式会被编译。如果编译后的表达式求值失败(例如由于类型变化),这种失败会在内部被捕获,系统会重新切换回对该表达式的解释执行模式。基本上,在“IMMEDIATE”模式下调用者接收到的异常实际上是由内部处理的。之后某个时候,编译器可能会生成另一种编译后的表达式形式并切换到该形式进行求值。这种在解释执行和编译执行之间切换的循环会持续进行,直到系统判断继续尝试没有意义为止——例如,当达到某个失败的阈值时——此时系统将对该表达式永久切换回解释执行模式。

IMMEDIATE 模式的存在是因为 MIXED 模式可能会对具有副作用的表达式造成问题。如果一个编译后的表达式在部分成功执行后出错,它可能已经对系统状态产生了影响。在这种情况下,调用者可能不希望该表达式以解释器模式默默地重新运行,因为部分表达式可能会被执行两次。

选择了一种模式后,使用 SpelParserConfiguration 来配置解析器。以下示例展示了如何进行配置。

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 系统属性(或通过 SpringProperties 机制)将 spring_expression.compiler.mode 属性设置为 SpelCompilerMode 枚举中的一个值(offimmediatemixed)。

编译器限制

Spring并不支持编译每一种表达式。其主要关注的是在性能至关重要的场景中可能使用的常见表达式。以下类型的表达式无法被编译。

  • 涉及赋值的表达式
  • 依赖转换服务的表达式
  • 使用自定义解析器的表达式
  • 使用重载运算符的表达式
  • 使用数组构造语法的表达式
  • 使用选择或投影的表达式
  • 使用bean引用的表达式

未来可能会支持更多种类的表达式编译。