Spring RE 06 SpringEL表达式

Spring RE 06 SpringEL表达式

IOC和AOP两大内容看完了, 东西是真不少, 后边还有一个重型的东西, 关系着Web应用的好坏, 就是数据库. 在IOC+AOP和Spring对数据库的支持中间, 插入一个SpringEL来休闲一下吧. SpringEL表达式 SpringEL核心接口 表达式语法 - 文本字符和基本类型 表达式语

IOC和AOP两大内容看完了, 东西是真不少, 后边还有一个重型的东西, 关系着Web应用的好坏, 就是数据库.

在IOC+AOP和Spring对数据库的支持中间, 插入一个SpringEL来休闲一下吧.

  1. SpringEL表达式
  2. SpringEL核心接口
  3. 表达式语法 - 文本字符和基本类型
  4. 表达式语法 - 取对象属性
  5. 表达式语法 - 取集合类型的内容
  6. 表达式语法 - 方法解析
  7. 表达式语法 - 操作符解析
  8. 表达式语法 - ?系操作符
  9. 表达式语法 - 赋值, 类型, 构造器, 变量
  10. 表达式语法 - 集合过滤和转换
  11. 实际的Spring EL 表达式使用

SpringEL表达式

学Spring之前, Java Web重新过了一遍, 很多东西了解的更深刻了. JSP里的EL表达式, 本质上说, 就是在JSP引擎解析JSP文件, 将JSP文件变成一个Servlet的时候, EL表达式会由特定的解析引擎去解析, 像极了各种特殊的标签.

所以可以知道, 不管是EL还是SpringEL, 背后都是对应的类来操作, 解析表达式, 然后输出结果. 既然是EL, 就要注意, 其背后的本质, 也就是输出的时候是字符串, 但是内部其实是一个对象.

Spring 的EL表达式用在支持EL表达式的注解中, XML文件和Properties文件中, 可以发现, 这些文件或者注解, 都需要Spring解析, Spring解析的时候就会调用相关的类, 然后生成解析后的结果, 也就是一个对象. 很类似AspectJ注解中的切点函数表达式, 都是以字符串的形式写下语句然后解析.

既然是一个表达式, 肯定想到了, 要有一个语句对象, 然后有一个解析对象, 或者调用语句的解析方法, 得到一个解析后的语句, 然后就可以获得执行结果.

SpringEL表达式的支持都在org.springframework.expression 和 spel.supprot中.

SpringEL核心接口

SpringEL表达式的字符串形式是#{xxxx.xxxx}, 熟悉JSP 的EL表达式的话可以非常快的上手.

SpringEL的核心接口(类)如下:

  1. ExpressionParse, 这是一个解析器, 调用其parseExpression方法, 参数传入一个表达式, 就可以得到一个Expression对象
  2. Expression, 这是一个解析后的表达式对象, 通过getValue(T.class)得到结果T类型.
  3. EvaluationContext, 由于SpringEL表达式最常见的就是通过特定对象的属性名称获取值, 所以提供了这样一个类, 这个类需要指定一个对象, 然后就可以通过属性直接获取值. 可以将这个类的实例看做一个对象包装器, 具有传入的对象的全部属性. 一会来看一下例子.

这些类和方法, Spring在解析上边所说的各种文件的时候会自动调用. 现在就来实际使用一下这些类, 先看一个最标准的, 解析一个表达式, 获取结果:

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class Test1 {
    public static void main(String[] args) {
        //创建parser
        ExpressionParser parser = new SpelExpressionParser();
        //用parser解析一个表达式, 得到一个Expression对象
        Expression expression = parser.parseExpression("'SD GUNDAM G GENERATION'");
        //从Expression对象中获取值
        String value = expression.getValue(String.class);

        System.out.println(value);
    }
}

从程序里可以看出, 解析得到的是一个字符串对象, 这是因为EL表达式中表示字符串使用单引号, 这里的表达式被解析为一个纯字符串.

编写一个简单的User类:

