掘金 后端 ( ) • 2024-04-01 16:17

highlight: a11y-dark

1. 简单介绍

1.1. 什么是 Spring EL

Spring3 中引入了 Spring 表达式语言 — Spring EL,SpEL 是一种强大,简洁的装配 Bean 的方式,它可以通过运行期间执行的表达式将值装配到我们的属性或构造函数当中,更可以调用 JDK 中提供的静态常量,获取外部 Properties 文件中的的配置。

1.2. 为什么要使用 Spring EL

我们平常通过配置文件或注解注入的 Bean,其实都可以称为静态性注入,试想一下,如果我的 Bean A 中有变量 A,它的值需要根据 Bean B 的 B 变量为参考,在这个场景下静态注入就显得非常无力,而 Spring3 增加的 Spring EL 就可以完全满足这种需求,而且还可以对不同 Bean 的字段进行计算再进行赋值,功能非常强大。

1.3. 如何使用 Spring EL

Spring EL 从名字来看就能看出,和 EL 是有关系的,Spring EL 的使用和 EL 表达式的使用非常相似,EL 表达式在 JSP 页面更方便的获取后台中的值,而 Spring EL 就是为了更方便获取 Spring 容器中的 Bean 的值,EL 使用${},而 Spring EL 使用#{}进行表达式的声明。

2. 简单使用

2.1. 辅助类代码

package com.example.spel.bean;

import org.springframework.stereotype.Component;
import java.util.*;

@Component
public class TestConstant {
    public static final String STR = "测试SpEL";
    public String nickname = "一线大码";
    public String name = "笑傲江湖";
    public int num = 5;
    public List<String> testList = Arrays.asList("aaa", "bbb", "ccc");
    public Map testMap = new HashMap() {{
        put("aaa", "元宇宙算法");
        put("hello", "world");
    }};
    public List cityList = new ArrayList<City>() {{
        add(new City("aaa", 500));
        add(new City("bbb", 600));
        add(new City("ccc", 1000));
        add(new City("ddd", 1000));
        add(new City("eee", 2000));
        add(new City("fff", 3000));
    }};

    public String showProperty() {
        return "Hello";
    }

    public String showProperty(String str) {
        return "Hello " + str + "!";
    }
}
package com.example.spel.bean;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class City {

    private String name;
    private long population;
}

2.2. 测试类代码

package com.example.spel.el;

import com.example.spel.bean.City;
import com.example.spel.bean.TestConstant;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;

@ToString
@Component
public class TestSpringEL {

    /**
     * 注入简单值,输出num为5
     */
    @Value("#{5}")
    private Integer num;

    @Value("#{'rain'.toUpperCase()}")
    private String name;

    //注入bean,访问属性和方法

    /**
     * 注入ID为testConstant的Bean
     */
    @Value("#{testConstant}")
    private TestConstant testConstant;

    /**
     * 注入ID为testConstant的Bean中的STR常量/变量
     */
    @Value("#{testConstant.STR}")
    private String str;

    /**
     * 调用无参方法
     */
    @Value("#{testConstant.showProperty()}")
    private String method1;

    /**
     * 调用有参方法,接收字符串
     */
    @Value("#{testConstant.showProperty('World')}")
    private String method2;

    /**
     * 方法返回的String为大写
     */
    @Value("#{testConstant.showProperty().toUpperCase()}")
    private String method3;

    /**
     * 使用method3这种方式,如果showProperty返回为null,将会抛出NullPointerException,可以使用以下方式避免。
     * 使用?.符号表示如果左边的值为null,将不执行右边方法
     */
    @Value("#{testConstant.showProperty()?.toUpperCase}")
    private String method4;

    //注入JDK中的工具类常量或调用工具类的方法

    /**
     * 获取Math的PI常量
     */
    @Value("#{T(java.lang.Math).PI}")
    private double pi;

    /**
     * 调用random方法获取返回值
     */
    @Value("#{T(java.lang.Math).random()}")
    private double ramdom;

    /**
     * 获取文件路径符号
     */
    @Value("#{T(java.io.File).separator}")
    private String separator;

    //使用SpringEL进行运算及逻辑操作

    /**
     * 拼接字符串
     */
    @Value("#{testConstant.nickname + ' ' + testConstant.name}")
    private String concatString;

    /**
     * 对数字类型进行运算
     */
    @Value("#{ 3 * T(java.lang.Math).PI + testConstant.num}")
    private double operation;

    /**
     * 进行逻辑运算
     */
    @Value("#{testConstant.num > 100 and testConstant.num <= 200}")
    private boolean logicOperation;

    /**
     * 进行或非逻辑操作
     */
    @Value("#{not(testConstant.num == 100 or testConstant.num <= 200)}")
    private Boolean logicOperation2;

