您现在的位置是:首页 >技术教程 >从零开始 Spring Boot 31:Spring 表达式语言网站首页技术教程

从零开始 Spring Boot 31:Spring 表达式语言

魔芋红茶 2024-06-17 11:18:51
简介从零开始 Spring Boot 31:Spring 表达式语言

从零开始 Spring Boot 31:Spring 表达式语言

spring boot

图源:简书 (jianshu.com)

Spring表达式语言(Spring Expression Language,简称 “SpEL”)是一种强大的表达式语言,支持在运行时查询和操作对象图。该语言的语法与统一EL相似,但提供了额外的功能,最显著的是方法调用和基本的字符串模板功能。

评估

直接看一个简单示例:

ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'");
String value = (String) expression.getValue();
System.out.println(value);

SpelExpressionParser是一个Spel表达式解析器,主要实现了ExpressionParser接口:

package org.springframework.expression;

public interface ExpressionParser {
    Expression parseExpression(String expressionString) throws ParseException;
    Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}

org.springframework.expression是Spring中SpEL相关功能的包。

ExpressionParser.parseExpression()可以解析SpEL表达式并返回一个Expression对象。这里的'Hello World'是一个简单的字面量,所以最后的输出的是:

Hello World

Expression.getValue()方法可以获取表达式“评估”(Evaluation)后的值,这个值的类型是Object,需要进行转换。

如果评估失败,会抛出一个EvaluationException异常:

public interface Expression {
    @Nullable
    Object getValue() throws EvaluationException;
	// ...
}

在SpEL中,还可以调用方法:

ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'.concat('!')");
String value = expression.getValue(String.class);
System.out.println(value);

这里调用了String.concat()方法进行字符串连接,最后的输出是:

Hello World!

此外,示例中使用了一个泛型版本的getValue()方法,通过传入一个目标类型的Class对象,可以直接获取相应类型的结果,不用进行强制类型转换。

类似的,SpEL中也可以使用对象属性:

ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) expression.getValue();
System.out.println(Arrays.toString(bytes));

输出结果:

[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]

对属性也可以用.进行级联操作

ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("'Hello World'.bytes.length");
Integer value = (Integer) expression.getValue();
System.out.println(value);

输出结果:

11

在SpEL中,可以使用new关键字使用构造器:

ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("new String('Hello World').toUpperCase()");
String value = (String) expression.getValue();
System.out.println(value);

输出结果:

HELLO WORLD

SpEL更常见的用法是用一个SpEL表达式对指定对象进行评估,用于评估的对象被称作根对象(root object)。

下面看一个这样的示例,假设有这样的数据结构:

@Data
@AllArgsConstructor
private static class Person {
    private String name;
    private Integer age;
    private Address address;

    @Override
    public String toString() {
        return "%s, %d years old, from %s.".formatted(name, age, address.getCountry());
    }
}

@Data
@AllArgsConstructor
private static class Address {
    private String country;
    private String city;
}

创建一个Person对象,并用SpEL表达式进行评估:

Person person = new Person("icexmoon", 20, new Address("China", "NanJin"));
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("name");
String name = (String) expression.getValue(person);
System.out.println(name);

输出:

icexmoon

注意,这里的name没有被单引号包裹,所以并不是一个字面量。所以这个表达式意味着在评估的时候会获取根对象的name属性,相当于person.name

这里同样可以进行级联调用或者调用方法:

expression = expressionParser.parseExpression("address.country");
String country = (String) expression.getValue(person);
System.out.println(country);
expression = expressionParser.parseExpression("toString()");
String personText = (String) expression.getValue(person);
System.out.println(personText);

输出:

China
icexmoon, 20 years old, from China.

可以利用布尔表达式来评估根对象:

Person person = new Person("icexmoon", 20, new Address("China", "NanJin"));
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("age == 20");
Boolean result = expression.getValue(person, Boolean.class);
System.out.println(result);

输出:

true

