SpEL表达式


Spring表达式语言(简称“ SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。虽然SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,而是可以独立使用。这里我们就针对官网示例做一下学习!

SpEL可以在运行时查询和操作数据 ,我们甚至可以将表达式放到数据库,支持实时更新!SpEL表达式语言支持以下功能:

  • Literal expressions(字面表达式)
  • Boolean and relational operators(布尔运算符和关系运算符)
  • Regular expressions(正则表达式)
  • Class expressions(类表达式)
  • Accessing properties, arrays, lists, and maps(访问属性,数组,列表和map)
  • Method invocation(方法调用)
  • Relational operators(关系运算符)
  • Assignment(赋值)
  • Calling constructors(调用构造函数)
  • Bean references(bean引用)
  • Array construction(构造数组)
  • Inline lists(内联列表)
  • Inline maps(内联map)
  • Ternary operator(三元运算符)
  • Variables(变量)
  • User-defined functions(用户定义的函数)
  • Collection projection(集合投影)
  • Collection selection(集合选择)
  • Templated expressions(模板表达式)

这里我直接使用一个spring boot项目来演示(spring版本5.3.4),build.gradle如下:

plugins {
    id 'org.springframework.boot' version '2.4.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'cn.justme'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenLocal()
    maven { url 'https://repo.spring.io/milestone' }
    maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
    mavenCentral()
    jcenter()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

一. Evaluation

本节介绍SpEL接口的简单用法及其表达语言

示例1:

public class SpelTest {
    /**
     * 该 ExpressionParser 接口负责解析表达式字符串
     */
    private final ExpressionParser parser = new SpelExpressionParser();

    @Test
    void test1() {
        // 评估字符串表达式
        Expression exp = parser.parseExpression("'Hello World'");
//        String message = (String) exp.getValue();
        String message = exp.getValue(String.class);
        Assertions.assertEquals(message, "Hello World");
    }
  
    @Test
    void test2() {
        // 调用javaBean的属性
        Expression exp = parser.parseExpression("'Hello World'.concat('!')");
        String message = exp.getValue(String.class);
        Assertions.assertEquals(message, "Hello World!");
    }
  
    @Test
    void test3() {
        // 方法属性
        // SpEL还通过使用标准的点符号(例如prop1.prop2.prop3)以及相应的属性值设置来支持嵌套属性 。也可以访问公共字段。
        // invokes 'bgetBytes()'
        Expression exp = parser.parseExpression("'Hello World'.bytes");
        byte[] value = exp.getValue(byte[].class);
        Assertions.assertArrayEquals(value, "Hello World".getBytes());
      
        // invokes 'getBytes().length'
        Expression exp1 = parser.parseExpression("'Hello World'.bytes.length");
        Integer length = exp1.getValue(int.class);
        Assertions.assertEquals(length, "Hello World".getBytes().length);
    }
  
    @Test
    void test4() {
        // 调用构造器
        Expression exp = parser.parseExpression("new String('Hello World').toUpperCase()");
        String message = exp.getValue(String.class);
        Assertions.assertEquals(message, "Hello World".toUpperCase());
    }

}

1. EvaluationContext

在评估表达式以解析属性,方法或字段并帮助执行类型转换时,使用EvaluationContext接口。 Spring提供了两种实现:

  • SimpleEvaluationContext: 针对不需要SpEL语言语法完整范围且应受到有意义限制的表达式类别,公开了SpEL基本语言功能和配置选项的子集
  • StandardEvaluationContext: 公开了完整的SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略.

SimpleEvaluationContext设计为仅支持SpEL语言语法的子集。它不包括Java类型引用,构造函数和Bean引用。它还要求您明确选择对表达式中的属性和方法的支持级别。默认情况下,create()静态工厂方法仅启用对属性的读取访问。您还可以获取构建器来配置所需的确切支持级别,并针对以下一种或某些组合:

  • Custom PropertyAccessor only (no reflection)
  • Data binding properties for read-only access
  • Data binding properties for read and write

示例:

@Test
void test5() {
    Simple simple = new Simple();
    simple.booleanList.add(true);

    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    // "false" is passed in here as a String. SpEL and the conversion service
    // will recognize that it needs to be a Boolean and convert it accordingly.
    parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

    // b is false
    Assertions.assertFalse(simple.booleanList.get(0));
}

2. Parser Configuration

可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)配置SpEL表达式解析器。配置对象控制某些表达式组件的行为。例如,如果您索引到数组或集合中,并且指定索引处的元素是null,SpEL可以自动创建元素。当使用由属性引用链组成的表达式时,这很有用。如果您在数组或列表中的索引超出了数组或列表当前大小的末尾,则SpEL可以自动增长数组或列表以容纳该索引。为了在指定的索引处添加元素,SpEL将尝试在设置指定的值之前使用元素类型的默认构造函数创建该元素。如果元素类型没有默认构造函数,null则将其添加到数组或列表中。如果没有知道如何设置值的内置或自定义转换器,null它将保留在数组或列表中的指定索引处。下面的示例演示如何自动增加列表:

class Demo {
    public List<String> list;
}

@Test
void test6() {
    // Turn on:
    // - auto null reference initialization
    // - auto collection growing
    SpelParserConfiguration config = new SpelParserConfiguration(true,true);

    ExpressionParser parser = new SpelExpressionParser(config);

    Expression expression = parser.parseExpression("list[3]");
    Demo demo = new Demo();
    Object o = expression.getValue(demo);

    // demo.list will now be a real collection of 4 entries
    Assertions.assertEquals(o, "");
    // Each entry is a new empty String
    Assertions.assertLinesMatch(demo.list, Lists.list("", "", "", ""));
}

3. SpEL Compilation

Spring Framework 4.1包含一个基本的表达式编译器。通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵活性,但不能提供最佳性能。对于偶尔使用表达式来说这很好,但是,当与其他组件(如Spring Integration)一起使用时,性能可能非常重要,并且不需要动态性。

SpEL编译器旨在满足这一需求。在评估过程中,编译器生成一个Java类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式评估。由于缺少在表达式周围输入内容的信息,因此编译器在执行编译时会使用在表达式的解释求值过程中收集的信息。例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释求值时就知道它是什么。当然,如果各种表达元素的类型随时间变化,则基于此类派生信息进行编译会在以后引起麻烦。因此,编译最适合类型信息在重复求值时不会改变的表达式。

Compiler Configuration(编译器配置)

默认情况下不打开编译器,但是您可以通过两种不同的方式之一来打开它。当SpEL用法嵌入到另一个组件中时,可以使用解析器配置过程或使用系统属性(spring.expression.compiler.mode,可选值off, immediate,或mixed)来打开它.

编译器可以在org.springframework.expression.spel.SpelCompilerMode枚举中捕获的三种模式之一下运行 ,如下:

  • OFF: 关闭,默认值
  • IMMEDIATE: 在立即模式下,将尽快编译表达式。通常是在第一次解释评估之后。如果编译后的表达式失败(通常是由于类型更改,如前所述),则表达式求值的调用者将收到异常。
  • MIXED: 在混合模式下,表达式会随着时间静默在解释模式和编译模式之间切换。经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(例如,如前面所述的类型更改),则表达式会自动再次切换回解释形式。稍后,它可能会生成另一个已编译的表单并切换到该形式。基本上,用户进入IMMEDIATE模式的异常是在内部处理的。

IMMEDIATE之所以存在mode,是因为MIXEDmode可能会导致具有副作用的表达式出现问题。如果已编译的表达式在部分成功后就崩溃了,则它可能已经完成了影响系统状态的操作。如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,因为表达式的一部分可能运行了两次。

// 配置编译器
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);
Compiler Limitations(编译器限制)

