技术交流群:
Spring表达式语言(简称SpEl)是非常强大的表达式语言,它支持在运行时查询和手动操作对象图。这个语言语法类似EL但是提供了额外的特性,最著名的是方法调用和基本字符串模板功能。
虽然还有其他几种Java表达式语言OGNL
、MVEL
和JBoss EL
可用,但创建Spring表达式语言是为了向Spring社区提供一种受良好支持的表达式语言,可以跨Spring组合中的所有产品使用。它的语言特性是由Spring组合中的项目需求驱动的,包括用于Eclipse的Spring工具中的代码完成支持的工具需求。也就是说,SpEL基于与技术无关的API,如果需要,可以将其他表达语言实现集成在一起。虽然SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。为了自我包含,本章中的许多例子使用SpEL,好像它是一种独立的表达语言。这需要创建一些引导基础设施类,比如解析器。大多数Spring用户不需要处理这个基础设施,相反,而只需编写表达式字符串进行评估。这种典型用法的一个示例是将SpEL集成到创建XML或基于注解的Bean定义中,如Expression
支持中所示,用于定义bean定义。
本章介绍了表达语言,其API和语言语法的功能。在许多地方,Inventor
和Society
类都用作表达评估的目标对象。这些类声明和用于填充它们的数据在本章末尾列出(末尾给出示例代码)。
表达式语言支持以下功能:
- 文字表达式
- 布尔运算符和关系运算符
- 常用表达式
- 类表达式
- 访问属性,数组,列表和映射
- 方法调用
- 关系运算符
- 分配
- 调用构造函数
- Bean引用
- 数组构造
- 内联列表
- 内联Map
- 三元运算符
- 变量
- 用户定义的功能
- 集合投影
- 集合选择
- 模板表达式
4.1 评估
本节介绍SpEL接口及其表达语言的简单用法。完整的语言参考可以在“语言参考”中找到。
以下代码介绍了SpEL API,用于评估文字字符串表达式Hello World
。
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); //1
String message = (String) exp.getValue();
- 消息值变量使用
'Hello World'
。
你最可能使用的SpEL类和接口位于org.springframework.expression
包及其子包中,例如spel.support
。
ExpressionParser
接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。Expression
接口负责评估先前定义的表达式字符串。分别调用parser.parseExpression
和exp.getValue
时,可以引发两个异常,ParseException
和EvaluationException
。
SpEL支持多种功能,例如调用方法、访问属性和调用构造函数。
在以下方法调用示例中,我们在字符串文字上调用concat
方法:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); //1
String message = (String) exp.getValue();
- 消息值 '
Hello World!
'是 ‘Hello World!
’。
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); //1
byte[] bytes = (byte[]) exp.getValue();
- 此行将文字转换为字节数组。
SpEL还通过使用标准的点符号(例如prop1.prop2.prop3
)以及相应的属性值设置值支持嵌套属性。也可以访问公共字段。
下面的示例演示如何使用点表示法获取文字的长度:
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); //1
int length = (Integer) exp.getValue();
“Hello World” .bytes.length
给出文字的长度。
可以调用String的构造函数,而不是使用字符串文字,如以下示例所示:
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
属性或如何创建布尔条件:
// 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);
4.1.1 理解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识别到列表中的元素在放入列表之前需要转换为Boolean
。下面例子显示怎样去做:
class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}
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);
4.1.2 解析配置
可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration
)配置SpEL表达式解析器。配置对象控制某些表达式组件的行为。例如,如果你索引到数组或集合中并且指定索引处的元素为null
,则可以自动地创建该元素。当使用由属性引用链组成的表达式时,这很有用。如果你索引到数组或列表中并指定了超出数组或列表当前大小末尾的索引时则可以自动增长数组或列表以容纳该索引。下面的示例演示如何自动增加列表:
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
4.1.3 SpEL编译器
Spring Framework 4.1包含一个基本的表达式编译器。通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵活性,但不能提供最佳性能。对于偶尔使用表达式,这很好,但是,当与其他组件(如Spring Integration)集成一起使用时,性能可能非常重要,并且不需要动态性。
SpEL编译器旨在满足这一需求。在评估过程中,编译器会生成一个Java类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式评估。由于表达式周围缺乏输入,编译器在执行编译时使用在解释表达式评估期间收集的信息。例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释评估时就知道它是什么。当然,如果各种表达元素的类型随时间变化,则基于此类派生信息进行编译会在以后引起麻烦。因此,编译最适合类型信息在重复评估时不会改变的表达式。
考虑下面简单表达式:
someArray[0].someProperty.someOtherProperty < 0.1
由于前面的表达式涉及数组访问,一些属性取消引用和数字运算,因此性能提升可能非常明显。在一个示例中,进行了50000次迭代的微基准测试,使用解释器评估需要75毫秒,而使用表达式的编译版本仅需要3毫秒。
编译器配置
默认情况下不打开编译器,但是你可以通过两种不同的方式之一来打开它。当SpEL用法嵌入到另一个组件中时,可以使用解析器配置处理(前面讨论过)或使用系统属性来打开它。本节讨论这两个选项。
编译器可以在org.springframework.expression.spel.SpelCompilerMode
枚举中捕获的三种模式之一进行操作。模式如下:
OFF(default)
:编译器被关闭IMMEDIATE
:在立即模式下,表达式将尽快编译。通常是在第一次解释评估之后。如果编译的表达式失败(通常是由于类型更改,如前所述),则表达式评估的调用者将收到异常。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);
当指定编译器模式时,还可以指定一个类加载器(允许传递null
)。编译后的表达式定义在提供的任意下创建的子类加载器中。重要的是要确保,如果指定了类加载器,则它可以查看表达式评估过程中涉及的所有类型。如果未指定类加载器,则使用默认的类加载器(通常是在表达式评估期间运行的线程的上下文类加载器)。
第二种配置编译器的方法是将SpEL嵌入到其他组件中,并且可能无法通过配制对象进行配置。在这些情况下,可以使用系统属性。你可以将spring.expression.compiler.mode
属性设置为SpelCompilerMode
枚举值之一(OFF
、IMMEDIATE
或MIXED
)。
编译器限制
从Spring Framework 4.1开始,已经有了基本的编译框架。但是,该框架尚不支持编译每种表达式。最初的重点是可能在性能关键型上下文中使用的通用表达式。
目前无法编译以下类型的表达式:
- 涉及赋值的表达
- 表达式依赖转换服务
- 使用自定义解析器或访问器的表达式
- 使用选择或投影的表达式
4.2 在bean定义中的表达式
你可以将SpEL表达式与基于XML或基于注解的配置元数据一起使用,以定义BeanDefinition
实例。在这两种情况下,用于定义表达式的语法都采用#{<表达式字符串>}
的形式。
4.2.1 XML配置
可以使用表达式来设置属性或构造函数参数值,如以下示例所示:
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- other properties -->
</bean>
systemProperties
变量是预定义的,因此你可以在表达式中使用它,如以下示例显示示:
<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<!-- other properties -->
</bean>
请注意,在这种情况下,不必在预定义变量前加上#符号。
你还可以按名称引用其他bean属性,如以下示例所示:
<bean id=