EvaluationContext

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

  • SimpleEvaluationContext: 暴露了SpEL语言的基本特征和配置选项的一个子集,适用于不需要SpEL语言语法的全部范围,并且应该被有意义地限制的表达类别。例如,包括但不限于数据绑定表达式和基于属性的过滤器。
  • StandardEvaluationContext: 暴露了全套的SpEL语言功能和配置选项。你可以用它来指定一个默认的根对象,并配置每个可用的评估相关策略。

SimpleEvaluationContext 被设计为只支持SpEL语言语法的一个子集。它排除了Java类型引用、构造函数和Bean引用。它还要求你明确选择对表达式中的属性和方法的支持程度。默认情况下, create() 静态工厂方法只允许对属性进行读取访问。你也可以获得一个 builder 来配置所需的确切支持级别,目标是以下的一个或一些组合。

  • 仅限自定义 PropertyAccessor(无反射)。
  • 用于只读访问的数据绑定属性
  • 读和写的数据绑定属性

这里关于EvaluationContext的说明摘抄自官方文档核心技术 (springdoc.cn)

看下面的示例:

    @Setter
    @Getter
    private static class MyList {
        private List<Boolean> list = new ArrayList<>();
    }

    private static void spelTest8() {
        EvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        MyList myList = new MyList();
        myList.list.add(0, false);
        ExpressionParser expressionParser = new SpelExpressionParser();
        Expression expression = expressionParser.parseExpression("list[0]");
        expression.setValue(evaluationContext, myList, "true");
        System.out.println(myList.list.get(0));
    }

示例中创建了一个包含只读的DataBindingSimpleEvaluationContext,然后利用这个SimpleEvaluationContext设置了根对象myList的属性。

在设置属性的时候存在类型转换,这里提供的是字符串类型的"true",实际类型则是Boolean,依然可以正常转换,因为SpEL默认使用Spring中的ConversionService进行类型转换。

关于ConversionService可以阅读从零开始 Spring Boot 29:类型转换 - 红茶的个人站点 (icexmoon.cn)

实际上这里setValue方法没有使用EvaluationContext作为参数,依然可以正常转换类型,因为SpelExpression缺省EvaluationContext的时候,会默认使用StandardEvaluationContext

public class SpelExpression implements Expression {
    public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException {
        this.ast.setValue(new ExpressionState(this.getEvaluationContext(), this.toTypedValue(rootObject), this.configuration), value);
    }
    
    public EvaluationContext getEvaluationContext() {
        if (this.evaluationContext == null) {
            this.evaluationContext = new StandardEvaluationContext();
        }

        return this.evaluationContext;
    }
    // ...
}

解析器配置

我们可以设置通过设置解析器配置来变更某些行为,比如下面这个示例:

MyList myList = new MyList();
SpelExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("list[2]");
expression.setValue(myList, true);
System.out.println(myList.list);

运行时候会报错:

spel.SpelEvaluationException: EL1025E: The collection has '0' elements, index '2' is invalid

错误信息很明确,myList.list是一个空列表,所以索引2是非法的。

可以通过SpelParserConfiguration配置SpEL解析器,让访问非法索引时自动填充空对象:

SpelParserConfiguration spelParserConfiguration = new SpelParserConfiguration(true, true);
MyList myList = new MyList();
SpelExpressionParser expressionParser = new SpelExpressionParser(spelParserConfiguration);
Expression expression = expressionParser.parseExpression("list[2]");
expression.setValue(myList, true);
System.out.println(myList.list);

输出:

[null, null, true]

构造器SpelParserConfiguration(true, true)的意思是,自动生成空对象,自动填充容器:

public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) {
    // ...
}

编译器

一般来说,不用担心SpEL的性能问题,但如果在程序中大量频繁地使用编译器,就可能导致性能问题。这时候可以使用SpEL编译器进行性能优化。

SpEL编译器可以在SpEL表达式运行时,将其编译成Class文件,这样就避免了同一个表达式重复被动态解析,从而实现了性能优化。

