书接上文
接下来到 Spring framework core 的第四大块 —— spring 表达式语言(SpEL)
spring 表达式语言(SpEL)
Spring 表达式语言(简称“SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法与 Unified EL 相似,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。
尽管还有其他几种 Java 表达式语言可用-OGNL,MVEL 和 JBoss EL,仅举几例-Spring 表达式语言的创建是为了向 Spring 社区提供一种受良好支持的表达式语言,该语言可用于以下版本中的所有产品春季投资组合。它的语言功能受 Spring 产品组合中项目的要求驱动,包括 Spring Tools for Eclipse 中代码完成支持的工具要求。也就是说,SpEL 基于与技术无关的 API,如果需要,可以将其他表达语言实现集成在一起。
虽然 SpEL 是 Spring 产品组合中表达评估的基础,但它并不直接与 Spring 绑定,可以独立使用。为了自成一体,本章中的许多示例都将 SpEL 用作独立的表达语言。这需要创建一些自举基础结构类,例如解析器。 Spring 的大多数用户不需要处理这种基础结构,而只能编写表达式字符串进行评估。这种典型用法的一个示例是将 SpEL 集成到创建 XML 或基于注释的 Bean 定义中,如 Expression 支持中定义的 Bean 定义所示。
本章介绍了表达语言,其 API 和语言语法的功能。在许多地方,Inventor 和 Society 类都用作表达评估的目标对象。这些类声明和用于填充它们的数据在本章末尾列出。
表达式语言支持以下功能:
- 文字表达
- 布尔运算符和关系运算符
- 正则表达式
- 类表达式
- 访问属性,数组,列表和映射
- 方法调用
- 关系运算符
- 分配
- 调用构造函数
- Bean 引用
- 数组构造
- 内联列表
- 内联映射
- 三元运算符
- 变数
- 用户定义的功能
- 集合投影
- 集合选择
- 模板表达式
求值
本节介绍 SpEL 接口及其表达语言的简单用法。 完整的语言参考可以在“语言参考”中找到。
以下代码介绍了 SpEL API,用于计算文字字符串表达式 Hello World
。
1 | ExpressionParser parser = new SpelExpressionParser(); |
message 变量的值为“ Hello World”。
您最可能使用的 SpEL 类和接口位于 org.springframework.expression
包及其子包中,例如 spel.support
。
ExpressionParser
接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。Expression
接口负责评估先前定义的表达式字符串。分别调用 parser.parseExpression
和 exp.getValue
时,可以引发两个异常 ParseException
和 EvaluationException
。
SpEL 支持多种功能,例如调用方法,访问属性和调用构造函数。
在以下方法调用示例中,我们在字符串文字上调用 concat
方法:
1 | ExpressionParser parser = new SpelExpressionParser(); |
message == ‘Hello World!’
1 | ExpressionParser parser = new SpelExpressionParser(); |
SpEL 还通过使用标准的点符号(例如 prop1.prop2.prop3)以及相应的属性值设置来支持嵌套属性。 也可以访问公共字段。
下面的示例演示如何使用点表示法获取文字的长度:
1 | ExpressionParser parser = new SpelExpressionParser(); |
可以调用 String 的构造函数,而不是使用字符串文字,如以下示例所示:
1 | ExpressionParser parser = new SpelExpressionParser(); |
上例会从文字构造一个新的 String 并将其变为大写。
注意使用通用方法:public <T> T getValue(Class<T> desiredResultType)
。使用此方法无需将表达式的值强制转换为所需的结果类型。 如果该值不能转换为 T
类型或无法使用已注册的类型转换器转换,则将引发 EvaluationException
。
SpEL 的更常见用法是提供一个针对特定对象实例(称为根对象)进行评估的表达式字符串。 以下示例显示如何从 Inventor
类的实例检索 name
属性或如何创建布尔条件:
1 | // Create and set a calendar |
理解 EvaluationContext
在评估表达式以解析属性,方法或字段并帮助执行类型转换时,使用 EvaluationContext
接口。 Spring 提供了两种实现。
SimpleEvaluationContext
:针对不需要全部 SpEL 语言语法范围且应受到有意义限制的表达式类别,公开了 SpEL 基本语言功能和配置选项的子集。示例包括但不限于数据绑定表达式和基于属性的过滤器。StandardEvaluationContext
:公开了全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext
设计为仅支持 SpEL 语言语法的子集。它不包括 Java 类型引用,构造函数和 Bean 引用。它还要求您明确选择对表达式中的属性和方法的支持级别。默认情况下,create()
静态工厂方法仅启用对属性的读取访问。您还可以获取构建器来配置所需的确切支持级别,并针对以下一种或某种组合:
- 仅自定义
PropertyAccessor
(无反射) - 只读访问的数据绑定属性
- 读写的数据绑定属性
类型转换
默认情况下,SpEL 使用 Spring 核心中可用的转换服务(org.springframework.core.convert.ConversionService
)。此转换服务附带许多内置转换器,用于常见转换,但也可以完全扩展,以便您可以在类型之间添加自定义转换。此外,它是泛型感知的。这意味着,当您在表达式中使用泛型类型时,SpEL 会尝试进行转换以维护遇到的任何对象的类型正确性。
实际上这是什么意思?假设使用 setValue()
进行赋值来设置 List
属性。该属性的类型实际上是 List<Boolean>
。 SpEL 认识到列表中的元素在放入列表之前需要转换为布尔值。以下示例显示了如何执行此操作:
1 | class Simple { |
解析器配置
可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration
)配置 SpEL 表达式解析器。 配置对象控制某些表达式组件的行为。 例如,如果您索引到数组或集合中并且指定索引处的元素为 null
,则可以自动创建该元素。 当使用由属性引用链组成的表达式时,这很有用。 如果您索引到数组或列表中并指定了超出数组或列表当前大小末尾的索引,则可以自动增长数组或列表以容纳该索引。下面的示例演示如何自动增加列表:
1 | class Demo { |
SpEL 编译
Spring Framework 4.1 包含一个基本的表达式编译器。通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵活性,但不能提供最佳性能。对于偶尔使用表达式,这很好,但是,当与其他组件(例如 Spring Integration)一起使用时,性能可能非常重要,并且不需要动态性。
SpEL 编译器旨在满足这一需求。在评估过程中,编译器会生成一个 Java 类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式评估。由于缺少在表达式周围输入内容的信息,因此编译器在执行编译时会使用在表达式的解释式求值过程中收集的信息。例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释求值时就知道它是什么。当然,如果各种表达元素的类型随时间变化,则基于此类派生信息进行编译会在以后引起麻烦。因此,编译最适合类型信息在重复求值时不会改变的表达式。
考虑以下基本表达式:
1 | someArray[0].someProperty.someOtherProperty < 0.1 |
由于前面的表达式涉及数组访问,一些属性取消引用和数字运算,因此性能提升可能非常明显。在一个示例中,进行了 50000 次迭代的微基准测试,使用解释器评估需要 75 毫秒,而使用表达式的编译版本仅需要 3 毫秒。
编译器配置
两种不同的方式之一来打开它。当 SpEL 用法嵌入到另一个组件中时,可以使用解析器配置过程(前面讨论过)或使用系统属性来打开它。本节讨论这两个选项。
编译器可以在 org.springframework.expression.spel.SpelCompilerMode
枚举中捕获的三种模式之一进行操作。模式如下:
OFF
(默认):编译器已关闭。IMMEDIATE
:在立即模式下,将尽快编译表达式。通常是在第一次解释评估之后。如果编译的表达式失败(通常是由于类型更改,如前所述),则表达式求值的调用者将收到异常。MIXED
:在混合模式下,表达式会随着时间静默在解释模式和编译模式之间切换。经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(例如,如前面所述的类型更改),则表达式会自动再次切换回解释形式。稍后,它可能会生成另一个已编译的表单并切换到该表单。基本上,用户进入即时模式的异常是在内部处理的。
存在 IMMEDIATE
模式是因为 MIXED
模式可能会导致具有副作用的表达式出现问题。如果已编译的表达式在部分成功后就崩溃了,则它可能已经完成了影响系统状态的操作。如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,因为表达式的一部分可能运行了两次。
选择模式后,使用 SpelParserConfiguration
配置解析器。以下示例显示了如何执行此操作:
1 | SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, |
当指定编译器模式时,还可以指定一个类加载器(允许传递 null)。编译的表达式在提供的任何子类加载器中定义。重要的是要确保,如果指定了类加载器,则它可以查看表达式评估过程中涉及的所有类型。如果未指定类加载器,则使用默认的类加载器(通常是在表达式求值期间运行的线程的上下文类加载器)。
第二种配置编译器的方法是将 SpEL 嵌入到其他组件中,并且可能无法通过配置对象进行配置。 在这些情况下,可以使用系统属性。 您可以将 spring.expression.compiler.mode
属性设置为 SpelCompilerMode
枚举值之一(off
、immediate
或 mixed
)。
编译器限制
从 Spring Framework 4.1 开始,已经有了基本的编译框架。但是,该框架尚不支持编译每种表达式。最初的重点是可能在性能关键型上下文中使用的通用表达式。目前无法编译以下类型的表达式:
- 涉及赋值的表达
- 表达式依赖转换服务
- 使用自定义解析器或访问器的表达式
- 使用选择或投影的表达式
将来会编译更多类型的表达。
Bean 定义中的表达式
您可以将 SpEL 表达式与基于 XML 或基于注释的配置元数据一起使用,以定义 BeanDefinition
实例。 在这两种情况下,用于定义表达式的语法都采用 #{ <expression string> }
的形式。
XML 配置
可以使用表达式来设置属性或构造函数参数值,如以下示例所示:
1 | <bean id="numberGuess" class="org.spring.samples.NumberGuess"> |
应用程序上下文中的所有 bean 都可以使用其公共 bean 名称作为预定义变量使用。 这包括标准上下文 Bean,例如用于访问运行时环境的 environment
(类型为 org.springframework.core.env.Environment
)以及 systemProperties
和 systemEnvironment
(类型为 Map<String,Object>
)。
下面的示例显示了如何对 systemProperties
bean 作为 SpEL 变量的访问:
1 | <bean id="taxCalculator" class="org.spring.samples.TaxCalculator"> |
请注意,此处不必在预定义变量前加上 #
符号。
您还可以按名称引用其他 bean 属性,如以下示例所示:
1 | <bean id="numberGuess" class="org.spring.samples.NumberGuess"> |
注解配置
若要指定默认值,可以将 @Value
批注放置在字段,方法以及方法或构造函数参数上。
下面的示例设置字段变量的默认值:
1 | public class FieldValueTestBean { |
以下示例显示了等效的但使用属性设置器方法的示例:
1 | public class PropertyValueTestBean { |
自动装配的方法和构造函数也可以使用 @Value 批注,如以下示例所示:
1 | public class SimpleMovieLister { |
1 | public class MovieRecommender { |
语言参考
本节描述了 Spring Expression Language 的工作方式。 它涵盖以下主题:
- 文字表达式
- 属性,数组,列表,映射和索引器
- 内联列表
- 内联映射
- 数组构造
- 方法
- 运算符
- 类型
- 构造器
- 变数
- 函数
- Bean 引用
- 三元运算符(If-Then-Else)
- Elvis 运算符
- 安全导航运算符
文字表达式
支持的文字表达式的类型为字符串,数值(int,实数,十六进制),布尔值和 null。 字符串由单引号引起来。 要将单引号本身放在字符串中,请使用两个单引号字符。
以下清单显示了文字的简单用法。 通常,它们不是像这样孤立地使用,而是作为更复杂的表达式的一部分使用-例如,在逻辑比较运算符的一侧使用文字。
1 | ExpressionParser parser = new SpelExpressionParser(); |
数字支持使用负号,指数符号和小数点。 默认情况下,使用 Double.parseDouble()解析实数。
属性,数组,列表,映射和索引器
使用属性引用进行导航很容易。为此,请使用句点来指示嵌套的属性值。Inventor
类的实例 pupin
和 tesla
填充有 示例中使用的类 中列出的数据。要向下导航并获取特斯拉(Tesla)的出生年份和普平(Pupin)的出生城市,我们使用以下表达式:
1 | // evals to 1856 |
属性名称的首字母允许不区分大小写。 数组和列表的内容通过使用方括号表示法获得,如以下示例所示:
1 | ExpressionParser parser = new SpelExpressionParser(); |
通过在方括号内指定文字键值可以获取映射的内容。 在下面的示例中,由于 Officer 映射的键是字符串,因此我们可以指定字符串文字:
1 | // Officer's Dictionary |
内联列表
您可以使用 {}
表示法在表达式中直接表达列表。
1 | // evaluates to a Java list containing the four numbers |
{}
本身表示一个空列表。出于性能原因,如果列表本身完全由固定文字组成,则会创建一个常量列表来表示该表达式(而不是在每次求值时都建立一个新列表)。
内联映射
您也可以使用 {key:value}
表示法在表达式中直接表达映射。以下示例显示了如何执行此操作:
1 | // evaluates to a Java map containing the two entries |
{:}
本身意味着一个空的映射。出于性能原因,如果映射图本身由固定的文字或其他嵌套的常量结构(列表或映射)组成,则会创建一个常量映射来表示该表达式(而不是在每次求值时都构建一个新的映射)。映射键的引号是可选的。上面的示例使用的是不带引号的键。
构建数组
您可以使用熟悉的 Java 语法来构建数组,可以选择提供一个初始化程序,以在构造时填充该数组。 以下示例显示了如何执行此操作:
1 | int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); |
构造多维数组时,当前无法提供初始化程序。
方法
您可以使用典型的 Java 编程语法来调用方法。 您还可以在文字上调用方法。 还支持变量参数。 下面的示例演示如何调用方法:
1 | // string literal, evaluates to "bc" |
运算符
Spring 表达式语言支持以下几种运算符:
- 关系运算符
- 逻辑运算符
- 数学运算符
- 赋值运算符
关系运算符
使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于和大于或等于)。 以下清单显示了一些运算符示例:
1 | // evaluates to true |
对
null
的大于和小于比较遵循一个简单的规则:null
被视为无(不是零)。结果,任何其他值始终大于null
(X > null
始终为true
),并且其他任何值都不小于零(X < null
始终为false
)。如果您需要数字比较,请避免使用基于数字的
null
比较,而建议使用零进行比较(例如,X > 0
或X < 0
)。
除了标准的关系运算符外,SpEL 还支持 instanceof
和基于正则表达式的匹配运算符。 以下清单显示了两个示例:
1 | // evaluates to false |
请注意原始类型,因为它们会立即被包装为包装器类型,因此,按预期方式,
1 instanceof T(int)
的计算结果为false
,而1 instanceof T(Integer)
的计算结果为true
。
每个符号运算符也可以指定为纯字母等效项。 这样可以避免使用的符号对于嵌入表达式的文档类型具有特殊含义的问题(例如在 XML 文档中)。等效的文字是:
- lt (<)
- gt (>)
- le (<=)
- ge (>=)
- eq (==)
- ne (!=)
- div (/)
- mod (%)
- not (!)
所有的文本运算符都不区分大小写。
逻辑运算符
SpEL 支持以下逻辑运算符:
- 与(
&&
) - 或(
||
) - 非(
!
)
下面的示例演示如何使用逻辑运算符
1 | // -- AND -- |
数学运算符
您可以在数字和字符串上使用加法运算符。 您只能对数字使用减法,乘法和除法运算符。 您还可以使用模数(%)和指数幂(^)运算符。 强制执行标准运算符优先级。 以下示例显示了正在使用的数学运算符:
1 | // Addition |
赋值运算符
要设置属性,请使用赋值运算符(=)。 这通常在对 setValue
的调用内完成,但也可以在对 getValue
的调用内完成。 下面的清单显示了使用赋值运算符的两种方法:
1 | Inventor inventor = new Inventor(); |
类型
您可以使用特殊的 T
运算符来指定 java.lang.Class
(类型)的实例。静态方法也可以通过使用此运算符来调用。StandardEvaluationContext
使用 TypeLocator
查找类型,而 StandardTypeLocator
(可以替换)是在了解 java.lang
包的情况下构建的。 这意味着对 Java.lang
中的类型的 T()
引用不需要完全限定,但是所有其他类型引用都必须是完全限定的。下面的示例演示如何使用 T
运算符:
1 | Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); |
构造器
您可以使用 new
运算符来调用构造函数。 除基本类型(int
,float
等)和 String
以外的所有其他类都应使用完全限定的类名。 下面的示例演示如何使用 new 运算符调用构造函数:
1 | Inventor einstein = p.parseExpression( |
变量
您可以使用 #variableName
语法在表达式中引用变量。 通过在 EvaluationContext
实现上使用 setVariable
方法设置变量。
有效的变量名称必须由以下一个或多个受支持的字符组成。
- 字母:
A
到Z
和a
到z
- 数字:
0
到9
- 下划线:
_
- 美元符号:
$
以下示例显示了如何使用变量。
1 | Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); |
#this
和 #root
变量
#this
变量始终是定义的,并且引用当前的评估对象(反对解决不合格的引用)。#root
变量也是始终定义,并引用根上下文对象。尽管 #this
可能随表达式的组成部分的求值而变化,但 #root
始终引用根。 以下示例说明如何使用 #this
和 #root
变量:
1 | // create an array of integers |
函数
您可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SpEL。该函数通过 EvaluationContext
注册。下面的示例显示如何注册用户定义的函数:
1 | Method method = ...; |
例如,考虑以下用于反转字符串的实用程序方法:
1 | public abstract class StringUtils { |
然后,您可以注册并使用前面的方法,如以下示例所示:
1 | ExpressionParser parser = new SpelExpressionParser(); |
Bean 引用
如果评估上下文已使用 bean 解析器配置,则可以使用@符号从表达式中查找 bean。 以下示例显示了如何执行此操作:
1 | ExpressionParser parser = new SpelExpressionParser(); |
要访问工厂 bean 本身,您应该在 bean 名称前加上&符号。 以下示例显示了如何执行此操作:
1 | ExpressionParser parser = new SpelExpressionParser(); |
三元表达式
您可以使用三元运算符在表达式内部执行 if-then-else 条件逻辑。 以下清单显示了一个最小的示例:
1 | String falseString = parser.parseExpression( |
在这种情况下,布尔值 false 导致返回字符串值’falseExp’。 一个更现实的示例如下:
1 | parser.parseExpression("Name").setValue(societyContext, "IEEE"); |
有关三元运算符的更短语法,请参阅关于 Elvis 运算符的下一部分。
Elvis 运算符
Elvis 运算符是三元运算符语法的简化,并且在 Groovy 语言中使用。 使用三元运算符语法,通常必须将变量重复两次,如以下示例所示:
1 | String name = "Elvis Presley"; |
相反,您可以使用 Elvis 运算符(其命名类似于 Elvis 的发型)。 以下示例显示了如何使用 Elvis 运算符:
1 | ExpressionParser parser = new SpelExpressionParser(); |
以下显示了一个更复杂的示例:
1 | ExpressionParser parser = new SpelExpressionParser(); |
您可以使用 Elvis 运算符在表达式中应用默认值。 以下示例显示了如何在
@Value
表达式中使用 Elvis 运算符:
1
2 > "#{systemProperties['pop3.port'] ?: 25}") (
>
上例将注入系统属性
pop3.port
,为空将注入 25。
安全导航运算符
安全导航运算符用于避免 NullPointerException
,它来自 Groovy 语言。通常,当您引用一个对象时,可能需要在访问该对象的方法或属性之前验证其是否为 null。为了避免这种情况,安全导航运算符返回 null 而不是引发异常。 下面的示例演示如何使用安全导航操作符:
1 | ExpressionParser parser = new SpelExpressionParser(); |
集合选择
选择是一种强大的表达语言功能,可让您通过从源集合中进行选择来将其转换为另一个集合。
选择使用 .?[selectionExpression]
的语法。 它过滤集合并返回一个包含原始元素子集的新集合。例如,通过选择,我们可以轻松地获得 Serbian inventors 的列表,如以下示例所示:
1 | List<Inventor> list = (List<Inventor>) parser.parseExpression( |
在列表和映射上都可以选择。对于列表,将针对每个单独的列表元素评估选择标准。针对映射,针对每个映射条目(Java 类型 Map.Entry 的对象)评估选择标准。每个映射条目都有其键和值,可作为属性访问以供选择。
以下表达式返回一个新映射,该映射由原始映射中条目值小于 27 的那些元素组成:
1 | Map newMap = parser.parseExpression("map.?[value<27]").getValue(); |
除了返回所有选定的元素外,您只能检索第一个或最后一个值。为了获得与选择匹配的第一个条目,语法为 .^[selectionExpression]
。要获取最后一个匹配选择,语法为 .$[selectionExpression]
。
集合投影
投影使集合可以驱动子表达式的求值,结果是一个新的集合。投影的语法为 .![projectionExpression]
。例如,假设我们有一个 inventor 列表,但是想要他们出生的城市列表。实际上,我们希望为 inventor 列表中的每个条目计算“placeOfBirth.city”。 下面的示例使用投影来做到这一点:
1 | // returns ['Smiljan', 'Idvor' ] |
您还可以使用映射来驱动投影,在这种情况下,将针对映射中的每个条目(表示为 Java Map.Entry)对投影表达式进行评估。 跨映射的投影结果是一个列表,其中包含针对每个映射条目的投影表达式的评估。
表达式模板
表达式模板允许将文字文本与一个或多个评估块混合。每个评估块均以您可以定义的前缀和后缀字符分隔。常见的选择是使用 #{ }
作为分隔符,如以下示例所示:
1 | String randomPhrase = parser.parseExpression( |
通过将文字文本 'random number is '
与评估 #{ }
分隔符内的表达式的结果(在本例中为调用 random()
方法的结果)相连接来评估字符串。parseExpression()
方法的第二个参数的类型为 ParserContext
。ParserContext
接口用于影响表达式的解析方式,以支持表达式模板功能。TemplateParserContext
的定义如下:
1 | public class TemplateParserContext implements ParserContext { |
示例中使用的类
本节列出了本章示例中使用的类。
Inventor.java
1 | package org.spring.samples.spel.inventor; |
PlaceOfBirth.java
1 | package org.spring.samples.spel.inventor; |
Society.java
1 | package org.spring.samples.spel.inventor; |