public class User {

    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

然后看如何在上下文里绑定一个对象, 然后通过上下文获取属性:

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class Test1 {
    public static void main(String[] args) {

        User user = new User("cony", 5);

        ExpressionParser parser = new SpelExpressionParser();

        //使用EvaluationContext, 传入一个对象, 将其作为一个上下文环境的根对象
        EvaluationContext context = new StandardEvaluationContext(user);
        //可以认为传入user之后, context对象就可以认为是user对象

        //使用Expression接口重载的方法getValue(EvaluationContext), 从上下文中寻找"name"名称的属性
        String username = (String) parser.parseExpression("name").getValue(context);

        System.out.println(username);
    }
}

SpringEL后台解析表达式的时候, 核心程序结构不外乎上边两种套路, 即直接解释和以对象为基础进行解释.

这里唯一要多说说的就是EvaluationContext, 这个接口经常搭配Expression的setValue()来使用, 这其中还会自动进行基本类型的转换:

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class Test1 {
    public static void main(String[] args) {

        User user1 = new User("cony", 5);
        User user2 = new User("saner", 6);
        ExpressionParser parser = new SpelExpressionParser();

        //创建两个基于不同对象的上下文
        EvaluationContext context1 = new StandardEvaluationContext(user1);
        EvaluationContext context2 = new StandardEvaluationContext(user2);

        //这里的核心就是理解Expression对象其实相当于你获取的属性的包装器. 可以进一步getValue(context)表示从context环境中获取, 也可以向context环境中设置.
        parser.parseExpression("name").setValue(context1, "owl");
        parser.parseExpression("name").setValue(context2, 666);

        System.out.println(user1);
        System.out.println(user2);
    }
}

这段代码运行的结果是:

User{name='owl', age=5}
User{name='666', age=6}

可以看到, 通过绑定不同的上下文, 解析器可以通过名称从不同的上下文中取值和设置值, 这中间还会自动进行类型转换, 如果转换失败, 会抛出异常.`

基本的接口就是这些, 剩下就是看看Spel表达式的语法了.

表达式语法 - 文本字符和基本类型

表达式只要记得将两边引号内的部分当做类似动态语言来看待就可以, 不要当成字符串来看待.

文本在之前使用了单引号, 也可以使用反斜杠加双引号(注意不要看成字符串的转义).

除了字符串之外的基本类型, 默认会采用Double.parseDouble()方法进行转换, 如果想按照特定方法转换, 就使用getValue(Class T)的方法, 看几个例子:

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

        //字符串和基本类型
        String ex1 = "\"hello world\"";
        String ex2 = "'3 + 2 = 5'";
        String ex3 = "6.325";
        String ex4 = "0x73329";
        String ex5 = "true";

        System.out.println(parser.parseExpression(ex1).getValue(String.class));
        System.out.println(parser.parseExpression(ex2).getValue(String.class));
        System.out.println(parser.parseExpression(ex3).getValue(Double.class));
        //强制转换类型
        System.out.println(parser.parseExpression(ex3).getValue(Integer.class));
        System.out.println(parser.parseExpression(ex4).getValue(Integer.class));
        //强制转换类型
        System.out.println(parser.parseExpression(ex4).getValue(Double.class));

        System.out.println(parser.parseExpression(ex5).getValue(Boolean.class));
    }
}

表达式语法 - 取对象属性

这个大概是最常用的了, 写出来就是xxx.yyy.zzz的样子. 其背后的原理就是之前介绍过的对象上下文参数EvalulationContext的使用, 从对象上下文中取可以了. 还可以嵌套.

修改一下User, 给User添加一个Animal类:

public class Animal {

    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class User {

    private String name;
    private int age;
    private Animal animal;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

    public Animal getAnimal() {
        return animal;
    }

    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}

然后试着来创建一个嵌套版本的取属性:

public class Test1 {
    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        //以User为根对象创建上下文
        EvaluationContext context = new StandardEvaluationContext(user);
        //表达式以写根对象的属性开始
        String getProperty = "animal.name";
        //使用重载的getValue()方法从上下文中按照表达式, 取出指定类型的结果
        String animalName = parser.parseExpression(getProperty).getValue(context, String.class);
        System.out.println(animalName);
    }
}

表达式语法 - 取集合类型的内容

在Java里集合也是对象, 所以本质上和取属性一样, 也需要将集合对象设置在EvaluationContext中. 解析表达式分为解析创建数组的表达式, 和解析取内容的表达式.

先看解析创建集合类型:

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

        //创建数组的语句和java很类似, 不能使用多维数组
        String arrayExp = "new int[]{1,2,3,4,5,6}";

        //创建List类型需要使用的大括号
        String listExp = "{\"owl\",\"saner\",\"sitong\"}";

        int[] array1 = parser.parseExpression(arrayExp).getValue(int[].class);

        List<String> list1 = (List<String>) parser.parseExpression(listExp).getValue();

        //创建MAP对象使用大括号中的键值对
        String mapExp = "{user:'cony', age:5}";

        Map<String, Integer> map = (Map<String, Integer>) parser.parseExpression(mapExp).getValue();

        System.out.println(Arrays.toString(array1));

        System.out.println(list1);

        System.out.println(map);
    }
}

然后可以从中取出内容, 取法全部都很类似Python或者JavaScript的方法.取得时候要将对象放入上下文对象中:

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

    String arrayExp = "new int[]{1,2,3,4,5,6}";
    String listExp = "{\"owl\",\"saner\",\"sitong\"}";
    String mapExp = "{user:'cony', age:5}";

    int[] array1 = parser.parseExpression(arrayExp).getValue(int[].class);
    List<String> list1 = (List<String>) parser.parseExpression(listExp).getValue();
    Map<String, Integer> map = (Map<String, Integer>) parser.parseExpression(mapExp).getValue();

    EvaluationContext arrayContext = new StandardEvaluationContext(array1);
    //取数组用索引
    int thirdElement = parser.parseExpression("[2]").getValue(arrayContext,Integer.class);

    EvaluationContext listContext = new StandardEvaluationContext(list1);
    //取list也是用索引
    String secondListElement = parser.parseExpression("[1]").getValue(listContext, String.class);

    EvaluationContext mapContext = new StandardEvaluationContext(map);
    //取Map中的内容用键名
    String name = parser.parseExpression("[\"user\"]").getValue(mapContext, String.class);
    int age = parser.parseExpression("['age']").getValue(mapContext, Integer.class);

    System.out.println(thirdElement);
    System.out.println(secondListElement);
    System.out.println(name);
    System.out.println(age);
}

表达式语法 - 方法解析

由于表达式被当成程序代码解析, 而不是字符串, 所以在其中还可以像程序里边一样调用方法. 可以调用实例方法, 但是不能调用私有方法. 不过在EL表达式里调用方法会比较难看, 知道就好.

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

    User user = new User("cony", 5);
    //调用字符串类的方法
    String[] result = parser.parseExpression("'hello world'.split(\" \")").getValue(String[].class);

    //调用对象方法, 通过上下文
    EvaluationContext userContext = new StandardEvaluationContext(user);
    int age = parser.parseExpression("getAge()").getValue(userContext, Integer.class);

    System.out.println(Arrays.toString(result));
    System.out.println(age);

}

表达式语法 - 操作符解析

操作符分大类为关系操作符, 逻辑操作符, 算术运算操作符. 写法和Java一样, 来看每个种类中比较特殊的操作符.

  1. 关系操作符
    1. intanceof T(Class), 这个是固定用法, 要用一个字符 T, 然后括号内部是Java的类型名称.
    2. matches, 直接跟在字符串后边, 然后是Java的正则表达式.
    3. 可以直接用大于号和小于号来比较字符串
  2. 逻辑操作符
    1. 可以采用and 和 or 两个关键字来进行逻辑运算, 其他标准的运算符也可以
    2. 毕竟是按照Java来解释, 不能使用0和整数作为布尔值, 必须使用Java标准的布尔值
  3. 算术操作符
    1. 加号可以用于字符串和日期
    2. 减号可以用于日期
    3. 乘除法只可以用于数字, 还可以使用取模%和幂^, 运算顺序和标准的Java一样.

来看一个综合的例子:

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

        //关系运算符 - 比较字符串
        System.out.println(parser.parseExpression("'cony' > 'owl'").getValue(Boolean.class));
        //关系运算符 - 判断类型
        System.out.println(parser.parseExpression(" 30.0 instanceof T(String)").getValue(Boolean.class));
        System.out.println(parser.parseExpression(" 30.0 instanceof T(Double)").getValue(Boolean.class));
        //关系运算符 - 使用正则表达式
        System.out.println(parser.parseExpression("'saner' matches '^s[a-z]+'").getValue(Boolean.class));
        System.out.println(parser.parseExpression("'saner' matches '^\\w+'").getValue(Boolean.class));

        //逻辑运算符
        System.out.println(parser.parseExpression("true && false").getValue(Boolean.class));
        System.out.println(parser.parseExpression("true and true").getValue(Boolean.class));
        System.out.println(parser.parseExpression("true or false").getValue(Boolean.class));

        //算术运算符
        System.out.println(parser.parseExpression("4 * 5 -32 ").getValue());
        System.out.println(parser.parseExpression("0x8832 % 7 ").getValue());
    }
}

表达式语法 - ?系操作符

这一系列操作符就有动态语言的特色了, 都是内部进行一个小判断, 然后再输出结果, 有如下三种:

  1. 安全导航操作符, 来自基于JDK的动态语言Groovy, 用法是 name?, 如果name是null, 就返回null, 否则返回结果, 有点像Optional. 看一个例子:
        public static void main(String[] args) {
            ExpressionParser parser = new SpelExpressionParser();
            //没有给user对象设置animal属性
            User user = new User("cony", 5);
    
            EvaluationContext userContext = new StandardEvaluationContext(user);
    
            System.out.println(parser.parseExpression("animal?.name").getValue(userContext, String.class));
    
            //设置之后就可以显示查询的结果
            user.setAnimal(new Animal("owl"));
            System.out.println(parser.parseExpression("animal?.name").getValue(userContext, String.class));
        }
    
  2. 三元操作符, 和很多动态语言里类似, 表达式1? 表达式2:表达式3, 这个例子就省略了.
  3. Elvis操作符, 其实就是一个简化版的三元表达式, 用法是 表达式1?:表达式2. 可以看到相比三元操作省略了一个表达式, 意思是如果表达式1是null, 就取表达式2, 否则取表达式1.

表达式语法 - 赋值, 类型, 构造器, 变量

一个短短的表达式, 也做了足够多的内容. 还可以给上下文对象赋值, 使用setValue方法或者直接在表达式里设置都可以, 解析表达式的时候就完成了设置:

public static void main(String[] args) {
    ExpressionParser parser = new SpelExpressionParser();
    //没有给user对象设置animal属性
    User user = new User("cony", 5);

    EvaluationContext userContext = new StandardEvaluationContext(user);
    //先解析表达式找到animal属性, 然后通过setValue方法设置指定的属性
    //这里顺便就使用了构造器
    parser.parseExpression("animal").setValue(userContext, new cc.conyli.el.Animal("owl"));
    System.out.println(user);

    //还可以直接使用表达式设置:
    parser.parseExpression("animal = new cc.conyli.el.Animal(\"kiki\")").getValue(userContext);
    System.out.println(user);
}

构造器的写法和Java完全一致, 只不过除了lang包中的类, 其他类要使用全称.

刚才在instanceof 中已经使用了T()操作, 这个操作实际上相当于加载类, 返回这个类的Class对象, 比如:

public static void main(String[] args) {
    ExpressionParser parser = new SpelExpressionParser();
    //没有给user对象设置animal属性
    User user = new User("cony", 5);

    Class aClass = parser.parseExpression("T(cc.conyli.el.User)").getValue(Class.class);

    System.out.println(aClass);
    System.out.println(aClass == user.getClass());
}

T操作还有一个特殊之处是可以调用静态方法:

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

    //调用静态方法
    double number = parser.parseExpression("T(java.lang.Math).random()").getValue(Double.class);
    System.out.println(number);
}

接着是使用变量的奇技淫巧, 变量需要设置在上下文对象中, 使用变量的时候前边要加#:

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

    User cony = new User("cony", 5);

    EvaluationContext conyContext = new StandardEvaluationContext(cony);
    conyContext.setVariable("varName", "conyli");

    //将name属性的值编程varName对应的值
    parser.parseExpression("name=#varName").getValue(conyContext);

    System.out.println(cony);
}

有了在上下文中设置变量的方法, 就可以自由的设置变量, 然后通过表达式来获取或者修改值.

表达式语法 - 集合过滤和转换

这个有点意思, 对于一个集合对象, 可以使用特殊的 集合对象.?[布尔表达式] 来过滤对象. 只会保留集合中布尔表达式为真的内容.

绿色的?表示保留全部符合条件的对象, 还可以使用 ^ 保留第一个, $ 来保留最后一个, 看例子:

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

        //准备两个对象
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        Map<String, Integer> maps = new HashMap<>();
        maps.put("cony", 5);
        maps.put("owl", 2);
        maps.put("dazhuan", 14);
        maps.put("xczhuan", 10);

        //创建一个空的上下文然后将两个集合设置到上下文中
        EvaluationContext context = new StandardEvaluationContext();

        context.setVariable("list", list);
        context.setVariable("map", maps);

        //过滤list中大于5的元素, 对于List的过滤, 注意要使用#this变量来代替其中的每一个元素(因为无法使用索引), 这是特殊之处
        List<Integer> listAfterFilter = (List<Integer>) parser.parseExpression("#list.?[#this>5]").getValue(context);
        System.out.println(listAfterFilter);

        //过滤map则需要使用key关键字表示每个键, value关键字表示每个键对应的值
        Map<String, Integer> mapAfterFilter1 = (Map<String, Integer>) parser.parseExpression("#map.$[key.equals('cony')]").getValue(context);
        Map<String, Integer> mapAfterFilter2 = (Map<String, Integer>) parser.parseExpression("#map.?[value>=10]").getValue(context);
        System.out.println(mapAfterFilter1);
        System.out.println(mapAfterFilter2);
    }

过滤集合的写法比较特殊, 一定要注意. 都是固定写法, 没有键的集合使用#this, 有键值的集合使用key和value, 都是固定用法.

集合转换类似于函数式编程的map(), 只需要把上边的表达式中的?$^所在的位置换成!, 然后表达式写一个得到一个值的表达式, 就可以转换, 结果得到一个新的集合:

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

    //创建一个集合
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }

    //创建一个空的上下文然后设置集合到上下文中
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("list", list);

    //转换List<Integer>每个元素加上10, 然后转换到List<String>
    List<String> result = (List<String>) parser.parseExpression("#list.![new String(#this+10)]").getValue(context);
    System.out.println(result);
}

实际的Spring EL 表达式使用

实际的Spring EL表达式当然不是怎么写代码了, 主要就用在两个地方:

一是Spring在解析所有的XML文件的时候, 遇到SpringEL表达式就会调用上边我们学过的类来进行处理, 并将结果替换到表达式的部分.

二是与注解@Value搭配使用, 从配置文件中加载参数的值.

在实际使用的时候, 采取#{}的形式, #{}就相当于直接使用上边那些类时候的双引号.

例子如下:

<bean id="id1" class="cc.conyli.raw.MyImplClass"
    p:randomNumber = #{T(java.lang.Math).random() * 100}
/>

搭配@Value使用的时候, 需要在Spring 的容器XML配置文件中写如下内容:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <util:properties id="properties" location="config.properties"/>
    ......
</beans>

如果config.properties中的内容如下:

name=cony
age=5

在容器中配置Bean的时候就可以写:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Bean {
    @Value("#{properties['age']}")
    private int age;
}

可以看到@Value注解是和BeanFactory相关的, 橙色部分就是配置的utilBean的id名称, 配置文件中的每一个键值对, 就用键名就可以获取到值, 如果是基本类型还可以自动转换.

"#{properties['age']}"这种方式写起来比较麻烦, 只要给utilBean的配置中添加:

<util:properties id="properties" location="config.properties"/>
<context:property-placeholder properties-ref="properties"/></pre>

这一行表示将默认的上边橙色部分的utilBean的名称, 对应给properties-ref属性中指向的Bean. 这样配置之后, 不显式写出橙色部分的utilBean名称, 就会自动去寻找这个默认的utilBean:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Bean {
    @Value("#{age}")
    private int age;
}

如果定义了多个utilBean以对应多个配置文件的话, 还是需要区分具体的名称.

Spring EL表达式背后的东西终于弄明白了, 技术总是进步的, 也是有相似之处的, 从JSP的标签处理和EL表达式, 看到Spring的EL表达式, 再联想起Vue的解析模板, 果然技术是要越涉猎的多, 越有意思啊.

LICENSED UNDER CC BY-NC-SA 4.0
Comment