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

简介: Spring 表达式语言简称为 SpEL,一种类似 Ognl 的对象图导航语言(对于 ognl 不熟悉的同学可以参考一下: Ognl 系列博文)SeEL 为 Spring 提供了丰富的想象空间,除了一些基本的表达式操作之外,还支持

image.png


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


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

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


I. 语法百科



以下内容均来自官方文档: docs.spring.io/spring-fram…


1. 字面表达式


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


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


输出结果

str: Hello World
double: 6.0221415E23
int: 2147483647
bool: true
null: null
复制代码


2. Inline List


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


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);
复制代码


输出结果

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


3. Inline map


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

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);
}
复制代码


输出结果

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]的方式访问数组中的元素


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);
}
复制代码


输出如下

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正则匹配等基表表达式


下面给出一些简单的实例

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
1<2= true
true ? hello : false > hello
instance : true
22 是否为两位数字 :true
复制代码


6. Type 与静态类


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


比如我们有一个静态类

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,请注意圆括号中的是完整签名;访问静态方法方式类似


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());
}
复制代码


输出结果如下

txt: 静态属性
static method return: 静态属性 : 一灰灰blog
class: java.lang.String
复制代码


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


7. 构造方法


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


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 创建一个对象的实例

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);
}
复制代码


输出结果如下:

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 必须作为传参丢进去哦


一个简单的实例

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);
}
复制代码


输出结果如下

variable name: 一灰灰blog
variable age: 18
复制代码


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


9. 函数


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


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();
    }
}
复制代码


输出结果如下

function call: 静态属性 : 一灰灰
复制代码


10. bean 访问


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


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

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


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


@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


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);
}
复制代码


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

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 支持三元表达式,在上述的表达式中也给出了实例


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


输出结果如下


ifTheElse: 正确
复制代码


12. elvis


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

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


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);
}
复制代码


输出结果如下

elvis-before Unknown
elvis-after Exists!
复制代码


13. 安全表达式


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


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


举例说明

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);
}
复制代码


输出结果如下

safeOperate-before: null
safeOperate-after: 7
复制代码


14. 容器截取


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


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


举例说明

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);
}
复制代码


输出结果如下

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], 将表达式计算的结果作为输出容器中的成员


举例如下

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);
}
复制代码


输出结果如下:

newList: [2, 6, 8, 12, 14, 16, 18]
newListByMap: [2, 20, 8, 14]
复制代码


16. 表达式模板


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


"random number is #{T(java.lang.Math).random()}"
复制代码


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


举例说明

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);
}
复制代码


输出结果如下

template: random number is 0.10438946298113871
复制代码


17. 小结


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


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



相关文章
|
3天前
|
SQL 前端开发 关系型数据库
SpringBoot使用mysql查询昨天、今天、过去一周、过去半年、过去一年数据
SpringBoot使用mysql查询昨天、今天、过去一周、过去半年、过去一年数据
31 9
|
27天前
|
Java 测试技术 Spring
springboot学习三:Spring Boot 配置文件语法、静态工具类读取配置文件、静态工具类读取配置文件
这篇文章介绍了Spring Boot中配置文件的语法、如何读取配置文件以及如何通过静态工具类读取配置文件。
41 0
springboot学习三:Spring Boot 配置文件语法、静态工具类读取配置文件、静态工具类读取配置文件
|
1月前
|
自然语言处理 搜索推荐 Java
SpringBoot 搜索引擎 海量数据 Elasticsearch-7 es上手指南 毫秒级查询 包括 版本选型、操作内容、结果截图(一)
SpringBoot 搜索引擎 海量数据 Elasticsearch-7 es上手指南 毫秒级查询 包括 版本选型、操作内容、结果截图
47 0
|
1月前
|
存储 自然语言处理 搜索推荐
SpringBoot 搜索引擎 海量数据 Elasticsearch-7 es上手指南 毫秒级查询 包括 版本选型、操作内容、结果截图(二)
SpringBoot 搜索引擎 海量数据 Elasticsearch-7 es上手指南 毫秒级查询 包括 版本选型、操作内容、结果截图(二)
32 0
|
4月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的武汉市公交路线查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的武汉市公交路线查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
107 7
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的公共交通查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的公共交通查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
32 1
基于SpringBoot+Vue的公共交通查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
|
4月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的城市公交在线查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的城市公交在线查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
|
4月前
|
Java 关系型数据库 API
使用Spring Boot和PostgreSQL构建高级查询
使用Spring Boot和PostgreSQL构建高级查询
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的武汉市公交路线查询系统的详细设计和实现
基于SpringBoot+Vue+uniapp的武汉市公交路线查询系统的详细设计和实现
48 14
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的自驾游攻略查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的自驾游攻略查询系统的详细设计和实现(源码+lw+部署文档+讲解等)
51 3
下一篇
无影云桌面