Spring Framework 4.1开始,已经有了基本的编译框架。但是,该框架尚不支持编译每种表达式。最初的重点是可能在性能关键型上下文中使用的通用表达式。目前无法编译以下类型的表达式:

  • 涉及赋值的表达
  • 表达式依赖转换服务
  • 使用自定义解析器或访问器的表达式
  • 使用选择或投影的表达式

将来会编译更多类型的表达。

二. bean定义中使用表达式

您可以将SpEL表达式与基于XML或基于注释的配置元数据一起使用来定义BeanDefinition实例。在这两种情况下,定义表达式的语法均为形式#{ <expression string> }

1. xml配置

设置属性或者构造器参数值

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

应用程序上下文中的所有bean都可以使用其公共bean名称作为预定义变量使用。这包括用于访问运行时环境的标准上下文Bean,例如environment(类型为 org.springframework.core.env.Environment)以及systemPropertiessystemEnvironment(类型为Map<String, Object>)。

下面的示例显示对systemProperties作为SpEL变量的bean的访问:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

    <!-- other properties -->
</bean>

2. 注解配置

若要指定默认值,可以将@Value注释放在字段,方法以及方法或构造函数参数上。

public class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}


public class PropertyValueTestBean {

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}


public class SimpleMovieLister {

    private MovieFinder movieFinder;
    private String defaultLocale;

    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }

    // ...
}


