前言
SpEL即Spring表达式语言(Spring Expression Language),它能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合。
SpEL是单独模块(org.springframework.expression
),只依赖于core模块,不依赖于其他模块,可以单独使用。
Hello World
首先从一个简单的“literal string expression”例子引入,所谓“literal string”就是指代码中直接使用双引号括起来的字符串(起码在Java下是这样):
1 | ExpressionParser parser = new SpelExpressionParser(); |
在上面的代码中,首先创建了解析器ExpressionParser
,负责解析表达式字符串,表达式字符串是由周围的单引号表示的字符串字面量。而后解析表达式,Expression
接口负责评估所定义的表达式字符串。最后对表达式进行求值,获取信息。
通过这一流程可以总结SpEL在求表达式值时的一般步骤:
- 创建解析器
- 解析表达式
- 构造上下文
- 表达式求值
其中第三步构造上下文是一个可选步骤,当需要准备上下文变量时会使用。
PS:在调用 parser.parseExpression
和 exp.getValue
时,可能分别抛出两个异常:ParseException
和 EvaluationException
。
PPS:Evaluate可以理解为“求值”或“计算”。
一些功能
SpEL支持多种功能,如调用方法、访问属性和调用构造函数,这里用一个例子快速过一下。
1 | ExpressionParser parser = new SpelExpressionParser(); |
更常见的用法
SpEL更常见的用法是提供一个表达式字符串,针对特定对象实例(称为根对象)进行求值。
1 | // 创建并设置一个日历对象 |
全部语法
关于如何编写正确的表达式,可以参见 https://docs.spring.io/spring-framework/reference/core/expressions/language-ref.html
EvaluationContext(求值上下文)
EvaluationContext
接口用于计算表达式,以解析属性、方法或字段,并帮助执行类型转换。Spring 提供了两种实现:
SimpleEvaluationContext
:该接口公开了基本 SpEL 功能和配置选项的子集,适用于不需要完整 SpEL 语言语法且应受到有意义限制的表达式类别,包括但不限于数据绑定表达式和基于属性的过滤器。StandardEvaluationContext
:提供全套 SpEL 功能和配置选项,可以用它来指定默认根对象,并配置所有可用的求值相关策略。
SimpleEvaluationContext
只支持 SpEL 语法的一个子集。它不包括 Java 类型引用、构造函数和 Bean 引用。它还要求使用者明确选择对表达式中属性和方法的支持级别,默认情况下,create()
静态工厂方法只能对属性进行读取访问。用户还可以获取一个构建器来配置所需的特定支持级别,针对以下一种或几种组合:
- Custom PropertyAccessor only (no reflection)
- Data binding properties for read-only access
- Data binding properties for read and write
类型转换
默认情况下,SpEL 使用 Spring core 中的转换服务(org.springframework.core.convert.ConversionService)。该转换服务为常见转换提供了许多内置转换器,但也具有完全可扩展性,因此您可以在类型间添加自定义转换。此外,它还具有泛型感知功能。这意味着,当您在表达式中使用泛型类型时,SpEL 会尝试进行转换,以保持遇到的任何对象的类型正确性。
举个例子,假设使用 setValue()
进行赋值是为了设置 List 属性。该属性的类型实际上是 List<Boolean>
。SpEL会识别到在将列表元素放入其中之前,需要将其转换为布尔值:
1 | class Simple { |
解析器配置
可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)来配置 SpEL 表达式解析器,该配置对象可控制某些表达式组件的行为。例如,在对数组或集合进行索引时,指定索引处的元素为空,SpEL 会自动创建该元素。这在使用由一连串属性引用组成的表达式时非常有用。如果用户对数组或列表进行索引,并指定一个超出数组或列表当前大小的索引,SpEL 可以自动增长数组或列表以容纳该索引。为了在指定的索引处添加元素,SpEL 将尝试使用元素类型的默认构造函数创建元素,然后再设置指定的值。如果元素类型没有默认构造函数,则会将空值添加到数组或列表中。如果没有转换器(内置或自定义的)知道如何设置值,空值将保留在数组或列表的指定索引处。
下面的示例演示了如何自动增长列表:
1 | class Demo { |
表达式编译(提升求值速度)
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
配置解析器。下面的示例演示了如何进行配置:
1 | SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, |
在指定编译器模式时,还可以指定一个类加载器(允许传递空值)。编译后的表达式将定义在一个子类加载器中,该类加载器将根据所提供的任何类型创建。重要的是,如果指定了类加载器,要确保它能看到表达式求值过程中涉及的所有类型。如果未指定类加载器,则会使用默认类加载器(通常是表达式求值过程中运行线程的上下文类加载器)。
配置编译器的第二种方法适用于 SpEL 嵌入其他组件的情况,这种情况下可能无法通过配置对象进行配置。在这种情况下,可以通过 JVM 系统属性(或 SpringProperties 机制)将 spring.expression.compiler.mode
属性设置为SpelCompilerMode
枚举值。
编译器的局限性
自 Spring Framework 4.1 以来,基本的编译框架已经到位。不过,该框架还不支持编译所有类型的表达式。最初的重点是可能在性能关键型上下文中使用的常见表达式。以下几种表达式暂时无法编译:
- 涉及赋值的表达式
- 依赖转换服务的表达式
- 使用自定义解析器或访问器的表达式
- 使用选择或投影的表达式
使用SpEL定义Bean
用户可以在基于XML或注解的配置元数据中使用SpEL表达式来定义BeanDefinition
实例。在这两种情况下,定义表达式的语法形式为#{ <expression string> }
。这里忽略XML配置方式,用的不多了。
基于注解的配置
要指定默认值,可在字段、方法、方法或构造函数参数上添加@Value
注解。
1、设置一个字段的默认值:1
2
3
4
5
6
7
8
9
10
11
12
13public class FieldValueTestBean {
// 读取的是服务部署机器的region
private String defaultLocale;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
2、注入属性set方法默认值:1
2
3
4
5
6
7
8
9
10
11
12
13public class PropertyValueTestBean {
private String defaultLocale;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
通过这种方式,当Spring容器创建 PropertyValueTestBean
对象时,它将调用 setDefaultLocale
方法并传入系统属性中 ‘user.region’ 对应的值,从而设置 defaultLocale
成员变量的值。
3、@Autowired
和构造函数也可以使用 @Value
注解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// Case1
public class SimpleMovieLister {
private MovieFinder movieFinder;
private String defaultLocale;
public void configure(MovieFinder movieFinder,
String defaultLocale){
this.movieFinder = movieFinder;
this.defaultLocale = defaultLocale;
}
// ...
}
// Case2
public class MovieRecommender {
private String defaultLocale;
private CustomerPreferenceDao customerPreferenceDao;
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
String defaultLocale){
this.customerPreferenceDao = customerPreferenceDao;
this.defaultLocale = defaultLocale;
}
// ...
}
更多功能
一些值得关注的功能…
ClassType表达式
使用T(Type)
来表示java.lang.Class
实例,Type
必须是类全限定名(java.lang
包下的类除外)。使用ClassType表达式还可以访问类静态方法及类静态字段。
下面的例子展示了如何使用ClassType表达式和如何进行条件判断:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// java.lang包内的不需要使用全限定名
parser.parseExpression("T(String)").getValue(Class.class);
// 类静态字段访问
parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
// 类静态方法调用
parser.parseExpression("#{T(java.lang.Math).random() * 100.0}", ParserContext.TEMPLATE_EXPRESSION).getValue(Double.class);
// 条件判断:
// 算数运算表达式
parser.parseExpression("1 + 1").getValue(Integer.class);
// 关系表达式
parser.parseExpression("2==2").getValue(Boolean.class);
// 逻辑表达式
parser.parseExpression("2>1 and (NOT true or NOT false)").getValue(boolean.class);
// instanceof表达式
parser.parseExpression("'xyz' instanceof T(Integer)").getValue(Boolean.class);
// 正则表达式
parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
函数、对象、变量的定义及引用
1 | // author: SilenceZheng66 |
空值处理
SpEL引入了Groovy语言中的安全导航运算符(对象|属性)?.属性
,用来避免?.
前边的表达式为null时抛出空指针异常,转而返回null。还可以使用?:
选择在表达式为null时返回默认值。
1 | Inventor tesla = new Inventor("Nikola Tesla", new Date(), "Serbian"); |
List运算
在 SpEL 中,?[]
和 ![]
分别表示集合选择(collection selection)和集合投影(collection projection)。?[]
表达式用于选择满足指定条件的集合元素,![]
表达式用于对集合进行投影操作。
集合投影或集合映射的基本思想是:通过对集合中的每个元素应用一个表达式,生成一个新的集合,该集合包含了原始集合中的元素经过某种转换后的值。
1 | // author: SilenceZheng66 |
访问Map
1 | // 访问map |
参考文献
[1] https://zhuanlan.zhihu.com/p/174786047
[2] https://zhuanlan.zhihu.com/p/149920813
[3] https://docs.spring.io/spring-framework/reference/core/expressions.html
后记
首发于 silencezheng.top,转载请注明出处。