    /**
     * 使用三元运算符
     */
    @Value("#{testConstant.num > 100 ? testConstant.num : testConstant.num + 100}")
    private Integer logicOperation3;

    //SpringEL使用正则表达式

    /**
     * 验证是否邮箱地址正则表达式
     */
    @Value("#{testConstant.STR matches '\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+'}")
    private boolean regularExpression;

    //TestConstant类中有名为testList的List变量,和名为testMap的Map变量

    /**
     * 获取下标为0的元素
     */
    @Value("#{testConstant.testList[0]}")
    private String firstStr;

    /**
     * 获取下标为0元素的大写形式
     */
    @Value("#{testConstant.testList[0]?.toUpperCase()}")
    private String upperFirstStr;

    /**
     * 获取map中key为hello的value
     */
    @Value("#{testConstant.testMap['hello']}")
    private String mapValue;

    /**
     * 根据testList下标为0元素作为key获取testMap的value
     */
    @Value("#{testConstant.testMap[testConstant.testList[0]]}")
    private String mapValueByTestList;

    //声明City类,有population(人口)属性。testConstant拥有名为cityList的City类List集合

    /**
     * 过滤testConstant中cityList集合population属性大于1000的全部数据注入到本属性
     */
    @Value("#{testConstant.cityList.?[population > 1000]}")
    private List<City> cityList;

    /**
     * 过滤testConstant中cityList集合population属性等于1000的第一条数据注入到本属性
     */
    @Value("#{testConstant.cityList.^[population == 1000]}")
    private City city;

    /**
     * 过滤testConstant中cityList集合population属性小于1000的最后一条数据注入到本属性
     */
    @Value("#{testConstant.cityList.$[population < 1000]}")
    private City city2;

    /*
     * 首先为city增加name属性,代表城市的名称
     */

    /**
     * 假如我们在过滤城市集合后只想保留城市的名称,可以使用如下方式进行投影
     */
    @Value("#{testConstant.cityList.?[population > 1000].![name]}")
    private List<String> cityName;
}

2.3. 执行测试

package com.example.spel;

import com.example.spel.el.TestSpringEL;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author wangbo
 * @date 2021/11/26
 */
@SpringBootTest
class Test1 {

    @Autowired
    private TestSpringEL testSpringEL;

    @Test
    void test(){
        System.out.println(testSpringEL);
    }
}

测试结果:

TestSpringEL(num=5, name=rain, name1=RAIN, testConstant=com.example.spel.bean.TestConstant@35cd68d4, str=测试SpEL, method1=Hello, method2=Hello World!, method3=HELLO, method4=HELLO, pi=3.141592653589793, ramdom=0.38512203414759527, separator=\, concatString=一线大码 笑傲江湖, operation=14.42477796076938, logicOperation=false, logicOperation2=false, logicOperation3=105, regularExpression=false, firstStr=aaa, upperFirstStr=AAA, mapValue=world, mapValueByTestList=元宇宙算法, cityList=[City(name=eee, population=2000), City(name=fff, population=3000)], city=City(name=ccc, population=1000), city2=City(name=bbb, population=600), cityName=[eee, fff])

3. EL 表达式解析引擎

3.1. 带缓存的 EL 表达式工具类

package gtcom.governance.impl.util;

import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * SpEL表达式解析
 *
 * @author wangbo
 * @date 2021/11/26
 */
public class ExpressionUtils {

    private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();

    private static final Map<String, Expression> EXPRESSION_CACHE = new ConcurrentHashMap<>();

    /**
     * 获取解析后的表达式
     *
     * @param expression EL表达式字符串
     * @return 解析后的表达式,如果之前已经解析过,则返回缓存的表达式
     */
    public static Expression getExpression(String expression) {
        if (StringUtils.isBlank(expression)) {
            return null;
        }
        expression = expression.trim();
        return EXPRESSION_CACHE.computeIfAbsent(expression, EXPRESSION_PARSER::parseExpression);
    }
}

上面工具的测试代码:

package gtcom.governance.impl.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.expression.Expression;

import java.util.Objects;

/**
 * 测试SpEL表达式解析
 *
 * @author wangbo
 * @date 2021/11/26
 */
public class TestExpression {

    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode root = mapper.createObjectNode();
        root.put("pubTime", "11111");
        ObjectNode supplier = root.putObject("supplier");
        supplier.put("sourceType", "TestSourceType");
        supplier.put("comFrom", "QX");

        System.out.println(mapper.writeValueAsString(root));

        String sourceType = root.get("supplier").path("sourceType").asText();
        System.out.println(sourceType);