public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}

三. 语言参考

主要涵盖下面几个部分:

1. 字面量表达式

支持的字面量表达式的类型为字符串,数值(int,实数,十六进制),布尔值和null。字符串用单引号引起来。要将单引号本身放在字符串中,请使用两个单引号字符。

@Test
void test8() {
    // evals to "Hello World"
    String helloWorld1 = (String) parser.parseExpression("'Hello World'").getValue();
    Assertions.assertEquals(helloWorld1, "Hello World");

    // evals to "'Hello World'"
    String helloWorld2 = (String) parser.parseExpression("'''Hello World'''").getValue();
    Assertions.assertEquals(helloWorld2, "'Hello World'");

    double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
    Assertions.assertEquals(avogadrosNumber, 6.0221415E+23);

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

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

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

数字支持使用负号,指数符号和小数点。默认情况下,使用Double.parseDouble()解析实数。

2. 属性,数组,列表,映射和索引器

@Data
public class User {

    private String username;

    private Integer age;

    private PlaceOfBirth placeOfBirth;

    private String[] hobbies;

    private List<String> friends;

    Map<String, String> otherInfo;
  
    public boolean isFriend(String name) {
        if (CollectionUtils.isEmpty(friends)) {
            return false;
        }

        return friends.contains(name);
    }

}

@Test
void test9() {
    User user = new User();
    user.setUsername("shiv");
    user.setAge(24);
    user.setPlaceOfBirth(new PlaceOfBirth("安徽", "中国"));
    user.setHobbies(new String[]{ "basketball" });
    user.setFriends(Lists.newArrayList("lao hu"));
    user.setOtherInfo(Maps.newHashMap("company", "deyi"));

    EvaluationContext context = SimpleEvaluationContext
      .forReadOnlyDataBinding()
      .withRootObject(user)
      .build();
    // 1. 获取属性
    Integer age = parser.parseExpression("age + 4").getValue(context, int.class);
    Assertions.assertEquals(age, user.getAge() + 4);

    String city = parser.parseExpression("placeOfBirth.city").getValue(context, String.class);
    Assertions.assertEquals(city, user.getPlaceOfBirth().getCity());

    // 2. 数组和列表的内容通过使用方括号表示法获得
    String hobby = parser.parseExpression("hobbies[0]").getValue(context, String.class);
    Assertions.assertEquals(hobby, user.getHobbies()[0]);

    String friend = parser.parseExpression("friends[0]").getValue(context, String.class);
    Assertions.assertEquals(friend, user.getFriends().get(0));

    // 3. 获取map值
    String company = parser.parseExpression("otherInfo['company']").getValue(context, String.class);
    Assertions.assertEquals(company, user.getOtherInfo().get("company"));
    // 注意不能这样获取map值
    // parser.parseExpression("otherInfo.company").getValue(context, String.class);

}

3. 内联列表

使用{}符号直接在表达式中表达列表:

@Test
void test10() {
    EvaluationContext context = SimpleEvaluationContext
      .forReadOnlyDataBinding()
      .build();

    // evaluates to a Java list containing the four numbers
    List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
    Assertions.assertIterableEquals(numbers, Lists.newArrayList(1, 2, 3, 4));

    List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
    Assertions.assertIterableEquals(listOfLists, Lists.newArrayList(Lists.newArrayList("a", "b"), Lists.newArrayList("x", "y")));
}

4. 内联map

使用{key:value}符号直接在表达式中表达map:

@Test
void test11() {
    EvaluationContext context = SimpleEvaluationContext
      .forReadOnlyDataBinding()
      .build();

    // evaluates to a Java list containing the four numbers
    Map map = (Map) parser.parseExpression("{a: 1}").getValue(context);
    System.out.println(map); // {a=1}
    Assertions.assertIterableEquals(map.entrySet(), Maps.newHashMap("a", 1).entrySet());
}

5. 构造数组

@Test
void test12() {
    EvaluationContext context = SimpleEvaluationContext
      .forReadOnlyDataBinding()
      .build();

    int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
    Assertions.assertArrayEquals(numbers1, new int[4]);

    // Array with initializer
    int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
    Assertions.assertArrayEquals(numbers2, new int[]{1,2,3});

    // Multi dimensional array 构造多维数组时,当前无法提供初始化程序
    int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
    Assertions.assertArrayEquals(numbers3, new int[4][5]);
}

6. 方法

可以使用Java编程语法来调用方法

@Test
void test13() {
    User user = new User();
    user.setUsername("shiv");
    user.setAge(24);
    user.setPlaceOfBirth(new PlaceOfBirth("安徽", "中国"));
    user.setHobbies(new String[]{ "basketball" });
    user.setFriends(Lists.newArrayList("lao hu"));
    user.setOtherInfo(Maps.newHashMap("company", "deyi"));

    // 不能使用 SimpleEvaluationContext,支持功能有限
    EvaluationContext context = new StandardEvaluationContext(user);

    // string literal, evaluates to "bc"
    String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
    Assertions.assertEquals(bc, "abc".substring(1, 3));

    Boolean value1 = parser.parseExpression("isFriend('lao hu')").getValue(context, Boolean.class);
    Assertions.assertTrue(value1);

    Boolean value2 = parser.parseExpression("isFriend('lao xu')").getValue(context, Boolean.class);
    Assertions.assertFalse(value2);
}

7. 操作符

  • 关系运算符
  • 逻辑运算符
  • 数学运算符
  • 赋值运算符
7.1 关系运算符

支持等于,不等于,小于,小于或等于,大于和大于或等于。

@Test
void test14() {
    Boolean value1 = parser.parseExpression("1 == 1").getValue(Boolean.class);
    Assertions.assertTrue(value1);

    Boolean value2 = parser.parseExpression("1 == 2").getValue(Boolean.class);
    Assertions.assertFalse(value2);

    Boolean value3 = parser.parseExpression("1 < 2").getValue(Boolean.class);
    Assertions.assertTrue(value3);

    // evaluates to true
    boolean value4 = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);
    Assertions.assertTrue(value4);

    boolean value5 = parser.parseExpression("'black' == 'black'").getValue(Boolean.class);
    Assertions.assertTrue(value5);

    boolean value6 = parser.parseExpression("1 + 1 <= 3").getValue(Boolean.class);
    Assertions.assertTrue(value6);
  
    // 与之进行的大于和小于比较null遵循一个简单的规则:null被视为无(不是零)。
    // 任何其他值始终大于null(X > null始终为true),并且其他任何值都不小于零(X < null始终为false)
    boolean value7 = parser.parseExpression("1 > null").getValue(Boolean.class);
    Assertions.assertTrue(value7);

    boolean value8 = parser.parseExpression("1 < null").getValue(Boolean.class);
    Assertions.assertFalse(value8);

    boolean value9 = parser.parseExpression("'a' < null").getValue(Boolean.class);
    Assertions.assertFalse(value9);

    boolean value10 = parser.parseExpression("'a' > null").getValue(Boolean.class);
    Assertions.assertTrue(value10);
  
    boolean value11 = parser.parseExpression("null == null").getValue(Boolean.class);
    Assertions.assertTrue(value11);
}

