Appearance
Spring 核心:表达式语言(SpEL)
Tip:基于 Spring Core 5.3.30 版本。
Spring 表达式语言(简称 “SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。其语法类似于统一表达式语言(Unified EL),但提供了额外的功能,最显著的是方法调用和基本的字符串模板功能。
虽然市面上存在多种其他 Java 表达式语言(如 OGNL、MVEL 和 JBoss EL 等),Spring 表达式语言的创建旨在为 Spring 社区提供一个得到良好支持的单一表达式语言,可用于 Spring 产品组合中的所有产品。其语言特性由 Spring 产品组合中项目的需求驱动,包括在 Spring Tools for Eclipse 中提供代码补全支持的工具需求。尽管如此,SpEL 基于一个与技术无关的 API,允许在需要时集成其他表达式语言实现。
虽然 SpEL 是 Spring 产品组合中表达式求值的基础,但它并不直接绑定于 Spring,可以独立使用。为了保持自包含性,本章中的许多示例将 SpEL 视为一个独立的表达式语言。这需要创建一些引导基础设施类,例如解析器。大多数 Spring 用户无需处理这些基础设施,而只需编写用于求值的表达式字符串。这种典型用法的示例是将 SpEL 集成到基于 XML 或注解的 Bean 定义中,如表达式支持定义 Bean 所示。
本章介绍了表达式语言的特性、其 API 以及语言语法。在多个地方,使用 Inventor 和 Society 类作为表达式求值的目标对象。这些类的声明以及用于填充它们的数据列在本章末尾。
该表达式语言支持以下功能:
- 字面值表达式
- 布尔和关系运算符
- 正则表达式
- 类表达式
- 访问属性、数组、列表和映射
- 方法调用
- 关系运算符
- 赋值
- 调用构造函数
- Bean 引用
- 数组构造
- 内联列表
- 内联映射
- 三元运算符
- 变量
- 用户定义函数
- 集合投影
- 集合选择
- 模板化表达式
1. 求值
本节介绍 SpEL 接口及其表达式语言的简单使用。完整的语言参考可在语言参考中找到。
以下代码展示了使用 SpEL API 求值字面值字符串表达式 Hello World:
Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();Note:
message变量的值为'Hello World'。
你最有可能使用的 SpEL 类和接口位于 org.springframework.expression 包及其子包(如 spel.support)中。
ExpressionParser 接口负责解析表达式字符串。在前述示例中,表达式字符串是一个由单引号包围的字符串字面值。Expression 接口负责对之前定义的表达式字符串进行求值。调用 parser.parseExpression 和 exp.getValue 时,可能会抛出两个异常:ParseException 和 EvaluationException。
SpEL 支持广泛的功能,例如调用方法、访问属性和调用构造函数。
以下是方法调用的示例,我们在字符串字面值上调用 concat 方法:
Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();Note:
message的值现在是'Hello World!'。
以下示例展示了调用 JavaBean 属性的情况,调用 String 类的 Bytes 属性:
Java
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();Note:此行将字面值转换为字节数组。
SpEL 还支持使用标准的点号语法(例如 prop1.prop2.prop3)访问嵌套属性,并支持相应的属性值设置。也可以访问公共字段。
以下示例展示如何使用点号语法获取字面值的长度:
Java
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = (Integer) exp.getValue();Note:
'Hello World'.bytes.length给出字面值的长度。
可以调用 String 的构造函数而不是使用字符串字面值,如下例所示:
Java
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
String message = exp.getValue(String.class);Note:从字面值构造一个新的
String并将其转换为大写。
注意泛型方法的使用:public <T> T getValue(Class<T> desiredResultType)。使用此方法无需将表达式的值强制转换为期望的结果类型。如果值无法转换为类型 T 或通过注册的类型转换器进行转换,则会抛出 EvaluationException。
SpEL 更常见的用法是提供一个针对特定对象实例(称为根对象)求值的表达式字符串。以下示例展示了如何从 Inventor 类实例中检索 name 属性或创建布尔条件:
Java
// 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 == true1.1. 理解 EvaluationContext
EvaluationContext 接口用于在求值表达式时解析属性、方法或字段,并帮助执行类型转换。Spring 提供了两种实现:
SimpleEvaluationContext:暴露一组基本的 SpEL 语言特性和配置选项,适用于不需要完整 SpEL 语言语法且应受到合理限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。StandardEvaluationContext:暴露完整的 SpEL 语言特性和配置选项。可用于指定默认根对象并配置所有可用的求值相关策略。
SimpleEvaluationContext 设计为仅支持 SpEL 语言语法的子集。它不包括 Java 类型引用、构造函数和 Bean 引用。它还要求你明确选择对表达式中属性和方法的支持级别。默认情况下,create() 静态工厂方法仅启用对属性的只读访问。你还可以获取一个构建器来配置所需的确切支持级别,针对以下一种或多种组合:
- 仅自定义
PropertyAccessor(不使用反射) - 数据绑定属性,仅限只读访问
- 数据绑定属性,支持读写
1.1.1. 类型转换
默认情况下,SpEL 使用 Spring 核心中提供的转换服务(org.springframework.core.convert.ConversionService)。此转换服务内置了许多常见转换的转换器,同时完全可扩展,允许添加类型之间的自定义转换。此外,它支持泛型。这意味着,当你在表达式中使用泛型类型时,SpEL 会尝试进行转换,以保持它遇到的任何对象的类型正确性。
这在实践中意味着什么?假设使用 setValue() 赋值来设置一个 List 属性。该属性的类型实际上是 List<Boolean>。SpEL 能识别出列表元素在放入之前需要转换为 Boolean。以下示例展示了如何实现:
Java
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);1.2. 解析器配置
可以通过使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)配置 SpEL 表达式解析器。该配置对象控制某些表达式组件的行为。例如,如果你在数组或集合中索引,且指定索引处的元素为 null,SpEL 可以自动创建该元素。这在使用由属性引用链组成的表达式时很有用。如果你在数组或列表中索引并指定一个超出当前数组或列表大小的索引,SpEL 可以自动扩展数组或列表以适应该索引。为了在指定索引处添加元素,SpEL 会尝试使用元素类型的默认构造函数创建元素,然后设置指定值。如果元素类型没有默认构造函数,则会添加 null 到数组或列表中。如果没有内置或自定义转换器知道如何设置该值,则在指定索引处数组或列表中将保留 null。以下示例展示了如何自动扩展列表:
Java
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 String1.3. SpEL 编译
Spring Framework 4.1 引入了一个基本的表达式编译器。表达式通常以解释方式执行,这在求值时提供了大量的动态灵活性,但无法提供最佳性能。对于偶尔使用的表达式,这种方式是可以接受的,但当其他组件(如 Spring Integration)使用时,性能可能非常重要,而动态性则并非真正必需。
SpEL 编译器旨在满足这一需求。在求值过程中,编译器会生成一个 Java 类,该类在运行时体现了表达式的行为,并利用该类实现更快的表达式求值。由于表达式周围缺乏类型信息,编译器在执行编译时会使用解释性求值期间收集的信息。例如,它无法仅从表达式中得知属性引用的类型,但在第一次解释性求值期间,它会发现其类型。当然,如果各种表达式元素的类型随时间变化,基于此类衍生信息的编译可能会在后期引发问题。因此,编译最适合类型信息在重复求值中不会发生变化的表达式。
考虑以下基本表达式:
Java
someArray[0].someProperty.someOtherProperty < 0.1由于上述表达式涉及数组访问、属性解引用和数值运算,其性能提升可能非常显著。在运行 50000 次迭代的微基准测试中,使用解释器求值耗时 75 毫秒,而使用编译后的表达式仅需 3 毫秒。
1.3.1. 编译器配置
编译器默认未启用,但可以通过两种不同方式启用。你可以通过解析器配置过程(此前讨论过)启用它,或者在 SpEL 使用嵌入到其他组件中时通过 Spring 属性启用。本节将讨论这两种选项。
编译器可在三种模式下运行,这些模式在 org.springframework.expression.spel.SpelCompilerMode 枚举中定义。模式如下:
OFF(默认):编译器关闭。IMMEDIATE:在即时模式下,表达式会尽快编译,通常在第一次解释性求值后。如果编译后的表达式失败(通常由于类型变化,如前所述),表达式求值的调用者会收到异常。MIXED:在混合模式下,表达式会随时间在解释模式和编译模式之间无声切换。经过若干次解释运行后,它们会切换为编译形式,如果编译形式出现问题(例如类型变化,如前所述),表达式会自动切换回解释形式。之后,它可能会生成另一个编译形式并切换过去。基本上,用户在IMMEDIATE模式下可能收到的异常会在内部处理。
IMMEDIATE 模式的存在是因为 MIXED 模式可能对具有副作用的表达式造成问题。如果编译后的表达式在部分成功后失败,它可能已经执行了某些影响系统状态的操作。如果发生这种情况,调用者可能不希望它在解释模式下无声地重新运行,因为部分表达式可能会运行两次。
选择模式后,使用 SpelParserConfiguration 配置解析器。以下示例展示如何操作:
Java
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 嵌入到其他组件中且无法通过配置对象进行配置的情况。在这些情况下,可以通过 JVM 系统属性(或通过 SpringProperties 机制)设置 spring.expression.compiler.mode 属性为 SpelCompilerMode 枚举值之一(off、immediate 或 mixed)。
1.3.2. 编译器限制
自 Spring Framework 4.1 起,基本编译框架已就位。然而,该框架尚不支持编译所有类型的表达式。最初的重点放在性能关键上下文中可能使用的常见表达式上。目前以下类型的表达式无法编译:
- 涉及赋值的表达式
- 依赖转换服务的表达式
- 使用自定义解析器或访问器的表达式
- 使用选择或投影的表达式
未来将支持更多类型的表达式编译。
2. Bean 定义中的表达式
你可以在基于 XML 或注解的配置元数据中使用 SpEL 表达式来定义 BeanDefinition 实例。在这两种情况下,定义表达式的语法形式为 #{ <expression string> }。
2.1. XML 配置
可以使用表达式设置属性或构造函数参数值,如以下示例所示:
XML
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- other properties -->
</bean>应用程序上下文中所有 Bean 都可作为预定义变量使用,其名称为其常用 Bean 名称。这包括标准上下文 Bean,例如 environment(类型为 org.springframework.core.env.Environment)以及 systemProperties 和 systemEnvironment(类型为 Map<String, Object>),用于访问运行时环境。
以下示例展示了如何将 systemProperties Bean 作为 SpEL 变量访问:
XML
<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<!-- other properties -->
</bean>注意,在此处无需为预定义变量添加 # 前缀。
你还可以通过名称引用其他 Bean 属性,如以下示例所示:
XML
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- other properties -->
</bean>
<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
<property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>
<!-- other properties -->
</bean>2.2. 注解配置
要指定默认值,可以在字段、方法以及方法或构造函数参数上使用 @Value 注解。
以下示例设置字段的默认值:
Java
public class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}以下示例展示了在属性 setter 方法上的等效用法:
Java
public class PropertyValueTestBean {
private String defaultLocale;
@Value("#{ systemProperties['user.region'] }")
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}自动注入的方法和构造函数也可以使用 @Value 注解,如以下示例所示:
Java
public class SimpleMovieLister {
private MovieFinder movieFinder;
private String defaultLocale;
@Autowired
public void configure(MovieFinder movieFinder,
@Value("#{ systemProperties['user.region'] }") String defaultLocale) {
this.movieFinder = movieFinder;
this.defaultLocale = defaultLocale;
}
// ...
}Java
public class MovieRecommender {
private String defaultLocale;
private CustomerPreferenceDao customerPreferenceDao;
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
@Value("#{systemProperties['user.country']}") String defaultLocale) {
this.customerPreferenceDao = customerPreferenceDao;
this.defaultLocale = defaultLocale;
}
// ...
}3. 语言参考
本节描述了 Spring 表达式语言的工作原理。它涵盖以下主题:
3.1. 字面值表达式
SpEL 支持以下类型的字面值表达式:
- 字符串
- 数值:整数(
int或long)、十六进制(int或long)、实数(float或double) - 布尔值:
true或false null
字符串可以用单引号(')或双引号(")分隔。要在用单引号括起来的字符串字面值中包含单引号,使用两个相邻的单引号字符。类似地,要在用双引号括起来的字符串字面值中包含双引号,使用两个相邻的双引号字符。
数值支持使用负号、指数表示法和小数点。默认情况下,实数使用 Double.parseDouble() 进行解析。
以下列表展示了字面值的简单用法。通常,它们不会像这样单独使用,而是作为更复杂表达式的一部分,例如在一侧使用字面值进行逻辑比较运算,或作为方法的参数。
Java
ExpressionParser parser = new SpelExpressionParser();
// evaluates to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
// evaluates to "Tony's Pizza"
String pizzaParlor = (String) parser.parseExpression("'Tony''s Pizza'").getValue();
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
// evaluates to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
boolean trueValue = (Boolean) parser.parseExpression("true").getValue();
Object nullValue = parser.parseExpression("null").getValue();3.2. 属性、数组、列表、映射和索引器
使用属性引用进行导航很简单。为此,使用点号表示嵌套属性值。Inventor 类的实例 pupin 和 tesla 已使用示例中使用的类部分列出的数据进行了填充。要 “向下” 导航对象图并获取 Tesla 的出生年份和 Pupin 的出生城市,我们使用以下表达式:
Java
// evaluates to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);
String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);Note:属性名称的首字母允许大小写不敏感。因此,上述示例中的表达式可以分别写为
Birthdate.Year + 1900和PlaceOfBirth.City。此外,属性还可以选择通过方法调用访问,例如使用getPlaceOfBirth().getCity()而不是placeOfBirth.city。
数组和列表的内容通过方括号表示法获取,如以下示例所示:
Java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// Inventions Array
// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String.class);
// Members List
// evaluates to "Nikola Tesla"
String name = parser.parseExpression("members[0].name").getValue(
context, ieee, String.class);
// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("members[0].inventions[6]").getValue(
context, ieee, String.class);映射的内容通过在括号中指定字面键值获取。在以下示例中,因为 officers 映射的键是字符串,我们可以指定字符串字面值:
Java
// Officer's Dictionary
Inventor pupin = parser.parseExpression("officers['president']").getValue(
societyContext, Inventor.class);
// evaluates to "Idvor"
String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
societyContext, String.class);
// setting values
parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue(
societyContext, "Croatia");3.3. 内联列表
你可以通过 {} 表示法直接在表达式中表达列表。
Java
// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);{} 本身表示空列表。出于性能考虑,如果列表完全由固定字面值组成,则会创建一个常量列表来表示该表达式(而不是在每次求值时构建新列表)。
3.4. 内联映射
你还可以通过 {key:value} 表示法直接在表达式中表达映射。以下示例展示了如何操作:
Java
// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);{:} 本身表示空映射。出于性能考虑,如果映射本身由固定字面值或其他嵌套常量结构(列表或映射)组成,则会创建一个常量映射来表示该表达式(而不是在每次求值时构建新映射)。映射键的引用是可选的(除非键包含点号(.))。上述示例未使用引号括起来的键。
3.5. 数组构造
你可以使用熟悉的 Java 语法构建数组,可选择提供初始化器以在构造时填充数组。以下示例展示了如何操作:
Java
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);目前在构造多维数组时无法提供初始化器。
3.6. 方法
你可以使用典型的 Java 编程语法调用方法。你还可以在字面值上调用方法。还支持可变参数。以下示例展示了如何调用方法:
Java
// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
societyContext, Boolean.class);3.7. 运算符
Spring 表达式语言支持以下类型的运算符:
3.7.1. 关系运算符
关系运算符(等于、不等于、小于、小于等于、大于、大于等于)使用标准的运算符表示法支持。以下列表展示了一些运算符示例:
Java
// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);Note
与
null进行大于和小于比较遵循一个简单规则:null被视为无(而不是零)。因此,任何其他值始终大于null(X > null始终为true),且没有值会小于无(X < null始终为false)。如果你更倾向于数值比较,避免基于数字的
null比较,改为与零比较(例如,X > 0或X < 0)。
除了标准关系运算符外,SpEL 还支持 instanceof 和基于正则表达式的 matches 运算符。以下列表展示了这两者的示例:
Java
// evaluates to false
boolean falseValue = parser.parseExpression(
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression(
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression(
"'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);Caution:要小心基本类型,因为它们会被立即装箱为其包装类型。例如,
1 instanceof T(int)求值为false,而1 instanceof T(Integer)求值为true,符合预期。
每个符号运算符还可以指定为纯字母等价形式。这避免了在嵌入表达式的文档类型(如 XML 文档)中符号具有特殊含义的问题。文本等价形式包括:
lt(<)gt(>)le(<=)ge(>=)eq(==)ne(!=)div(/)mod(%)not(!)
所有文本运算符对大小写不敏感。
3.7.2. 逻辑运算符
SpEL 支持以下逻辑运算符:
and(&&)or(||)not(!)
以下示例展示了如何使用逻辑运算符:
Java
// -- AND --
// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- OR --
// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- NOT --
// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);3.7.3. 数学运算符
加法运算符(+)可用于数字和字符串。减法(-)、乘法(*)和除法(/)运算符仅适用于数字。模运算(%)和幂运算(^)也适用于数字。标准的运算符优先级得到强制执行。以下示例展示了数学运算符的使用:
Java
// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2
String testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String.class); // 'test string'
// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4
double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000
// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0
// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0
// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3
int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1
// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -213.7.4. 赋值运算符
要设置属性,使用赋值运算符(=)。这通常在调用 setValue 时完成,但也可以在调用 getValue 时执行。以下列表展示了两种使用赋值运算符的方式:
Java
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic");
// alternatively
String aleks = parser.parseExpression(
"name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);3.8. 类型
可以使用特殊的 T 运算符指定 java.lang.Class 的实例(类型)。静态方法也通过此运算符调用。StandardEvaluationContext 使用 TypeLocator 查找类型,而 StandardTypeLocator(可替换)内置了对 java.lang 包的理解。这意味着对 java.lang 包内类型的 T() 引用无需完全限定,但其他类型引用必须完全限定。以下示例展示了如何使用 T 运算符:
Java
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
boolean trueValue = parser.parseExpression(
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
.getValue(Boolean.class);3.9. 构造函数
可以使用 new 运算符调用构造函数。除了位于 java.lang 包中的类型(Integer、Float、String 等)外,所有类型都应使用完全限定的类名。以下示例展示了如何使用 new 运算符调用构造函数:
Java
Inventor einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor.class);
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);3.10. 变量
可以使用 #variableName 语法在表达式中引用变量。变量通过 EvaluationContext 实现上的 setVariable 方法设置。
Note
有效的变量名必须由以下支持的字符中的一个或多个组成:
- 字母:
A到Z和a到z- 数字:
0到9- 下划线:
_- 美元符号:
$
以下示例展示了如何使用变量:
Java
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");
parser.parseExpression("name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()) // "Mike Tesla"3.10.1. #this 和 #root 变量
#this 变量始终定义并指向当前求值对象(未限定引用在此对象上解析)。#root 变量始终定义并指向根上下文对象。尽管 #this 在表达式组件求值时可能会变化,但 #root 始终指向根对象。以下示例展示了如何使用 #this 和 #root 变量:
Java
// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));
// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);
// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
"#primes.?[#this>10]").getValue(context);3.11. 函数
可以通过注册用户自定义函数扩展 SpEL,这些函数可在表达式字符串中调用。函数通过 EvaluationContext 注册。以下示例展示了如何注册用户自定义函数:
Java
Method method = ...;
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);例如,考虑以下反转字符串的实用方法:
Java
public abstract class StringUtils {
public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder(input.length());
for (int i = 0; i < input.length(); i++) {
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
}然后可以注册并使用上述方法,如以下示例所示:
Java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class));
String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);3.12. Bean 引用
如果求值上下文已配置了 Bean 解析器,可以使用 @ 符号从表达式中查找 Bean。以下示例展示了如何操作:
Java
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);要访问工厂 Bean 本身,应使用 & 符号作为 Bean 名称的前缀。以下示例展示了如何操作:
Java
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);3.13. 三元运算符(If-Then-Else)
可以使用三元运算符在表达式中执行 if-then-else 条件逻辑。以下列表展示了一个简单示例:
Java
String falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String.class);在这种情况下,布尔值 false 导致返回字符串值 'falseExp'。以下是一个更现实的示例:
Java
parser.parseExpression("name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");
expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
"+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";
String queryResultString = parser.parseExpression(expression)
.getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"请参阅下一节关于 Elvis 运算符的内容,了解三元运算符的更简短语法。
3.14. Elvis 运算符
Elvis 运算符是三元运算符语法的简化形式,在 Groovy 语言中使用。使用三元运算符语法时,通常需要重复两次变量,如以下示例所示:
Java
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");相反,你可以使用 Elvis 运算符(因其与 Elvis 的发型相似而得名)。以下示例展示了如何使用 Elvis 运算符:
Java
ExpressionParser parser = new SpelExpressionParser();
String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name); // 'Unknown'以下列表展示了一个更复杂的示例:
Java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Nikola Tesla
tesla.setName(null);
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis PresleyNote
你可以使用 Elvis 运算符在表达式中应用默认值。以下示例展示了如何在
@Value表达式中使用 Elvis 运算符:Java@Value("#{systemProperties['pop3.port'] ?: 25}")这将注入系统属性
pop3.port(如果已定义),否则注入 25。
3.15. 安全导航运算符
安全导航运算符用于避免 NullPointerException,源自 Groovy 语言。通常,当你引用一个对象时,可能需要在访问对象的属性或方法之前验证它是否为 null。为了避免这种情况,安全导航运算符会返回 null 而不是抛出异常。以下示例展示了如何使用安全导航运算符:
Java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!3.16. 集合选择
选择是表达式语言的一项强大功能,允许你通过从源集合的条目中选择来将其转换为另一个集合。
选择使用 .?[selectionExpression] 语法。它过滤集合并返回包含原始元素子集的新集合。例如,选择可以轻松获取塞尔维亚发明者列表,如以下示例所示:
Java
List<Inventor> list = (List<Inventor>) parser.parseExpression(
"members.?[nationality == 'Serbian']").getValue(societyContext);选择支持数组以及实现 java.lang.Iterable 或 java.util.Map 的任何对象。对于列表或数组,选择条件针对每个单独元素进行求值。对于映射,选择条件针对每个映射条目(Java 类型为 Map.Entry 的对象)进行求值。每个映射条目可以通过其 key 和 value 属性访问以用于选择。
以下表达式返回一个新映射,其中包含原始映射中值小于 27 的元素:
Java
Map newMap = parser.parseExpression("map.?[value<27]").getValue();除了返回所有选定元素外,你还可以仅检索第一个或最后一个元素。要获取匹配选择条件的第一个元素,语法为 .^[selectionExpression]。要获取最后一个匹配的选择,语法为 .$[selectionExpression]。
3.17. 集合投影
投影允许集合驱动子表达式的求值,结果是一个新集合。投影的语法为 .![projectionExpression]。例如,假设我们有一个发明者列表,但想要获取他们的出生城市列表。实际上,我们希望对发明者列表中的每个条目求值 placeOfBirth.city。以下示例使用投影实现这一点:
Java
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]");投影支持数组以及实现 java.lang.Iterable 或 java.util.Map 的任何对象。当使用映射驱动投影时,投影表达式针对映射中的每个条目(表示为 Java 的 Map.Entry)进行求值。跨越映射的投影结果是一个列表,由对每个映射条目求值的投影表达式组成。
3.18. 表达式模板
表达式模板允许将字面文本与一个或多个求值块混合。每个求值块使用你可以定义的前缀和后缀字符分隔。常见的选择是使用 #{ } 作为分隔符,如以下示例所示:
Java
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);
// evaluates to "random number is 0.7038186818312008"该字符串通过将字面文本 'random number is ' 与在 #{ } 分隔符内求值的表达式结果(在此案例中为调用 random() 方法的结果)连接而成。parseExpression() 方法的第二个参数类型为 ParserContext。ParserContext 接口用于影响表达式的解析方式,以支持表达式模板功能。TemplateParserContext 的定义如下:
Java
public class TemplateParserContext implements ParserContext {
public String getExpressionPrefix() {
return "#{";
}
public String getExpressionSuffix() {
return "}";
}
public boolean isTemplate() {
return true;
}
}4. 示例中使用的类
本节列出了本章示例中使用的类。
Inventor.JavaJava
package org.spring.samples.spel.inventor;
import java.util.Date;
import java.util.GregorianCalendar;
public class Inventor {
private String name;
private String nationality;
private String[] inventions;
private Date birthdate;
private PlaceOfBirth placeOfBirth;
public Inventor(String name, String nationality) {
GregorianCalendar c= new GregorianCalendar();
this.name = name;
this.nationality = nationality;
this.birthdate = c.getTime();
}
public Inventor(String name, Date birthdate, String nationality) {
this.name = name;
this.nationality = nationality;
this.birthdate = birthdate;
}
public Inventor() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNationality() {
return nationality;
}
public void setNationality(String nationality) {
this.nationality = nationality;
}
public Date getBirthdate() {
return birthdate;
}
public void setBirthdate(Date birthdate) {
this.birthdate = birthdate;
}
public PlaceOfBirth getPlaceOfBirth() {
return placeOfBirth;
}
public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
this.placeOfBirth = placeOfBirth;
}
public void setInventions(String[] inventions) {
this.inventions = inventions;
}
public String[] getInventions() {
return inventions;
}
}Java
package org.spring.samples.spel.inventor;
public class PlaceOfBirth {
private String city;
private String country;
public PlaceOfBirth(String city) {
this.city=city;
}
public PlaceOfBirth(String city, String country) {
this(city);
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String s) {
this.city = s;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}Java
package org.spring.samples.spel.inventor;
import java.util.*;
public class Society {
private String name;
public static String Advisors = "advisors";
public static String President = "president";
private List<Inventor> members = new ArrayList<Inventor>();
private Map officers = new HashMap();
public List getMembers() {
return members;
}
public Map getOfficers() {
return officers;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMember(String name) {
for (Inventor inventor : members) {
if (inventor.getName().equals(name)) {
return true;
}
}
return false;
}
}