【基础系列】SpEL语法扫盲与查询手册

文章目录
  1. I. 语法百科
    1. 1. 字面表达式
    2. 2. Inline List
    3. 3. Inline map
    4. 4. 数组
    5. 5. 表达式
    6. 6. Type与静态类
    7. 7. 构造方法
    8. 8. 变量引用
    9. 9. 函数
    10. 10. bean访问
    11. 11. ifElse
    12. 12. elvis
    13. 13. 安全表达式
    14. 14. 容器截取
    15. 15. 容器映射
    16. 16. 表达式模板
    17. 17. 小结
  2. II. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog

Spring表达式语言简称为SpEL,一种类似Ognl的对象图导航语言(对于ognl不熟悉的同学可以参考一下: Ognl系列博文

SeEL为Spring提供了丰富的想象空间,除了一些基本的表达式操作之外,还支持

  • 访问bean对象
  • 调用方法,访问(修改)类(对象)属性
  • 计算表达式
  • 正则匹配

I. 语法百科

以下内容均来自官方文档: https://docs.spring.io/spring-framework/docs/5.2.1.RELEASE/spring-framework-reference/core.html#expressions

1. 字面表达式

Spel支持strings, numeric values (int, real, hex), boolean, and null等基本类型,实例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

// double 类型
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

请注意,字符串需要用单引号包括,浮点数默认为double类型,用null表示null object

输出结果

1
2
3
4
5
str: Hello World
double: 6.0221415E23
int: 2147483647
bool: true
null: null

2. Inline List

通过{}来表明List表达式,一个空的列表直接用{}表示

1
2
3
4
5
6
7
8
ExpressionParser parser = new SpelExpressionParser();
// Integer列表
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue();
System.out.println("list: " + numbers);

// List的元素为List
List<List> listlOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue();
System.out.println("List<List> : " + listlOfLists);

输出结果

1
2
list: [1, 2, 3, 4]
List<List> : [[a, b], [x, y]]

3. Inline map

{key:value}来表示map表达式,空Map直接用{:}表示

1
2
3
4
5
6
7
8
9
private void map() {
ExpressionParser parser = new SpelExpressionParser();
Map map = (Map) parser.parseExpression("{txt:'Nikola',dob:'10-July-1856'}").getValue();
System.out.println("map: " + map);
Map mapOfMaps =
(Map) parser.parseExpression("{txt:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}")
.getValue();
System.out.println("Map<Map>: " + mapOfMaps);
}

输出结果

1
2
map: {txt=Nikola, dob=10-July-1856}
Map<Map>: {txt={first=Nikola, last=Tesla}, dob={day=10, month=July, year=1856}}

4. 数组

数组可以借助new构造方法来实现,通过下标ary[index]的方式访问数组中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void array() {
ExpressionParser parser = new SpelExpressionParser();
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue();
System.out.println("array: " + JSON.toJSONString(numbers1));

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue();
System.out.println("array: " + JSON.toJSONString(numbers2));

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue();
System.out.println("array: " + JSON.toJSONString(numbers3));


int[] nums = new int[]{1, 3, 5};
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("num", nums);

// 通过下标访问数组中的元素
Integer numVal = parser.parseExpression("#num[1]").getValue(context, Integer.class);
System.out.println("numVal in array: " + numVal);
}

输出如下

1
2
3
4
array: [0,0,0,0]
array: [1,2,3]
array: [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]
numVal in array: 3

5. 表达式

Spel支持一些Java语法中常规的比较判断,算数运算,三元表达式,类型判断,matches正则匹配等基表表达式

下面给出一些简单的实例

1
2
3
4
5
6
7
8
9
10
11
12
public void expression() {
ExpressionParser parser = new SpelExpressionParser();
// 运算
System.out.println("1+2= " + parser.parseExpression("1+2").getValue());
// 比较
System.out.println("1<2= " + parser.parseExpression("1<2").getValue());
System.out.println("true ? hello : false > " + parser.parseExpression("3 > 2 ? 'hello': 'false' ").getValue());
// instanceof 判断,请注意静态类,用T进行包装
System.out.println("instance : " + parser.parseExpression("'a' instanceof T(String)").getValue());
//正则表达式
System.out.println("22 是否为两位数字 :" + parser.parseExpression("22 matches '\\d{2}'").getValue());
}

输出结果

1
2
3
4
5
1+2= 3
1<2= true
true ? hello : false > hello
instance : true
22 是否为两位数字 :true

6. Type与静态类

如果想获取Class对象,或者访问静态成员/方法,可以借助T()语法来实现

比如我们有一个静态类

1
2
3
4
5
6
7
public static class StaClz {
public static String txt = "静态属性";

public static String hello(String tag) {
return txt + " : " + tag;
}
}

如果希望访问静态属性txt, 表达式可以写成T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt,请注意圆括号中的是完整签名;访问静态方法方式类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void type() {
// class,静态类
ExpressionParser parser = new SpelExpressionParser();
String name =
parser.parseExpression("T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt").getValue(String.class);
System.out.println("txt: " + name);

String methodReturn =
parser.parseExpression("T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).hello" + "('一灰灰blog')")
.getValue(String.class);
System.out.println("static method return: " + methodReturn);

// class类获取
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println("class: " + stringClass.getName());
}

输出结果如下

1
2
3
txt: 静态属性
static method return: 静态属性 : 一灰灰blog
class: java.lang.String

上面的写法,请重点看一下T(String),这里的String没有用完整的包路径,即直接位于java.lang包下的类,是可以省略掉完整包名的,就像我们平时写代码时,也不需要显示的加一个import java.lang.*

7. 构造方法

上面介绍array的时候,就介绍了使用new来创建数组对象,当然也可以直接构造其他的普通对象, 如我们新建一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class Person {
private String name;

private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "Person{" + "txt='" + name + '\'' + ", age=" + age + '}';
}
}

