spring expression language

spring expression language

Spel概述

SpEL是Spring内置的表达式语言,它是一种能够支持运行时查询和操作对象图的强大的表达式,语法与OGNL等其他表达式语言十分类似。SpEL设计之初就是朝着做一个表达式语言的通用框架,可以独立运行【Spring3.x引入的SpEL】。

Spel的主要相关类

SpEL对表达式语法解析过程进行了很高的抽象,抽象出解析器、表达式、解析上下文、估值(Evaluate)上下文等对象,非常优雅的表达了解析逻辑。主要的对象如下:

类名 说明
ExpressionParser 表达式解析器接口,包含了(Expression) parseExpression(String), (Expression) parseExpression(String, ParserContext)两个接口方法
ParserContext 解析器上下文接口,主要是对解析器Token的抽象类,包含3个方法:getExpressionPrefix,getExpressionSuffixisTemplate,就是表示表达式从什么符号开始什么符号结束,是否是作为模板(包含字面量和表达式)解析。一般保持默认。
Expression 表达式的抽象,是经过解析后的字符串表达式的形式表示。通过expressionInstance.getValue方法,可以获取表示式的值。也可以通过调用getValue(EvaluationContext),从评估(evaluation)上下文中获取表达式对于当前上下文的值
EvaluationContext 估值上下文接口,只有一个setter方法:setVariable(String, Object),通过调用该方法,可以为evaluation提供上下文变量

The EvaluationContext interface is used when evaluating an expression to resolve properties, methods, or fields and to help perform type conversion. Spring provides two implementations.

  • SimpleEvaluationContext: Exposes a subset of essential SpEL language features and configuration options, for categories of expressions that do not require the full extent of the SpEL language syntax and should be meaningfully restricted. Examples include but are not limited to data binding expressions and property-based filters.
  • StandardEvaluationContext: Exposes the full set of SpEL language features and configuration options. You can use it to specify a default root object and to configure every available evaluation-related strategy.

image-20231117180719612

完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class _01_Spel1 {
/**
* 代码解释:
* (1)创建一个模板表达式,所谓模板就是带字面量和表达式的字符串。其中#{}表示表达式的起止,#user是表达式字符串,表示引用一个变量。
* (2)创建表达式解析器,SpEL框架创建了一个语言无关的处理框架,在这里我们学习的是SpEL所以使用SpelExpressionParser()
* (3)解析表达式,如果表达式是一个模板表达式,需要为解析传入模板解析器上下文。如果不传入模板解析器上下文,指定表达式为模板,那么表达式字符串Hello, #{ #user },解析器会首先去尝试解析Hello。例子中的模板表达式,与'Hello, ' + #user是等价的。
* (4)通过evaluationContext.setVariable可以在上下文中设定变量。
* (5)使用Expression.getValue()获取表达式的值,这里传入了Evalution上下文,第二个参数是类型参数,表示返回值的类型
*/
public static void main(String[] args) {
// (1)
String greetingExp = "Hello, #{ #user }";
//(2)
ExpressionParser parser = new SpelExpressionParser();

// (3)
Expression expression = parser.parseExpression(greetingExp, new TemplateParserContext());
// Expression expression = parser.parseExpression(greetingExp, new TemplateParserContext());

// (4)
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", "Gangyou");
// (5)
final String value = expression.getValue(context, String.class);
System.out.println(value); //(5)
}
}

SpEL语法

本节介绍下SpEL中的各类语法,在语法中会发现很多熟悉的影子,比如Java,JavaScript, Groovy等等,SpEL支持如下表达式:

  • 基本表达式:字面量表达式、关系,逻辑与算数运算表达式、字符串连接及截取表达式、三目运算、正则表达式、括号优先级表达式;
  • 类相关表达式:类类型表达式、类实例化、instanceof表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean引用;
  • 集合相关表达式:内联List、内联数组、集合,字典访问、列表,字典,数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义;
  • 其他表达式:模板表达式。

。依次介绍SpEL中的

  • 字面量
  • 数组和Map字面量
  • 二元操作符和三元操作符
  • 来自Groovy的操作符
  • 数组列表和Map的访问
  • Java Bean属性访问
  • Java Bean方法调用
  • Java 类型访问
  • Java 实例访问

SpEL的使用基本总结如下:

  • SpEL 字面量:
    - 整数:#{8}
    - 小数:#{8.8}
    - 科学计数法:#{1e4}
    - String:可以使用单引号或者双引号作为字符串的定界符号。
    - Boolean:#{true}
  • SpEL引用bean , 属性和方法:
    - 引用其他对象:#{car}
    - 引用其他对象的属性:#{car.brand}
    - 调用其它方法 , 还可以链式操作:#{car.toString()}
    - 调用静态方法静态属性:#{T(java.lang.Math).PI}
  • SpEL支持的运算符号:
    - 算术运算符:+,-,*,/,%,^(加号还可以用作字符串连接)
    - 比较运算符:< , > , == , >= , <= , lt , gt , eg , le , ge
    - 逻辑运算符:and , or , not , |
    - if-else 运算符(类似三目运算符):?:(temary), ?:(Elvis)
    - 正则表达式:#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}’}