SpEL还支持instanceof和基于正则表达式的matches运算符:

@Test
void test15() {
    boolean value1 = parser.parseExpression(
      "'xyz' instanceof T(Integer)").getValue(Boolean.class);
    Assertions.assertFalse(value1);

    boolean value2 = parser.parseExpression(
      "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
    Assertions.assertTrue(value2);

    boolean value3 = parser.parseExpression(
      "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
    Assertions.assertFalse(value3);
  
    // 	请注意基本类型,因为它们会立即被包装为包装器类型,因此按预期
    // 	1 instanceof T(int)计算,false
    // 	1 instanceof T(Integer) 计算结果true为。
    boolean value4 = parser.parseExpression(
      "1 instanceof T(Integer)").getValue(Boolean.class);
    Assertions.assertTrue(value4);
  
    boolean value5 = parser.parseExpression(
      "1 instanceof T(int)").getValue(Boolean.class);
    Assertions.assertFalse(value5);
}

每个符号运算符也可以指定为纯字母等效项。这样可以避免出现以下问题:所使用的符号对于嵌入表达式的文档类型具有特殊含义(例如在XML文档中)。等效的文字是:

  • lt (<)
  • gt (>)
  • le (<=)
  • ge (>=)
  • eq (==)
  • ne (!=)
  • div (/)
  • mod (%)
  • not (!)
7.2 逻辑运算符

SpEL支持以下逻辑运算符

  • and&&
  • or||
  • not!
@Test
void test16() {
    boolean value1 = parser.parseExpression("true and false").getValue(Boolean.class);
    Assertions.assertFalse(value1);

    boolean value2 = parser.parseExpression("true or false").getValue(Boolean.class);
    Assertions.assertTrue(value2);

    boolean value3 = parser.parseExpression("!true").getValue(Boolean.class);
    Assertions.assertFalse(value3);
}
7.3 数学运算符

您可以在数字和字符串上使用加法运算符。您只能在数字上使用减法,乘法和除法运算符。您还可以使用模数(%)和指数幂(^)运算符。强制执行标准运算符优先级。以下示例显示了正在使用的数学运算符

@Test
void test17() {
    int value1 = parser.parseExpression("1 + 1").getValue(Integer.class);
    Assertions.assertEquals(value1, 2);

    String value2 = parser.parseExpression("'test' + ' ' + 'string'").getValue(String.class);
    Assertions.assertEquals(value2, "test string");

    int value3 = parser.parseExpression("1 - -3").getValue(Integer.class);
    Assertions.assertEquals(value3, 4);

    double value4 = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);
    Assertions.assertEquals(value4, -9000);

    int value5 = parser.parseExpression("-2 * -3").getValue(Integer.class);
    Assertions.assertEquals(value5, 6);

    double value6 = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);
    Assertions.assertEquals(value6, 24);

    int value7 = parser.parseExpression("6 / -3").getValue(Integer.class);
    Assertions.assertEquals(value7, -2);

    double value8 = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);
    Assertions.assertEquals(value8, 1.0);

    int value9 = parser.parseExpression("7 % 4").getValue(Integer.class);
    Assertions.assertEquals(value9, 3);

    int value10 = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);
    Assertions.assertEquals(value10, 1);

    int value11 = parser.parseExpression("1+2-3*8").getValue(Integer.class);
    Assertions.assertEquals(value11, -21);
}
7.4 赋值运算符
@Test
void test18() {
    User user = new User();
    // 注意 不是 forReadOnlyDataBinding 只读了
    EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

    // 方法一
    parser.parseExpression("username").setValue(context, user, "shiv");
    Assertions.assertEquals(user.getUsername(), "shiv");


    // 方法二,使用 "=" 号
    Integer age = parser.parseExpression(
      "age = 24").getValue(context, user, Integer.class);
    Assertions.assertEquals(age, 24);
    Assertions.assertEquals(age, user.getAge());
}

