Spring 核心特性之表达式(SpEL)
Spring Framework 主要有 9 个核心特性,包括 IoC 容器、事件、资源、国际化、校验、数据绑定、类型转换、表达式以及 AOP。可以说,表达式是最没有存在感的核心特性了,用户直接使用的场景实在太少,这也是我一直没有提及它的原因。不过项目中确实有使用到它的地方,恰好最近整理 Spring 核心特性,为了知识结构完整性姑且总结一篇。
前言
Spring Framework 主要有 9 个核心特性,包括 IoC 容器、事件、资源、国际化、校验、数据绑定、类型转换、表达式以及 AOP。可以说,表达式是最没有存在感的核心特性了,用户直接使用的场景实在太少,这也是我一直没有提及它的原因。不过项目中确实有使用到它的地方,恰好最近整理 Spring 核心特性,为了知识结构完整性姑且总结一篇。
认识 SpEL
Spring 表达式即 Spring Expression Language,简称 SpEL,用于在运行时获取或设置表达式的值。
在 Java 中还有一些其他的表达式语言,如 OGNL、MVEL、JBoss EL 等,Spring 表达式与这些表达式语言在功能上基本类似,它的存在主要为了与 Spring 生态整合。
不过由于 Spring 表达式的 API 设计是中立的,不直接与 Spring 绑定,因此需要的话也可以集成其他的表达式语言实现。
使用 SpEL
SpEL 中有一些概念,需要在使用前理解。
1. 表达式字符串
表达式字符串是字符串形式的表达式,具有特定的语法,是用户直接接触最多的部分,如使用 'Hello,SpEL'
表示字符串 Hello,SpEL
。
2. 表达式解析器
表达式解析器用于将字符串形式的表达式解析为用 Expression
对象表示的表达式。使用接口 ExpressionParser
表示,常用的实现为 SpelExpressionParser
。
3. 解析上下文
解析上下文用于解析表达式时提供附加的信息,如表达式中是否存在模板。使用接口 ParserContext
表示,常用实现为 TemplateParserContext
。
4. 表达式 Expression
表达式 Expression
是表达式解析器 ExpressionParser
解析表达式字符串的结果,用于获取或设置表达式的值。常用实现为 SpelExpression
。
5. 评估上下文
评估上下文目的是在 Expression
获取表达式值时提供一些附加信息,例如表达式表示对象的属性时,评估表达式可设置属性所属对象。在 Spring 中使用接口 EvaluationContext
表示,常用实现为 StandardEvaluationContext
。
假定我们有对象如下。
@Data
public class User {
private String name;
private String age;
}
我们通过表达式获取 name
属性值的方式如下。
public class Application {
public static void main(String[] args) {
User user = new User();
user.setName("hkp");
String expressionString = "#{name}";
ParserContext parserContext = new TemplateParserContext("#{", "}");
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expressionString, parserContext);
EvaluationContext evaluationContext = new StandardEvaluationContext(user);
String name = expression.getValue(evaluationContext, String.class);
System.out.println(name);
}
}
SpEL 实现
SpEL 在 Spring 内部的实现可以简单理解如下:
- 首先用户调用
ExpressionParser#parseExpression
方法触发表达式解析。 - 表达式解析器在内部先进行词法解析,将字符串形式的表达式拆分成不同的
Token
,如1 + 2
表达式会被拆分成1
、+
、2
三部分。解析时同时会参考上下文ParserContext
,如上述示例中的#{name}
表达式,解析器会先去掉前后缀#{}
,然后再进行解析。 - 随后
Token
将被转换为抽象语法树,在内部使用SpelNode
表示,为了简化用户操作语法树被包装到Expression
。 - 用户使用
Expression#getValue
方法获取表达式的值,在内部也会参考评估上下文EvaluationContext
进行解析,例如上述示例中设置的根对象user
。
SpEL 语法
SpEL 语法支持的功能丰富多彩,常见语法如下。
1. 字面量表达式
字面量表达式支持字符串、数值(整型、浮点数、16进制)、boolean 和 null,其中字符串使用单引号'
表示,如果字符串内包含单引号'
,可以使用两个单引号。
示例如下:
- 字符串:
Hello World!
- 浮点数:
6.0221415E+23
- 16 进制数:
0x7FFFFFFF
- boolean 类型:
true
- null 值:
null
2. 属性引用
属性引用允许访问普通对象、数组、集合、Map 包含的属性值。
- 普通对象:普通对象的属性引用直接使用属性名即可,如果遇到嵌套属性,可以使用
.
表示,并且属性名称的抵押给字母不区分大小写,如Birthdate.Year + 1900
。 - 数组、集合:使用中括号
[index]
的形式引用,如Members[0].Inventions[6]
。 - Map:使用中括号
['key']
的形式引用,如Officers['advisors'][0].PlaceOfBirth.Country
。
3. 方法调用
使用 Java 语法即可进行方法调用,如 'abc'.substring(1, 3)
。
4. 运算符
SpEL 支持关系运算符、逻辑运算符、数据运算符、赋值运算符。可以使用常见字符表示,也可以使用对应的文本表示,如 eq
等同于 ==
,如果使用文本表示则不区分大小写。
4.1 关系运算符
包括常见的等于:==(eq)
、不等于:!=(ne)
、小于:<(gt)
、小于等于:<=(le)
、大于:>(gt)
、大于等于>=(ge)
,此外还支持判断类型的 instanceOf
及正则判断的 matches
运算符。示例如下:
2 = 2
'xyz' instanceof T(Integer)
'5.00' matches '^-?\\d+(\\.\\d{2})?$'
4.2 逻辑运算符
包括与:&&(and)
、或:||(or)
、非:!(not)
,如 true and false
。
4.3 数学运算符
数学运算符可用于数字和字符串,对于数字可使用加:+
、减:-
、乘:*
、除:/
、取模:%
、指数幂:^
,多个数学运算符按照标准的运算符优先级。示例如下:
1 + 1
'test' + ' ' + 'string'
4.4 赋值运算符
赋值运算符为 =
,用于 Expression#setValue
或 Expression#getValue
方法调用设置表达式的值。示例如下。
public class Application {
public static void main(String[] args) {
User user = new User();
user.setName("hkp");
String expressionString = "age = 18";
Expression expression = new SpelExpressionParser().parseExpression(expressionString);
EvaluationContext evaluationContext = new StandardEvaluationContext(user);
expression.getValue(evaluationContext, Integer.class);
System.out.println(user.getAge());
}
}
5. 类型
- 可以使用
T(classname)
的形式表示java.lang.Class
的实例,如果包名为java.lang
可以忽略包名,如T(java.util.Date)
、T(String)
。 - 也可以使用
T(classname)
调用静态方法,如T(String).valueOf(123)
。
6. 构造方法
可以通过使用 new
关键字调用构造方法,注意除了 String 应该使用完整限定名,如 new String('abc')
。
7. 变量
可以使用 #variableName
的形式引用变量,变量在 EvaluationContext#setVariable
上进行设置,变量名只能包含字母 A
到Z
,a
到z
、数字 0
到 9
、下划线 _
以及美元符号 $
。变量使用示例如下。
public class Application {
public static void main(String[] args) {
User user = new User();
user.setName("hkp");
String expressionString = "age = #age";
Expression expression = new SpelExpressionParser().parseExpression(expressionString);
EvaluationContext evaluationContext = new StandardEvaluationContext(user);
evaluationContext.setVariable("age", 18);
expression.getValue(evaluationContext, Integer.class);
System.out.println(user.getAge());
}
}
8. 三元运算符
SpEL 同样支持三元运算符 ?:
,如 false ? 'trueExp' : 'falseExp'
。
此外还支持使用 ?:
语法对三元运算符进行简化,如 name != null ? name : 'Unknown'
可以简化为 name?:'Unknown'
。
9. 安全导航运算符
安全导航运算符即 ?.
,用于避免在访问对象的属性或方法前判断对象不为空,以免抛出 NullPointerException
异常,如 PlaceOfBirth?.City
。
10. 表达式模板
表达式模板用于将文本和其他要评估的表达式混合到一起,评估表达式需要包含在前缀和后缀中,前后缀通常为 #{
和 }
,示例如下。
public class Application {
public static void main(String[] args) {
String expressionString = "random number is #{T(java.lang.Math).random()}";
ParserContext parserContext = new TemplateParserContext("#{", "}");
Expression expression = new SpelExpressionParser().parseExpression(expressionString, parserContext);
String value = expression.getValue(String.class);
System.out.println(value);
}
}
更多语法内容,可参考 Spring 官网 《4. Spring Expression Language(SpEL)》。
SpEL 使用场景
我总结了 SpELl 日常在项目中的两个应用场景。
依赖注入
可以使用 @Value
注入表达式的值,示例如下。
public class PropertyValueTestBean {
private String defaultLocale;
@Value("#{ systemProperties['user.region'] }")
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
其中 systemProperties
是内置的表示系统属性的变量。
缓存 key 动态获取
另一种应用场景是可以使用 AOP 拦截方法的执行,使用方法参数作为缓存或分布式锁的 key,代码如下。
@Aspect
@Component
public class LockAspect {
@Around("@annotation(concurLock)")
public Object around(ProceedingJoinPoint joinPoint, ConcurLock concurLock) throws Throwable {
// 获取方法参数名并设置到 EvaluationContext 变量中
EvaluationContext context = new StandardEvaluationContext();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
String[] parametersNames = new DefaultParameterNameDiscoverer().getParameterNames(signature.getMethod());
for (int i = 0; i < args.length; i++) {
context.setVariable(parametersNames[i], args[i]);
}
// 解析表达式作为缓存 key
String lockKey = new SpelExpressionParser().parseExpression(concurLock.key()).getValue(context, String.class);
...省略缓存相关代码
return joinPoint.proceed();
}
}
更多推荐
所有评论(0)