前言
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); } }
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(); } }