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

news2024/11/17 22:19:41

从零开始 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表达式本身就很复杂,但通常使用并不需要熟悉所有的语法,如果有遇到没有掌握的语法,可以查阅以下的官方文档:

  • SpEL-语言参考。

The End,谢谢阅读。

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

参考资料

  • 核心技术 (springdoc.cn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/543767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024王道数据结构考研丨第一章:绪论

2024王道数据结构考研笔记专栏将持续更新&#xff0c;欢迎 点此 收藏&#xff0c;共同交流学习… 文章目录 第一章&#xff1a;绪论1.1数据结构的基本概念1.2数据结构的三要素1.3算法的基本概念1.4算法的时间复杂度1.5算法的空间复杂度 第一章&#xff1a;绪论 1.1数据结构的基…

Vue - vxe-table 表格合并行应用

Vue - vxe-table 表格合并行应用 一. 将相同的列数据合并为一行实现效果实现方法 二. 拓展合并&#xff1a;根据某个字段合并后的数据 进行合并其他字段列实现效果实现方法 vxe-table 地址&#xff1a;https://vxetable.cn/v2/#/table/start/install 一. 将相同的列数据合并为一…

亚马逊云科技作为中国出海力量之一,为中国企业提供技术桥梁

这是一个真实的故事&#xff1a;一家出海企业的项目交付需要在非洲吉布提部署上云&#xff0c;企业负责人在地图上找了半天才找到吉布提&#xff0c;而亚马逊云科技仅用了3天的时间就为企业在当地的业务开展&#xff0c;交付了IT基础设施。对于出海企业来说&#xff0c;这种效率…

前端学习--Vue(2)常见指令

一、Vue简介 1.1 概念 Vue是一套用于构建用户界面的前端框架 框架&#xff1a;现成解决方案&#xff0c;遵守规范去编写业务功能 指令、组件、路由、Vuex、vue组件库 1.2 特性 数据驱动视图 vue连接页面结构和数据&#xff0c;监听数据变化&#xff0c;自动渲染页面结构…

【遗传算法】【处理图像类问题】

文章目录 一、前言二、问题描述三、算法介绍四、其他知识点Reference 一、前言 近期感兴趣的算法&#xff0c;以前没这么好奇过一个算法。时间没想象的焦虑&#xff0c;认真做一些事情算法入门篇 二、问题描述 从前&#xff0c;一群扇贝在海岸边悠哉游哉地生活着。它们衣食不…

亚马逊云科技宣布推出一个新的开源示例应用程序

5月5日&#xff0c;亚马逊云科技宣布推出一个新的开源示例应用程序&#xff0c;这是一个虚构的二手书电子商务商店&#xff0c;被称之为Bob’s Used Books&#xff0c;可供使用亚马逊云科技的.NET开发人员使用。“亚马逊云科技的.NET宣传和开发团队定期与客户交谈&#xff0c;在…

如何科学地利用高光谱图像合成真彩色RGB图像?

如何科学地利用高光谱图像合成真彩色RGB图像? 1. 前言 参考链接: 色匹配函数是什么&#xff1f; - 知乎 (zhihu.com) 23. 颜色知识1-人类的视觉系统与颜色 - 知乎 (zhihu.com) 色彩空间基础 - 知乎 (zhihu.com) 色彩空间表示与转换 - 知乎 (zhihu.com) CIE XYZ - fresh…

Golang笔记:使用melody包进行WebSocket通讯

文章目录 目的使用示例与说明总结 目的 WebSocket是Web开发应用中非常常用的功能&#xff0c;用于客户端和服务器间长时间的实时双向数据通讯。Golang中官方并没有实现这个功能&#xff0c;需要借助第三方的包来实现。 目前被最广泛使用的包是 gorilla/websocket https://pkg…

Host头攻击

转载与&#xff1a;https://blog.csdn.net/weixin_47723270/article/details/129472716 01 HOST头部攻击漏洞知识 Host首部字段是HTTP/1.1新增的&#xff0c;旨在告诉服务器&#xff0c;客户端请求的主机名和端口号&#xff0c;主要用来实现虚拟主机技术。 运用虚拟主机技术&a…

第八章:C语言的简单指针

谈起指针&#xff0c;简直就是谈虎色变&#xff0c;学习C语言的人都知道&#xff0c;指针的难度&#xff0c;就好像高中的导数一样&#xff0c;难道离谱&#xff0c;但是但是&#xff0c;别慌呀&#xff0c;咱们慢慢来&#xff0c;空杯心态&#xff0c;一步一个脚印&#xff0c…

【STM32】STM32使用继电器

STM32使用继电器 其实继电器简单来说就是一个开关&#xff0c;VCC表示电源正极、GND表示电源负极、IN表示信号输入脚&#xff0c;COM表示公共端&#xff0c;NC&#xff08;normal close&#xff09;表示常闭端&#xff0c;NO(normal open)表示常开端。一般情况下是常闭状态。 …

为数字人充值AI情商 小冰“克隆人”要卖给谁?

近日&#xff0c;小冰公司启动“GPT克隆人计划”&#xff0c;据悉最短只要采集三分钟数据&#xff0c;就能帮助用户创造源于本人性格、技能、声音、外貌的AI克隆人&#xff0c;如同拥有“平行世界的第二人生”。 这不免让人想起了《流浪地球2》里华仔为剧中女儿“数字续命”的…

chatgpt赋能Python-python3_2__1

Python3-2<<1&#xff1a; 了解运算符的使用和优先级 Python是一种优雅而高效的编程语言&#xff0c;而Python3-2<<1是一个关于运算符优先级的例子&#xff0c;值得我们深入探讨。 在这篇文章中&#xff0c;我们将介绍Python3中运算符的优先级&#xff0c;并对其中…

chatgpt赋能Python-python3_6怎么用

Python3.6是什么&#xff1f; Python是一种非常流行的编程语言&#xff0c;旨在提供简洁、易读且易于维护的代码。Python3.6是该语言的下一个主要版本&#xff0c;带来了一些改进并改进了现有功能&#xff0c; Python3.6有哪些新特性&#xff1f; 字典内置方法&#xff0c;支…

chatgpt赋能Python-python3_53怎么安装

Python3.5.3安装方法及注意事项 Python是一种高级编程语言&#xff0c;被广泛应用于科学&#xff0c;数学&#xff0c;机器学习等领域。在本文中&#xff0c;我们将介绍如何安装Python 3.5.3版本&#xff0c;并提供注意事项以确保安装过程顺利进行。 步骤1&#xff1a;下载Py…

生态碳汇涡度通量数据分析

生态碳汇涡度相关监测与通量数据分析 朱老师&#xff08;副教授&#xff09;&#xff1a;来自国内重点高校&#xff0c;长期从事涡度通量观测与分析研究&#xff0c;发表SCI论文多篇&#xff0c;主持国家与地方科研项目多个&#xff0c;在生态环境数据处理与分析中具有丰富的实…

java企业车辆管理系统myeclipse定制开发mysql数据库网页模式java编程jdbc

一、源码特点 java企业车辆管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助 mysql数据库&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 java企业车辆管理系统myeclipse定制开发mysql 二、功能介绍 此次系统…

navicat连接mysql数据库密码忘记了快速找回

本机环境&#xff1a; mac 12.2.1 mysql8.0.3 问题&#xff1a; 连接navicat时&#xff0c;报错Access denied for user ‘root‘‘localhost‘ (using password: YES) 解决&#xff1a; 1.在navicat中导出xxx.ncx文件 勾选Export Password&#xff0c;选择要输出的文件目录…

nvidia-smi 参数详解

nvidia-smi 参数详解 参数详情GPU本机中的GPU编号&#xff0c;有多块显卡的时候从0开始&#xff0c;图中的GPU编号为0Fan风扇转速&#xff0c;N/A表示没有风扇NameGPU类型&#xff0c;图中GPU为NVIDIA GeForce GTX 1050TiTempGPU温度PerfGPU性能状态&#xff0c;从P0(最大性能)…

视频怎么转化为mp3,5种高效方法任选

视频怎么转化为mp3呢&#xff1f;想必这是我们工作过程中经常遇见的问题。众所周知&#xff0c;MP3格式是一种常见的音频格式&#xff0c;支持多种音频播放器和设备。通过将视频转换为MP3格式&#xff0c;用户可以方便地将视频的音频部分提取出来&#xff0c;保存为与视频大小不…