从零开始 Spring Boot 31:Spring 表达式语言
图源:简书 (jianshu.com)
Spring表达式语言(Spring Expression Language,简称 “SpEL”)是一种强大的表达式语言,支持在运行时查询和操作对象图。该语言的语法与统一EL相似,但提供了额外的功能,最显著的是方法调用和基本的字符串模板功能。
评估
直接看一个简单示例:
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'");
String value = (String) expression.getValue();
System.out.println(value);
SpelExpressionParser
是一个Spel表达式解析器,主要实现了ExpressionParser
接口:
package org.springframework.expression;
public interface ExpressionParser {
Expression parseExpression(String expressionString) throws ParseException;
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}
org.springframework.expression
是Spring中SpEL相关功能的包。
ExpressionParser.parseExpression()
可以解析SpEL表达式并返回一个Expression
对象。这里的'Hello World'
是一个简单的字面量,所以最后的输出的是:
Hello World
Expression.getValue()
方法可以获取表达式“评估”(Evaluation)后的值,这个值的类型是Object
,需要进行转换。
如果评估失败,会抛出一个EvaluationException
异常:
public interface Expression {
@Nullable
Object getValue() throws EvaluationException;
// ...
}
在SpEL中,还可以调用方法:
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'.concat('!')");
String value = expression.getValue(String.class);
System.out.println(value);
这里调用了String.concat()
方法进行字符串连接,最后的输出是:
Hello World!
此外,示例中使用了一个泛型版本的getValue()
方法,通过传入一个目标类型的Class
对象,可以直接获取相应类型的结果,不用进行强制类型转换。
类似的,SpEL中也可以使用对象属性:
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) expression.getValue();
System.out.println(Arrays.toString(bytes));
输出结果:
[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]
对属性也可以用.
进行级联操作:
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'.bytes.length");
Integer value = (Integer) expression.getValue();
System.out.println(value);
输出结果:
11
在SpEL中,可以使用new
关键字使用构造器:
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("new String('Hello World').toUpperCase()");
String value = (String) expression.getValue();
System.out.println(value);
输出结果:
HELLO WORLD
SpEL更常见的用法是用一个SpEL表达式对指定对象进行评估,用于评估的对象被称作根对象(root object)。
下面看一个这样的示例,假设有这样的数据结构:
@Data
@AllArgsConstructor
private static class Person {
private String name;
private Integer age;
private Address address;
@Override
public String toString() {
return "%s, %d years old, from %s.".formatted(name, age, address.getCountry());
}
}
@Data
@AllArgsConstructor
private static class Address {
private String country;
private String city;
}
创建一个Person
对象,并用SpEL表达式进行评估:
Person person = new Person("icexmoon", 20, new Address("China", "NanJin"));
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("name");
String name = (String) expression.getValue(person);
System.out.println(name);
输出:
icexmoon
注意,这里的name
没有被单引号包裹,所以并不是一个字面量。所以这个表达式意味着在评估的时候会获取根对象的name
属性,相当于person.name
。
这里同样可以进行级联调用或者调用方法:
expression = expressionParser.parseExpression("address.country");
String country = (String) expression.getValue(person);
System.out.println(country);
expression = expressionParser.parseExpression("toString()");
String personText = (String) expression.getValue(person);
System.out.println(personText);
输出:
China
icexmoon, 20 years old, from China.
可以利用布尔表达式来评估根对象:
Person person = new Person("icexmoon", 20, new Address("China", "NanJin"));
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("age == 20");
Boolean result = expression.getValue(person, Boolean.class);
System.out.println(result);
输出:
true
EvaluationContext
EvaluationContext
接口在表达式解析属性或方法时使用,并帮助进行类型转换。Spring提供了两种实现:
SimpleEvaluationContext
: 暴露了SpEL语言的基本特征和配置选项的一个子集,适用于不需要SpEL语言语法的全部范围,并且应该被有意义地限制的表达类别。例如,包括但不限于数据绑定表达式和基于属性的过滤器。StandardEvaluationContext
: 暴露了全套的SpEL语言功能和配置选项。你可以用它来指定一个默认的根对象,并配置每个可用的评估相关策略。
SimpleEvaluationContext
被设计为只支持SpEL语言语法的一个子集。它排除了Java类型引用、构造函数和Bean引用。它还要求你明确选择对表达式中的属性和方法的支持程度。默认情况下, create()
静态工厂方法只允许对属性进行读取访问。你也可以获得一个 builder 来配置所需的确切支持级别,目标是以下的一个或一些组合。
- 仅限自定义
PropertyAccessor
(无反射)。 - 用于只读访问的数据绑定属性
- 读和写的数据绑定属性
这里关于
EvaluationContext
的说明摘抄自官方文档核心技术 (springdoc.cn)。
看下面的示例:
@Setter
@Getter
private static class MyList {
private List<Boolean> list = new ArrayList<>();
}
private static void spelTest8() {
EvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
MyList myList = new MyList();
myList.list.add(0, false);
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("list[0]");
expression.setValue(evaluationContext, myList, "true");
System.out.println(myList.list.get(0));
}
示例中创建了一个包含只读的DataBinding
的SimpleEvaluationContext
,然后利用这个SimpleEvaluationContext
设置了根对象myList
的属性。
在设置属性的时候存在类型转换,这里提供的是字符串类型的"true"
,实际类型则是Boolean
,依然可以正常转换,因为SpEL默认使用Spring中的ConversionService
进行类型转换。
关于
ConversionService
可以阅读从零开始 Spring Boot 29:类型转换 - 红茶的个人站点 (icexmoon.cn)。
实际上这里setValue
方法没有使用EvaluationContext
作为参数,依然可以正常转换类型,因为SpelExpression
缺省EvaluationContext
的时候,会默认使用StandardEvaluationContext
:
public class SpelExpression implements Expression {
public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException {
this.ast.setValue(new ExpressionState(this.getEvaluationContext(), this.toTypedValue(rootObject), this.configuration), value);
}
public EvaluationContext getEvaluationContext() {
if (this.evaluationContext == null) {
this.evaluationContext = new StandardEvaluationContext();
}
return this.evaluationContext;
}
// ...
}
解析器配置
我们可以设置通过设置解析器配置来变更某些行为,比如下面这个示例:
MyList myList = new MyList();
SpelExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("list[2]");
expression.setValue(myList, true);
System.out.println(myList.list);
运行时候会报错:
spel.SpelEvaluationException: EL1025E: The collection has '0' elements, index '2' is invalid
错误信息很明确,myList.list
是一个空列表,所以索引2
是非法的。
可以通过SpelParserConfiguration
配置SpEL解析器,让访问非法索引时自动填充空对象:
SpelParserConfiguration spelParserConfiguration = new SpelParserConfiguration(true, true);
MyList myList = new MyList();
SpelExpressionParser expressionParser = new SpelExpressionParser(spelParserConfiguration);
Expression expression = expressionParser.parseExpression("list[2]");
expression.setValue(myList, true);
System.out.println(myList.list);
输出:
[null, null, true]
构造器SpelParserConfiguration(true, true)
的意思是,自动生成空对象,自动填充容器:
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) {
// ...
}
编译器
一般来说,不用担心SpEL的性能问题,但如果在程序中大量频繁地使用编译器,就可能导致性能问题。这时候可以使用SpEL编译器进行性能优化。
SpEL编译器可以在SpEL表达式运行时,将其编译成Class文件,这样就避免了同一个表达式重复被动态解析,从而实现了性能优化。
编译器有三种运行模式:
OFF
(默认):编译器被关闭。IMMEDIATE
: 在即时模式下,表达式被尽快编译。这通常是在第一次解释的评估之后。如果编译的表达式失败(通常是由于类型改变,如前所述),表达式评估的调用者会收到一个异常。MIXED
: 在混合模式下,表达式随着时间的推移在解释模式和编译模式之间默默地切换。在经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(比如类型改变,如前所述),表达式会自动再次切换回解释形式。稍后的某个时候,它可能会生成另一个编译形式并切换到它。基本上,用户在IMMEDIATE
模式下得到的异常反而被内部处理。
修改编译器模式有两种方式:代码方式和修改配置文件。
以代码方式修改编译器模式:
SpelParserConfiguration spelParserConfiguration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
SpelExpressionParser spelExpressionParser = new SpelExpressionParser(spelParserConfiguration);
Expression expression = spelExpressionParser.parseExpression("'hello'");
expression.getValue(String.class);
以配置文件方式,修改application.properties
:
spring.expression.compiler.mode=off
Spring的SpEL编译器存在一些缺陷,某些情况下不能被正常编译,具体可以参考核心技术 (springdoc.cn)。
bean 定义中的表达式
可以通过SpEL表达式来构建bean定义,比如:
@RestController
@RequestMapping("/hello")
public class HelloController {
@Value("#{ environment.getProperty('user.region') }")
private String region;
@Autowired
private Environment environment;
@Value("${user.region}")
private String region2;
@GetMapping("")
public String hello() {
System.out.println(this.region);
String region = environment.getProperty("user.region");
System.out.println(region);
System.out.println(region2);
return Result.success().toString();
}
}
这里通过三种方式从application.properties
配置文件获取配置项user.region
:其中@Value("${...}")
是很常见的直接获取配置项,environment.getProperty(...)
则是通过注入的Environment
对象来获取配置项,而@Value("#{...}")
则是通过SpEL表达式获取environment
这个bean,然后调用对应的getProperty
方法获取。
- SpEL表达式可以直接获取Spring中预设的bean,比如
enviroment
。- 类似的,用XML方式定义bean的时候,同样可以使用SpEL。
除了可以利用SpEL定义bean的属性之外,还可以在用于创建bean的构造器(默认构造器或者@Autowired
标记的构造器)的参数上使用:
@RestController
@RequestMapping("/hello")
public class HelloController {
// ...
private int randomNum;
public HelloController(@Value("#{ T(java.lang.Math).random()*10+1 }") int randomNum) {
this.randomNum = randomNum;
}
// ...
}
这里通过@Value
注解,用一个SpEL表达式#{ T(java.lang.Math).random()*10+1 }
为参数randomNumn
指定了一个1~10
之间的整形值。
这样就规避了因为Java语法上不支持参数默认值导致的无法为bean的构造器提供常规类型参数的问题。
除了构造器以外,其他用于注入的方法同样可以用SpEL表达式:
@RequestMapping("/hello")
public class HelloController {
// ...
private String author;
public HelloController(@Value("#{ T(java.lang.Math).random()*10+1 }") int randomNum) {
this.randomNum = randomNum;
}
@Autowired
public void configure(HelloService helloService, @Value("#{ 'icexmoon' }") String author){
this.helloService = helloService;
this.author = author;
}
// ...
}
当然,这里实际上并不需要为通过使用SpEL表达式的方式初始化author
属性,只是为了演示这种用法。
语言参考
作为一门完备的语言,SpEL表达式本身就很复杂,但通常使用并不需要熟悉所有的语法,如果有遇到没有掌握的语法,可以查阅以下的官方文档:
- SpEL-语言参考。
The End,谢谢阅读。
本文的所有示例可以从ch31/spel · 魔芋红茶/learn_spring_boot - 码云 - 开源中国 (gitee.com)获取。
参考资料
- 核心技术 (springdoc.cn)