        String expression1 = "get('supplier').path('sourceType').asText";
        Expression expr1 = ExpressionUtils.getExpression(expression1);
        System.out.println(expr1);
        String sourceType1 = Objects.requireNonNull(expr1).getValue(root, String.class);
        System.out.println(sourceType1);

        String expression2 = "get('supplier').path('sourceType').asText";
        Expression expr2 = ExpressionUtils.getExpression(expression2);
        System.out.println(expr2);
        String sourceType2 = Objects.requireNonNull(expr2).getValue(root, String.class);
        System.out.println(sourceType2);
    }
}

3.2. 设置上下文的 EL 表达式

package com.example.spel;

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

public class TestStringSubExpression {
    public static void main(String[] args) {
        String expressionStr = "'hello world'.toUpperCase().substring(1,5)";
        //指定SpelExpressionParser解析器实现类
        ExpressionParser parser = new SpelExpressionParser();
        //解析表达式
        Expression expression = parser.parseExpression(expressionStr);
        System.out.println(expression.getValue());
    }
}

等价设置了上下文的代码:

package com.example.spel;

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

public class TestStringSubExpression {
    public static void main(String[] args) {
        String expressionStr = "'hello world'.toUpperCase().substring(#start, #end)";
        //指定SpelExpressionParser解析器实现类
        ExpressionParser parser = new SpelExpressionParser();
        //解析表达式
        Expression expression = parser.parseExpression(expressionStr);
        //设置对象模型基础
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("start", 1);
        context.setVariable("end", 5);
        System.out.println(expression.getValue(context));
    }
}

另一个设置了上下文的 SpEL 测试代码:

package com.example.spel;

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

public class Test {
    public static void main(String[] args) {
        //测试SpringEL解析器。
        //设置文字模板,其中#{}表示表达式的起止,#user是表达式字符串,表示引用一个变量。
        String template = "#{#user},早上好";
        //创建表达式解析器。
        ExpressionParser parser = new SpelExpressionParser();

        //通过evaluationContext.setVariable可以在上下文中设定变量。
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("user", "黎明");

        //解析表达式,如果表达式是一个模板表达式,需要为解析传入模板解析器上下文。
        Expression expression = parser.parseExpression(template, new TemplateParserContext());

        //使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文,第二个参数是类型参数,表示返回值的类型。
        System.out.println(expression.getValue(context, String.class));
    }
}

代码执行结果:

黎明,早上好

4. @Value 中使用 EL

@Value中的${...}表示占位符,它会读取上下文的属性值装配到属性中。

@Value中的#{...}表示启用 Spring 表达式,具有运算功能。

T(...)表示引入类,Systemjava.lang.*包下的类,是 Java 默认加载的包,因此可以不用写全限定名,如果是其它的包,则需要写出全限定名才能引用类。

@Value("${database.driverName}")
private String driver;

@Value("#{T(System).currentTimeMillis()}")
private Long initTime = null;

4.1. 使用 Spring EL 进行赋值

//赋值字符串
@Value("#{'使用 Spring EL 赋值字符串'}")
private String str = null;

//科学计数法赋值
@Value("#{9.3E3}")
private double d;

//赋值浮点数
@Value("#{3.14}")
private float pi;
//赋值 bean 的属性
@Value("#{beanName.str}")
private String otherBeanProp = null;

//?表示判断是否为 null,不为空才会执行后面的表达式
@Value("#{beanName.str?.toUpperCase()}")
private String otherBeanProp1 = null;

4.2. 使用 Spring EL 进行计算

//数学运算
@Value("#{1+2}")
private int run;

//浮点数比较运算
@Value("#{beanName.pi == 3.14f}")
private boolean piFlag;

//字符串比较运算
@Value("#{beanName.str eq 'Spring Boot'}")
private boolean strFlag;

//字符串连接
@Value("#{beanName.str + ' 连接字符串'}")
private String strApp = null;

//三元运算
@Value("#{beanName.d > 1000 ? '大于' : '小于'}")
private String resultDesc = null;

5. 总结

优点:

Spring EL 功能非常强大,在 Annotation 的方式开发时可能感觉并不强烈,因为可以直接编写到源代码来实现 Spring EL 的功能,但如果是在 XML 文件中进行配置,Spring EL 可以弥补 XML 静态注入的不足,从而实现更强大的注入。

缺点:

Spring EL 在使用时仅仅是一个字符串,不易于排错与测试,也没有 IDE 检查我们的语法(目前 DIEA 可以检测 EL 语言),当出现错误时较难检测。

总结:

Spring3.0 让人为之惊艳的非 Spring EL 莫属,为我们的注入提供了另一种强大的形式,传统注入能做到的事情,和做不到的事情,Spring EL 一概能完成,但在项目当中并不适宜大量使用 Spring EL,适当的技术放在适当的位置,才能更好的完成事情。