前言
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 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]);
}
String lockKey = new SpelExpressionParser().parseExpression(concurLock.key()).getValue(context, String.class);
...省略缓存相关代码
return joinPoint.proceed();
}
}
|