编译器有三种运行模式:

  • OFF (默认):编译器被关闭。
  • IMMEDIATE: 在即时模式下,表达式被尽快编译。这通常是在第一次解释的评估之后。如果编译的表达式失败(通常是由于类型改变,如前所述),表达式评估的调用者会收到一个异常。
  • MIXED: 在混合模式下,表达式随着时间的推移在解释模式和编译模式之间默默地切换。在经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(比如类型改变,如前所述),表达式会自动再次切换回解释形式。稍后的某个时候,它可能会生成另一个编译形式并切换到它。基本上,用户在 IMMEDIATE 模式下得到的异常反而被内部处理。

修改编译器模式有两种方式:代码方式和修改配置文件。

以代码方式修改编译器模式:

SpelParserConfiguration spelParserConfiguration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null);
SpelExpressionParser spelExpressionParser = new SpelExpressionParser(spelParserConfiguration);
Expression expression = spelExpressionParser.parseExpression("'hello'");
expression.getValue(String.class);

以配置文件方式,修改application.properties

spring.expression.compiler.mode=off

Spring的SpEL编译器存在一些缺陷,某些情况下不能被正常编译,具体可以参考核心技术 (springdoc.cn)

bean 定义中的表达式

可以通过SpEL表达式来构建bean定义,比如:

@RestController
@RequestMapping("/hello")
public class HelloController {
    @Value("#{ environment.getProperty('user.region') }")
    private String region;
    @Autowired
    private Environment environment;
    @Value("${user.region}")
    private String region2;

    @GetMapping("")
    public String hello() {
        System.out.println(this.region);
        String region = environment.getProperty("user.region");
        System.out.println(region);
        System.out.println(region2);
        return Result.success().toString();
    }
}

这里通过三种方式从application.properties配置文件获取配置项user.region:其中@Value("${...}")是很常见的直接获取配置项,environment.getProperty(...)则是通过注入的Environment对象来获取配置项,而@Value("#{...}")则是通过SpEL表达式获取environment这个bean,然后调用对应的getProperty方法获取。

  • SpEL表达式可以直接获取Spring中预设的bean,比如enviroment
  • 类似的,用XML方式定义bean的时候,同样可以使用SpEL。

除了可以利用SpEL定义bean的属性之外,还可以在用于创建bean的构造器(默认构造器或者@Autowired标记的构造器)的参数上使用:

@RestController
@RequestMapping("/hello")
public class HelloController {
    // ...
    private int randomNum;

    public HelloController(@Value("#{ T(java.lang.Math).random()*10+1 }") int randomNum) {
        this.randomNum = randomNum;
    }
	// ...
}

这里通过@Value注解,用一个SpEL表达式#{ T(java.lang.Math).random()*10+1 }为参数randomNumn指定了一个1~10之间的整形值。

这样就规避了因为Java语法上不支持参数默认值导致的无法为bean的构造器提供常规类型参数的问题。

除了构造器以外,其他用于注入的方法同样可以用SpEL表达式:

@RequestMapping("/hello")
public class HelloController {
    // ...
    private String author;

    public HelloController(@Value("#{ T(java.lang.Math).random()*10+1 }") int randomNum) {
        this.randomNum = randomNum;
    }

    @Autowired
    public void configure(HelloService helloService, @Value("#{ 'icexmoon' }") String author){
        this.helloService = helloService;
        this.author = author;
    }
	// ...
}

当然,这里实际上并不需要为通过使用SpEL表达式的方式初始化author属性,只是为了演示这种用法。

语言参考

作为一门完备的语言,SpEL表达式本身就很复杂,但通常使用并不需要熟悉所有的语法,如果有遇到没有掌握的语法,可以查阅以下的官方文档:

The End,谢谢阅读。

本文的所有示例可以从ch31/spel · 魔芋红茶/learn_spring_boot - 码云 - 开源中国 (gitee.com)获取。

参考资料

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。