通过SpEl创建一个对象的实例

1
2
3
4
5
6
public void construct() {
ExpressionParser parser = new SpelExpressionParser();
Person person = parser.parseExpression("new com.git.hui.boot.spel.demo.BasicSpelDemo.Person('一灰灰', 20)")
.getValue(Person.class);
System.out.println("person: " + person);
}

输出结果如下:

1
person: Person{txt='一灰灰', age=20}

请注意,构造方法中类的完整签名

8. 变量引用

细心的小伙伴,在上面介绍数组的成员演示的实例中,写法如"#num[1]",这个num前面有一个#,这是一个语法定义,有#修饰的表示变量访问

要理解这一小节,首先得理解EvaluationContext, 在我们的SpEL表达式的解析中,getValue有一个参数就是这个Context,你可以将他简单理解为包含一些对象的上下文,我们可以通过SpEL的语法,来访问操作Context中的某些成员、成员方法属性等

一般的操作过程如下:

  • context.setVariable("person", person);EvaluationContext中塞入成员变量
  • parser.parseExpression(xxx).getValue(context) 解析SpEL表达式,context必须作为传参丢进去哦

一个简单的实例

1
2
3
4
5
6
7
8
9
10
11
12
public void variable() {
ExpressionParser parser = new SpelExpressionParser();
Person person = new Person("一灰灰blog", 18);
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("person", person);

String name = parser.parseExpression("#person.getName()").getValue(context, String.class);
System.out.println("variable name: " + name);

Integer age = parser.parseExpression("#person.age").getValue(context, Integer.class);
System.out.println("variable age: " + age);
}

输出结果如下

1
2
variable name: 一灰灰blog
variable age: 18

友情提示,如果访问对象的私有Field/method,会抛异常

9. 函数

Context中的变量,除了是我们常见的基本类型,普通的对象之外,还可以是方法,在setVariable时,设置的成员类型为method即可

1
2
3
4
5
6
7
8
9
10
11
12
13
public void function() {
try {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// 注册一个方法变量,参数为method类型
context.setVariable("hello", StaClz.class.getDeclaredMethod("hello", String.class));

String ans = parser.parseExpression("#hello('一灰灰')").getValue(context, String.class);
System.out.println("function call: " + ans);
} catch (Exception e) {
e.printStackTrace();
}
}

输出结果如下

1
function call: 静态属性 : 一灰灰

10. bean访问

在Spring中,什么对象最常见?当然是bean, 那么我们可以直接通过SpEL访问bean的属性、调用方法么?

要访问bean对象,所以我们的EvaluationContext中需要包含bean对象才行

  • 借助BeanResolver来实现,如context.setBeanResolver(new BeanFactoryResolver(applicationContext));
  • 其次访问bean的前缀修饰为@符号

为了演示这种场景,首先创建一个普通的Bean对象

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Component
public class BeanDemo {

private String blog = "https://spring.hhui.top";

private Integer num = 8;

public String hello(String name) {
return "hello " + name + ", welcome to my blog " + blog + ", now person: " + num;
}
}

接着我们需要获取ApplicationContext,所以可以稍微改一下我们的测试类,让它继承自ApplicationContextAware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}


public void bean() {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(applicationContext));

// 获取bean对象
BeanDemo beanDemo = parser.parseExpression("@beanDemo").getValue(context, BeanDemo.class);
System.out.println("bean: " + beanDemo);

// 访问bean方法
String ans = parser.parseExpression("@beanDemo.hello('一灰灰blog')").getValue(context, String.class);
System.out.println("bean method return: " + ans);
}

上面的写法和之前的并没有太大的区别,实际输出结果如下

