|
对于最新稳定版本,请使用 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 | 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 接口负责对先前定义的表达式字符串进行求值。调用 ParseException 和 EvaluationException 时,可能会分别抛出两种异常:parser.parseExpression 和 exp.getValue。
SpEL 支持丰富的功能,例如调用方法、访问属性以及调用构造函数。
在以下方法调用的示例中,我们在字符串字面量上调用 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!'。 |
以下调用 JavaBean 属性的示例调用了 String 类型的属性 Bytes:
-
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)。
使用此方法可以避免将表达式的值强制转换为所需的结果类型。
如果该值无法转换为类型 EvaluationException,或者无法通过已注册的类型转换器进行转换,则会抛出 T 异常。
SpEL 更常见的用法是提供一个表达式字符串,该字符串针对特定的对象实例(称为根对象)进行求值。以下示例展示了如何从 name 类的实例中获取 Inventor 属性,或创建一个布尔条件:
-
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 接口用于解析属性、方法或字段,并协助执行类型转换。Spring 提供了两种实现。
-
SimpleEvaluationContext:公开 SpEL 语言核心功能和配置选项的一个子集,适用于那些不需要完整 SpEL 语言语法、且应进行有意义限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。 -
StandardEvaluationContext:提供完整的 SpEL 语言特性和配置选项。你可以用它来指定默认的根对象,并配置所有可用的与表达式求值相关的策略。
SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。
它排除了 Java 类型引用、构造函数和 Bean 引用。同时,它还要求你显式选择表达式中对属性和方法的支持级别。
默认情况下,create() 静态工厂方法仅启用对属性的读取访问。
你也可以获取一个构建器(builder),以配置所需的确切支持级别,针对以下一项或多项组合进行设置:
-
仅自定义
PropertyAccessor(无反射) -
用于只读访问的数据绑定属性
-
用于读取和写入的数据绑定属性
类型转换
默认情况下,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 将会保留在指定索引处的数组或列表中。以下示例演示了如何自动扩展列表:
-
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 编译
Spring Framework 4.1 包含一个基本的表达式编译器。表达式通常以解释方式执行,这在求值过程中提供了很大的动态灵活性,但无法实现最佳性能。对于偶尔使用的表达式来说,这没有问题;然而,当被其他组件(例如 Spring Integration)使用时,性能就变得非常重要,而此时实际上并不需要这种动态性。
SpEL 编译器正是为满足这一需求而设计的。在表达式求值过程中,编译器会生成一个 Java 类,该类在运行时体现表达式的行为,并利用该类实现更快的表达式求值。由于表达式本身缺乏类型信息,编译器在编译时会利用解释执行阶段收集到的表达式求值信息。例如,仅从表达式本身无法得知某个属性引用的具体类型,但在首次解释执行求值时,编译器就能确定其实际类型。当然,如果表达式中各个元素的类型随时间发生变化,那么基于此类推导出的信息进行编译可能会在后续引发问题。因此,编译最适合用于那些在重复求值过程中类型信息不会发生变化的表达式。
考虑以下基本表达式:
someArray[0].someProperty.someOtherProperty < 0.1
由于上述表达式涉及数组访问、某些属性解引用以及数值运算,因此性能提升可能非常明显。在一个包含 50000 次迭代的微基准测试示例中,使用解释器求值耗时 75 毫秒,而使用编译后的表达式版本仅耗时 3 毫秒。
编译器配置
编译器默认未启用,但您可以通过以下两种方式之一来启用它。您可以使用解析器配置过程(如前所述)来启用,或者在 SpEL 被嵌入到另一个组件中使用时,通过设置一个 Spring 属性来启用。本节将讨论这两种选项。
编译器可以以三种模式之一运行,这些模式在 org.springframework.expression.spel.SpelCompilerMode 枚举中定义。这些模式如下所示:
-
OFF(默认):编译器已关闭。 -
IMMEDIATE:在即时模式下,表达式会尽快被编译。这通常发生在首次解释执行之后。如果编译后的表达式执行失败(通常是由于前面所述的类型发生变化),调用该表达式求值的代码将收到一个异常。 -
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)
当你指定编译器模式时,还可以指定一个类加载器(允许传入 null)。 编译后的表达式定义在一个子类加载器中,该子类加载器是在所提供的任何类加载器之下创建的。 重要的是要确保:如果指定了类加载器,则该类加载器必须能够访问表达式求值过程中涉及的所有类型。 如果你未指定类加载器,则会使用默认的类加载器(通常是执行表达式求值的线程的上下文类加载器)。
配置编译器的第二种方式适用于 SpEL 嵌入在其他组件内部,且无法通过配置对象对其进行配置的场景。在这些情况下,可以通过 JVM 系统属性(或通过
SpringProperties机制)将spring.expression.compiler.mode属性设置为SpelCompilerMode枚举值之一(off、immediate或mixed)。