字面量

SpEL支持如下类型的字面量:

类型 说明 示例
数字 支持任何数字类型,包括了各种进制的数字,科学计数法等 6.0221415E+23,0xFFFFFFFF
布尔量 true, false
字符串 用单引号包围的字符串,单引号要用2个单引号转义 ‘Gangyou’’s, Blog’
日期 没写成功 //TODO
null 直接转成字符串null null

属性

SpEL对属性的访问遵循Java Bean语法,对于表达式中的属性都要提供相应的setter。

比如这样一个Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static class Person {    
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}

表达式firstName + ' ' + lastName就等价于person.getFirstName() + " " + person.getLastName()。如果属性本身是对象,还支持嵌套属性,如person.address.city,就等价于person.getAddress().getCity()

数组、列表和Map

数组和列表都可以用过数字下标进行访问,比如list[0]

Map的访问就类似JavaScript的访问方式,使用key访问。例如java代码map.put("name", "gangyou")其中的map对象,可以通过map['name']获取到字符串gangyou

内联的数组和Map

Spel允许在表达式内创建数组(列表)和Map,如{1,2,3,4},{'firstName': 'Gang', 'lastName': 'You'}

方法调用

SpEL使用java的相同语法进行方法调用,如'Hello.concat(‘, World!’)`,输出** Hello, World!**。

类型

SpEL中可以使用特定的Java类型,经常用来访问Java类型中的静态属性或静态方法,需要用T()操作符进行声明。括号中需要包含类名的全限定名,也就是包名加上类名。唯一例外的是,SpEL内置了java.lang包下的类声明,也就是说java.lang.String可以通过T(String)访问,而不需要使用全限定名。

两个例子: T(Math).random()T(java.lang.Math).random()

创建实例

使用new可以直接在SpEL中创建实例,需要创建实例的类要通过全限定名进行访问,如:new java.util.Date().now()

二元操作符

SpEL中的二元操作符同Java中的二元操作符,包括了数学运算符、位运算符、关系运算符等等。

三元操作符

SpEL的三元操作符主要是 ** if else then ** 操作符 condition ? true statement : false statement与Java中的一致。

另外一个就是借鉴自Groovy的 Elvis 操作符?:

举一个例子

1
2
String expressionStr1 = " name != null ? name : 'Default Value'";
String expressionStr2 = "name ?: 'Default Value'";

上面的两个表达式都是先判断 getName()的返回值是不是null,如果是就返回Default Value,通过下面的Elvis操作符可以让代码更加的清晰。

安全访问符

同样借鉴自Groovy,在SpEL中引入了安全访问符Safe Navigator Operator——.?,解决了很大问题。相信每个Javaer都遇到过NullPointException的运行时异常,通常是对象还未实例化或者找不到对象,却访问对象属性造成的。通过安全访问符就可以避免这样的问题。

1
String expStr = "thisMayBeNull.?property"

这句表达式在求值的时候,不会因为thisMayBeNull是Null值而抛出NullPointException,而是会简单的返回null。个人认为此处结合Elvis操作符,是一个很完善的处理方式。

集合选择

同样借鉴自Groovy,SpEL提供了集合选择语法,如下面的例子:

1
2
List<Inventor> list = (List<Inventor>) parser.parseExpression(
"Members.?[Nationality == 'Serbian']").getValue(societyContext);

如果Members List不是Null,则在列表中选择getNationality() == 'Serbian'的对象集合返回。这个很类似于Java 8中的stream and filter方式,只是更加的简洁。

[]中间可以利用任何的布尔表达式,创建筛选条件。例如age > 18name.startsWith('You')等等。

自定义函数

SpEL提供了Java的基础功能,也引入了3个借鉴自Groovy的特性语法提供更为简洁的表达能力。作为一款设计为框架的语言,更提供了自定义的扩展能力。

比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class StringUtils {

public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder();
for (int i = 0; i < input.length(); i++)
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}

public static void main(String[] args){
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

context.registerFunction("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class }));

String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);
}
}

通过使用StandardEvalutinContext.registerFunction可以注册自定义的函数,唯一的一点要求就是需要在表达式中通过#注册函数名的方式引用函数。

1
2
3
4
5
6
7
8
9
10
11
public class CustomSpelFunction2 {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext(CustomSpelFunction.class);
ExpressionParser parser = new SpelExpressionParser();
context.setVariable("str", "test");
Object value = parser.parseExpression("reverseString(#str)").getValue(context, String.class);
System.out.println("result = " + value);

}
}


参考链接:

Spring Expression Language(SpEL) 4 学习笔记 - 简书 (jianshu.com)

Spring Expression Language (SpEL) :: Spring Framework

Spring学习总结(四)——表达式语言 Spring Expression Language - 张果 - 博客园 (cnblogs.com)

【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)-CSDN博客

在spel遇到的问题。( Attempted to call method isEmpty(java.lang.String) on null context object) - 简书 (jianshu.com)