8. 类型

您可以使用特殊T运算符指定java.lang.Class(类型)的实例。静态方法也可以通过使用此运算符来调用。T()对其中的类型的引用java.lang不需要完全限定,但是所有其他类型的引用都必须是完全限定的。以下示例显示如何使用T运算符:

@Test
void test19() {
    Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
    Assertions.assertEquals(dateClass, Date.class);

    Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
    Assertions.assertEquals(stringClass, String.class);

    boolean trueValue = parser.parseExpression(
      "T(Integer).MIN_VALUE < T(Integer).MAX_VALUE")
      .getValue(Boolean.class);
    Assertions.assertTrue(trueValue);
}

9. 构造器

您可以使用new运算符来调用构造函数。除了基本类型(intfloat等)和String之外,您都应使用完全限定的类名。下面的示例演示如何使用new运算符来调用构造函数:

@Test
void test20() {
    User user = parser.parseExpression("new cn.justme.demo20210222.bean.User()").getValue(User.class);
    Assertions.assertNotNull(user);
}

10. 变量

您可以使用#variableName语法在表达式中引用变量。通过setVariableEvaluationContext实现中使用方法来设置变量:

@Test
void test21() {
    User user = new User();
    user.setUsername("shi");
    EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
    // 设置变量
    context.setVariable("newName", "shiv");

    Object value = parser.parseExpression("username = #newName").getValue(context, user);
    System.out.println(value); // shiv
    Assertions.assertEquals(user.getUsername(), "shiv");
}
#this 和 #root