1
2
bean: BeanDemo(blog=https://spring.hhui.top, num=8)
bean method return: hello 一灰灰blog, welcome to my blog https://spring.hhui.top, now person: 8

11. ifElse

SpEL支持三元表达式,在上述的表达式中也给出了实例

1
2
3
4
5
6
public void ifThenElse() {
// 三元表达式,? :
ExpressionParser parser = new SpelExpressionParser();
String ans = parser.parseExpression("true ? '正确': '错误'").getValue(String.class);
System.out.println("ifTheElse: " + ans);
}

输出结果如下

1
ifTheElse: 正确

12. elvis

xx != null ? xx : yy => xx?:yy

这个也属于我们经常遇到的一种场景,如果xx为null,则返回yy;否则直接返回xx;简化写法为elvis写法: xx?:yy

1
2
3
4
5
6
7
8
9
10
11
12
public void elvis() {
// xx != null ? xx : yy => xx?:yy
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", null);
String name = parser.parseExpression("#name?:'Unknown'").getValue(context, String.class);
System.out.println("elvis-before " + name);

context.setVariable("name", "Exists!");
name = parser.parseExpression("#name?:'Unknown'").getValue(context, String.class);
System.out.println("elvis-after " + name);
}

输出结果如下

1
2
elvis-before Unknown
elvis-after Exists!

13. 安全表达式

在java中,最常见最讨厌的是一个就是NPE的问题,SpEL中当然也可能出现这种情况,但是若在SpEL中进行非空判断,那就很不优雅了,SpEL提供了xx?.yy的写法来避免npe,即

xx == null ? null : xx.yy => xx?.yy

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
public void safeOperate() {
// 防npe写法, xx == null ? null : xx.yy => xx?.yy
ExpressionParser parser = new SpelExpressionParser();
Person person = new Person(null, 18);

String name = parser.parseExpression("name?.length()").getValue(person, String.class);
System.out.println("safeOperate-before: " + name);

person.name = "一灰灰blog";
name = parser.parseExpression("name?.length()").getValue(person, String.class);
System.out.println("safeOperate-after: " + name);
}

输出结果如下

1
2
safeOperate-before: null
safeOperate-after: 7

14. 容器截取

遍历容器,获取子集,相当于jdk8 Stream中filter用法,语法格式如下

xx.?[expression], 请注意中括弧中的表达式必须返回boolean

举例说明

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
public void collectionSelection() {
// 容器截取,返回满足条件的子集
// xx.?[expression] , 将满足expression的子元素保留,返回一个新的集合,类似容器的 filter
List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 4, 6, 7, 8, 9));
ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("list", list);
// 用 #this 来指代列表中的迭代元素
List<Integer> subList = (List<Integer>) parser.parseExpression("#list.?[#this>5]").getValue(context);
System.out.println("subList: " + subList);


Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 10);
map.put("c", 4);
map.put("d", 7);
context.setVariable("map", map);
// 表达式内部用key, value 来指代map的k,v
Map subMap = parser.parseExpression("#map.?[value < 5]").getValue(context, Map.class);
System.out.println("subMap: " + subMap);

subMap = parser.parseExpression("#map.?[key == 'a']").getValue(context, Map.class);
System.out.println("subMap: " + subMap);
}

输出结果如下

1
2
3
subList: [6, 7, 8, 9]
subMap: {a=1, c=4}
subMap: {a=1}

注意

  • 在列表表达式中,可以通过#this来指代列表中的每一个元素
  • 在map表达式中,通过key, value来分别指代map中的k,v

15. 容器映射

将一个集合通过某种规则,映射为另一种集合,相当于jdk8 Stream中的map用法,语法如下

xx.![expression], 将表达式计算的结果作为输出容器中的成员

举例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void collectionProjection() {
// 容器操作之后,生成另一个容器, 类似lambda中的map方法
// xx.![expression]

List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 4, 6, 7, 8, 9));
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("list", list);

// 用 #this 来指代列表中的迭代元素
List newList = parser.parseExpression("#list.![#this * 2]").getValue(context, List.class);
System.out.println("newList: " + newList);


Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 10);
map.put("c", 4);
map.put("d", 7);
context.setVariable("map", map);
List newListByMap = parser.parseExpression("#map.![value * 2]").getValue(context, List.class);
System.out.println("newListByMap: " + newListByMap);
}

输出结果如下:

1
2
newList: [2, 6, 8, 12, 14, 16, 18]
newListByMap: [2, 20, 8, 14]

16. 表达式模板

SpEL还提供了一种自定义表达式模板的方式,将字面量和表达式放在一起使用,比如下面这一条语句

1
"random number is #{T(java.lang.Math).random()}"

其中#{T(java.lang.Math).random()}是一个SpEL表达式,左边的是普通字符串,这种写法也常见于@Value注解中的属性写法,当然直接通过上面的写法执行这个语句会报错,这个时候需要指定ParserContext

举例说明

1
2
3
4
5
6
7
public void template() {
// 模板,混合字面文本与表达式,使用 #{} 将表达式包裹起来
ExpressionParser parser = new SpelExpressionParser();
String randomPhrase = parser.parseExpression("random number is #{T(java.lang.Math).random()}",
ParserContext.TEMPLATE_EXPRESSION).getValue(String.class);
System.out.println("template: " + randomPhrase);
}

输出结果如下

1
template: random number is 0.10438946298113871

17. 小结

SpEL属于非常强大的表达式语言了,就我个人的感觉而言,它和OGNL有些像,当它们的上下文中包含了Spring的上下文时,可以访问任何的bean,而你可以借助它们的语法规范,做各种事情

推荐我之前的一个项目,https://github.com/liuyueyi/quick-fix,利用ognl结合ApplicationContext,可以随心所欲的访问控制应用中的任何bean对象

II. 其他

0. 项目

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到