#this始终定义并引用当前评估对象,#root始终定义并引用根上下文对象

@Test
void test22() {
    // create an array of integers
    List<Integer> primes = Lists.newArrayList(2, 3, 5, 7, 11, 13, 17);

    // create parser and set variable 'primes' as the array of integers
    ExpressionParser parser = new SpelExpressionParser();
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    context.setVariable("primes", primes);

    // all prime numbers > 10 from the list (using selection ?{...})
    // evaluates to [11, 13, 17]
    List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
      "#primes.?[#this > 10]").getValue(context);
    Assertions.assertIterableEquals(primesGreaterThanTen, primes.stream().filter(item -> item > 10).collect(Collectors.toList()));
}

11. 函数

我们还可以通过自定义函数来拓展SpEL

public abstract class StringUtils {

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


 @Test
void test23() throws NoSuchMethodException {
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    // Only static methods can be called via function references
    context.setVariable("reverseString",
                        StringUtils.class.getDeclaredMethod("reverseString", String.class));

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

12. Bean引用

如果评估上下文已使用bean解析器配置,则可以使用@符号从表达式中查找bean.

@SpringBootTest
public class SpelTest {

    @Autowired
    private ApplicationContext applicationContext;
    
    @Test
    void test() {
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setBeanResolver(new BeanFactoryResolver(applicationContext));

        ExpressionParser parser = new SpelExpressionParser();
        Properties result = parser.parseExpression("@systemProperties").getValue(context, Properties.class);
        System.out.println(result);
    }
}

image-20210223135712122

13. 三元运算符

使用方法:

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

14. Elvis运算符

Elvis运算符是三元运算符语法的简化:

name != null? name : "other" 可简写为 name?:"other"

示例:

@Test
void test24() {
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    context.setVariable("name", null);
    context.setVariable("mail", false);

    String value1 = parser.parseExpression("#name?:'xu'").getValue(context, String.class);
    Assertions.assertEquals(value1, "xu");

    Boolean value2 = parser.parseExpression("#mail?:true").getValue(context, Boolean.class);
    Assertions.assertFalse(value2);
}

15. 安全导航符

安全导航运算符主要用于避免NullPointerException!

@Test
void test25() {
    User user = new User();
    user.setUsername("shiv");
    user.setPlaceOfBirth(new PlaceOfBirth("安徽", "中国"));

    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

    String city1= parser.parseExpression("placeOfBirth?.city").getValue(context, user, String.class);
    Assertions.assertEquals(city1, "安徽");

    user.setPlaceOfBirth(null);
    String city2 = parser.parseExpression("placeOfBirth?.city").getValue(context, user, String.class);
    Assertions.assertNull(city2); // 不会抛出空指针
}

16. 集合选择

选择是一种强大的表达语言功能,可让您通过从源集合中进行选择来将其转换为另一个集合,语法.?[selectionExpression]

@Test
void test26() {
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    context.setVariable("list", Lists.newArrayList(1, 2, 3, 4, 5, 6));
    List value = parser.parseExpression("#list.?[#this % 2 == 0]").getValue(context, List.class);
    Assertions.assertIterableEquals(value, Lists.newArrayList(2, 4, 6));

    User user1 = new User();
    user1.setUsername("shiv1");
    user1.setPlaceOfBirth(new PlaceOfBirth("安徽", "中国"));
    User user2 = new User();
    user2.setUsername("shiv2");
    user2.setPlaceOfBirth(new PlaceOfBirth("上海", "中国"));

    context.setVariable("users", Lists.newArrayList(user1, user2));
    List<User> users = (List<User>) parser.parseExpression("#users.?[username == 'shiv1']").getValue(context);
    // 或者
    //        List<User> users = (List<User>) parser.parseExpression("#users.?[#this.username == 'shiv1']").getValue(context);
    System.out.println(users);
    Assertions.assertTrue(users.size() == 1 && "shiv1".equals(users.get(0).getUsername()));
  
  
    // 针对map,将每个map条目( Map.Entry)作为评估选择标准。每个map条目都有其键和值,可作为属性进行访问以供选择。
    Map<String, Integer> map = new HashMap<>();
    map.put("a", 1);
    map.put("b", 2);
    map.put("c", 3);
    map.put("d", 4);
    context.setVariable("map", map);
    Map<String, Integer> result1 = (Map<String, Integer>) parser.parseExpression("#map.?[value % 2 == 0]").getValue(context);
    System.out.println(result1); // {b=2, d=4}
    Map<String, Integer> result2 = (Map<String, Integer>) parser.parseExpression("#map.?[key == 'a']").getValue(context);
    System.out.println(result2); // {a=1}
  
    // 要获得与所选内容匹配的第一个条目,语法为 .^[selectionExpression]。要获取最后的匹配选择,语法为 .$[selectionExpression]
    Map<String, Integer> result3 = (Map<String, Integer>) parser.parseExpression("#map.^[value > 1]").getValue(context);
    System.out.println(result3); // {b=2}

    Map<String, Integer> result4 = (Map<String, Integer>) parser.parseExpression("#map.$[value > 1]").getValue(context);
    System.out.println(result4); // {d=4}
}

17. 集合投影

投影使集合可以驱动子表达式的求值,结果是一个新的集合。投影的语法为.![projectionExpression]

@Test
void test27() {
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

    User user1 = new User();
    user1.setUsername("shiv1");
    user1.setPlaceOfBirth(new PlaceOfBirth("安徽", "中国"));
    User user2 = new User();
    user2.setUsername("shiv2");
    user2.setPlaceOfBirth(new PlaceOfBirth("上海", "中国"));

    context.setVariable("users", Lists.newArrayList(user1, user2));
    List<String> value1 = (List<String>) parser.parseExpression("#users.![username]").getValue(context);
    Assertions.assertIterableEquals(value1, Lists.newArrayList("shiv1", "shiv2"));
}

18. 表达式模板

表达式模板允许将文字文本与一个或多个评估块混合。每个评估块均以您可以定义的前缀和后缀字符分隔。常见的选择是#{ }用作定界符,如以下示例所示:

@Test
void test28() {
    String randomPhrase = parser.parseExpression(
      "random number is #{T(java.lang.Math).random()}",
      new TemplateParserContext()).getValue(String.class);
    System.out.println(randomPhrase); // random number is 0.2509113639671299
}

四. 参考


文章作者: shiv
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 shiv !
评论
  目录