SpEL
全称:Spring Expression Language (Spring 表达式语言)
定义:SpEL 是 Spring
定义的一套在 Spring
框架内运行的表达式语言,说是语言,理解为通过特定格式的字符串来让 Spring
框架解析出原来的含义,可简化很多对数据的操作动作。后端类似的有 OGNL
, MVEL
和 JBoss EL
。前端方面
官网地址
Thymeleaf
,FreeMarker
的数据渲染语法也可以理解为一种表达式语言。
SpEL 大致功能
- 简单字符
- boolean值 与关系运算符支持
- 常用表达式
- 类表达式
- 访问 properties, arrays, lists, maps
- 方法调用
- 关系运算符支持
- 任务
- 调用构造函数
- Bean 引用
- 构造数组
- 单行配置 list
- 单号配置 map
- 三元运算符
- 变量
- 用户自定义函数
- Collection projection
- Collection selection
- 模板表达式
简单案例
基础字符串语法解析
解析字符串声明语句 'Hello World'
得到字符串 Hello World
解析字符串拼接语句 'Hello World'.concat('!')
得到字符串 Hello World!
| ExpressionParser parser = new SpelExpressionParser(); |
| Expression exp = parser.parseExpression("'Hello World'"); |
| String message = (String) exp.getValue(); |
| assert message.equals("Hello World"); |
| exp = parser.parseExpression("'Hello World'.concat('!')"); |
| message = (String) exp.getValue(); |
| assert message.equals("Hello World!"); |
| exp = parser.parseExpression("'Hello World'.bytes.length"); |
| assert exp.getValue().equals(11); |
EvaluationContext
接口
相关实现
当 Spring
处理 SpEL
的时候会通过这个接口解析属性,方法或字段。Spring
为该接口提供了两个实现:
SimpleEvaluationContext
简单实现:旨在仅支持 SpEL
语言语法的一个子集。它不包括 Java 类型引用
、构造函数和 bean 引用
。使用它明需要确选择对表达式中的属性和方法的支持级别。默认情况下,create()
静态工厂方法只允许对属性进行读取访问。可以通过获得构建器来配置所需的确切支持级别,针对以下一项或某种组合:
- 唯一的自定义
属性访问器
- 只读数据绑定
- 允许读写的数据绑定
StandardEvaluationContext
标准实现:支持所有的 SpEL
语言特性和配置选项。可以使用它来指定默认根对象并配置所有的的 evaluation-relate
策略。基于 StandardEvaluationContext
还有两种实现 MethodBasedEvaluationContext
、CacheEvaluationContext
。
类型转换
默认情况下:Spring
解析 SpEL
的时候会使用自身 org.springframework.core.convert.ConversionService
包中可用的转换器服务。而且由于 SpEL
是能泛型感知的,当解析数据时,会尝试将其以正确的泛型来进行解析,下面是一个字符串 "false"
解析成布尔值 false
的简单案例
| @Test |
| public void simpleEvaluationContextTest() { |
| class Simple { |
| public List<Boolean> booleanList = new ArrayList<>(); |
| } |
| |
| Simple simple = new Simple(); |
| simple.booleanList.add(true); |
| |
| EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); |
| |
| |
| |
| ExpressionParser parser = new SpelExpressionParser(); |
| parser.parseExpression("booleanList[0]").setValue(context, simple, "false"); |
| |
| |
| Boolean b = simple.booleanList.get(0); |
| } |
解析器配置
可以使用 org.springframework.expression.spel.SpelParserConfiguration
来配置 SpEL
的数据解析器。如下案例
| @Test |
| public void spelParserConfigurationTest() { |
| class Demo { |
| public List<String> list; |
| } |
| |
| |
| |
| |
| SpelParserConfiguration config = new SpelParserConfiguration(true, true); |
| |
| ExpressionParser parser = new SpelExpressionParser(config); |
| |
| Expression expression = parser.parseExpression("list[8]"); |
| |
| Demo demo = new Demo(); |
| |
| Object o = expression.getValue(demo); |
| |
| } |
SpEL 编译器
性能增强
SpEL
表达式通常使用默认的基础表达式解释器处理。但对于 Spring Integration
这种对于性能要求高的组件来说,性能是个很重要的指标。而对于一些非动态的赋值表达式语言,Spring
提供了一个编译器扩展处理该类表达式。
实现流程:在评估期间,编译器生成一个体现表达式运行时行为的 Java
类,并使用该类来实现更快的表达式评估。由于没有围绕表达式键入,编译器在执行编译时使用在表达式的解释评估期间收集的信息。
如下测试:在 50000 次迭代测试中,解释器需要 75 毫秒,而编译器方案只需要 3 毫秒。
| someArray[0].someProperty.someOtherProperty < 0.1 |
使用案例:
SpelParserConfiguration
配置 org.springframework.expression.spel.SpelCompilerMode
枚举值以选择不同的模式运行
OFF
(默认):编译器关闭
IMMEDIATE
:编译器开启,编译失败则调用者会收到异常
MIXD
:混合模式,表达式会随着时间在解释模式和编译模式之间静默切换。在经过一定次数的解释运行后,它们会切换到编译形式,如果编译形式出现问题(例如类型更改,如前所述),表达式会自动再次切换回解释形式。一段时间后,它可能会生成另一个已编译的表单并切换到它。基本上,用户进入IMMEDIATE模式的异常是在内部处理的
| @Test |
| public void spelCompilerConfigurationTest() { |
| |
| class MyMessage { |
| public String payload="Hello world"; |
| } |
| |
| SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, |
| this.getClass().getClassLoader()); |
| SpelExpressionParser parser = new SpelExpressionParser(config); |
| Expression expr = parser.parseExpression("payload"); |
| MyMessage message = new MyMessage(); |
| Object payload = expr.getValue(message); |
| } |
使用限制
一下情形无法使用 Spel Compilation
- 涉及赋值的表达式
- 依赖于转换服务的表达式
- 使用自定义解析器或访问器的表达式
- Expressions using selection or projection
Bean 定义中的表达式配置
注解方式
注解作用与字段上
| 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 MovieRecommender { |
| |
| private String defaultLocale; |
| |
| private CustomerPreferenceDao customerPreferenceDao; |
| |
| public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, |
| @Value("#{systemProperties['user.country']}") String defaultLocale) { |
| this.customerPreferenceDao = customerPreferenceDao; |
| this.defaultLocale = 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; |
| } |
| |
| |
| } |
xml 配置方式
| <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> |
常用案例
基础字符串语法表达式
可配置 字符串、数值(int、real、hex)、boolean 和 null。字符串通过英文单引号括起来声明,两个单引号表示一个单引号字符值。数字支持使用负号、指数表示法和小数点。默认情况下,实数使用Double.parseDouble()。
| @Test |
| public void BasicStringTest() { |
| |
| ExpressionParser parser = new SpelExpressionParser(); |
| |
| |
| String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); |
| |
| double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue(); |
| |
| |
| int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); |
| |
| boolean trueValue = (Boolean) parser.parseExpression("true").getValue(); |
| |
| Object nullValue = parser.parseExpression("null").getValue(); |
| } |
Properties, Arrays, Lists, Maps, and Indexers
获取实体属性、数组、list、map的内容值
通过json语法
获取实体属性第一个字母不区分大小写。也可以通过实体方法获取实体属性
| |
| int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context); |
| |
| String city1 = (String) parser.parseExpression("placeOfBirth.city").getValue(context); |
| |
| String city2 = (String) parser.parseExpression("placeOfBirth.city").getValue(context); |
| |
| String city3 = (String) parser.parseExpression("getPlaceOfBirth().getCity()").getValue(context); |
向量
(arrsy)和序列
(list)可以通过一下方式获取
| ExpressionParser parser = new SpelExpressionParser(); |
| EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); |
| |
| |
| |
| |
| String invention = parser.parseExpression("inventions[3]").getValue( |
| context, tesla, String.class); |
| |
| |
| |
| |
| String name = parser.parseExpression("members[0].name").getValue( |
| context, ieee, String.class); |
| |
| |
| |
| String invention = parser.parseExpression("members[0].inventions[6]").getValue( |
| context, ieee, String.class); |
通过在括号内指定文字键值来获取映射
(map)的内容。在下面的示例中,因为officers映射的键是字符串,我们可以指定字符串文字:
| |
| |
| Inventor pupin = parser.parseExpression("officers['president']").getValue( |
| societyContext, Inventor.class); |
| |
| |
| String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( |
| societyContext, String.class); |
| |
| |
| parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( |
| societyContext, "Croatia"); |
构建列表(Inline Lists)
{}
表示一个空序列
| |
| List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context); |
| |
| List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context); |
构建映射(Inline Maps)
{key:value}
表示一个简单的键值对关系,{:}
表示一个内容为空的 map。需要注意的是,除非 key
的值包含英文点号 .
,不然书写时是不需要英文单引号 '
括起来的,当然,你要给他加上也是没问题的。
| |
| Map inventorInfo = (Map) parser.parseExpression("{'name':'Nikola',dob:'10-July-1856'}").getValue(context); |
| |
| Map mapOfMaps = (Map) parser.parseExpression("{'na.me':{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); |
数组构造 (Array Construction)
| int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); |
| |
| |
| int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context); |
| |
| |
| int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context); |
方法(Methods)
表达式通过java语法
调用方法
| |
| String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class); |
| |
| |
| boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue( |
| societyContext, Boolean.class); |
运算(Operators)
lt (<)
、gt (>)
、le (<=)
、ge (>=)
、eq (==)
、ne (!=)
、div (/)
、mod (%)
、not (!)
进行大小比较时,null
比任意数据都小
instanceof
使用是会自动进行类型包装,造成结果是: true = instanceof T(Integer)
false = 1 instanceof T(int)
matches
遵循基础正则语法
| |
| boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class); |
| |
| |
| boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class); |
| |
| |
| boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class); |
| |
| |
| boolean falseValue = parser.parseExpression( |
| "'xyz' instanceof T(Integer)").getValue(Boolean.class); |
| |
| |
| boolean trueValue = parser.parseExpression( |
| "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); |
| |
| |
| boolean falseValue = parser.parseExpression( |
| "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); |
and (&&)
、or (||)
、not (!)
| |
| |
| |
| boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class); |
| |
| |
| String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"; |
| boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); |
| |
| |
| |
| |
| boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class); |
| |
| |
| String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')"; |
| boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); |
| |
| |
| |
| |
| boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class); |
| |
| |
| String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"; |
| boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); |
+
可以对数字与字符串使用,-
,*
,/
,取余运算符%
,指数幂运算符^
只可以对数字使用
| |
| int two = parser.parseExpression("1 + 1").getValue(Integer.class); |
| |
| String testString = parser.parseExpression( |
| "'test' + ' ' + 'string'").getValue(String.class); |
| |
| |
| int four = parser.parseExpression("1 - -3").getValue(Integer.class); |
| |
| double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); |
| |
| |
| int six = parser.parseExpression("-2 * -3").getValue(Integer.class); |
| |
| double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); |
| |
| |
| int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); |
| |
| double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); |
| |
| |
| int three = parser.parseExpression("7 % 4").getValue(Integer.class); |
| |
| int one1 = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); |
| |
| |
| int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); |
=
赋值运算符号可以在调用 getValue
方法的时候触发
| Inventor inventor = new Inventor(); |
| EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); |
| |
| parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic"); |
| |
| |
| String aleks = parser.parseExpression( |
| "name = 'Aleksandar Seovic'").getValue(context, inventor, String.class); |
(Types)
SpEL
中可通过特殊运算符 T
来指定 class实例
,该运算符也可以调用静态方法
StandardEvaluationContext
通过 TypeLocator
来查找 class实例
. 同时由于 StandardTypeLocator
实在java.lang
包下面的,所以查找这个包下面的 类实例无需写全名字,例如 java.lang.String
只需要 String
即可.
| Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); |
| |
| Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); |
| |
| boolean trueValue = parser.parseExpression( |
| "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR") |
| .getValue(Boolean.class); |
构造函数(Constructors)
构造函数的调用也是出了java.lang
包下面的类,别的类的构造函数调用都需要使用全量限定类名.
| Inventor einstein = p.parseExpression( |
| "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") |
| .getValue(Inventor.class); |
| |
| |
| p.parseExpression( |
| "Members.add(new org.spring.samples.spel.inventor.Inventor( |
| 'Albert Einstein', 'German'))").getValue(societyContext); |
变量(Variables)
在表达式中使用 #variableName
来表明一个变量.变量的实际赋值是通过EvaluationContext
的setVariable()
方法来赋值的.
| Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); |
| |
| EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); |
| context.setVariable("newName", "Mike Tesla"); |
| |
| parser.parseExpression("name = #newName").getValue(context, tesla); |
| System.out.println(tesla.getName()) |
#this
和#root
变量
#this
变量始终被定义并引用当前的评估对象(针对哪些不合格的引用被解析)。#root
变量始终被定义并引用根上下文对象。尽管#this评估表达式的组件时可能会有所不同,但 #root
始终指的是根。以下示例显示了如何使用 #this 和
#root`变量:
| |
| List<Integer> primes = new ArrayList<Integer>(); |
| primes.addAll(Arrays.asList(2,3,5,7,11,13,17)); |
| |
| |
| ExpressionParser parser = new SpelExpressionParser(); |
| EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess(); |
| context.setVariable("primes", primes); |
| |
| |
| |
| List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression( |
| "#primes.?[#this>10]").getValue(context); |
函数(Functions)
自定义函数注册到 EvaluationContext
后,就可以在 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(); |
| } |
| } |
注册并调用自定义函数
| ExpressionParser parser = new SpelExpressionParser(); |
| |
| EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); |
| context.setVariable("reverseString", |
| StringUtils.class.getDeclaredMethod("reverseString", String.class)); |
| |
| String helloWorldReversed = parser.parseExpression( |
| "#reverseString('hello')").getValue(context, String.class); |
Bean 引用(Bean References)
通过 @
符号指定 Bean
名称,然后从自定义的 Bean Resolver
获取 Bean
通过 $
符号指定的bean
名称包含$
.
| class MyBeanResolver implements BeanResolver { |
| @Override |
| public Object resolve(EvaluationContext context, String beanName) throws AccessException { |
| if (beanName.equals("testBean")) |
| return new int[]{1, 2}; |
| return null; |
| } |
| } |
| ExpressionParser parser = new SpelExpressionParser(); |
| StandardEvaluationContext context = new StandardEvaluationContext(); |
| context.setBeanResolver(new MyBeanResolver()); |
| |
| |
| Object bean = parser.parseExpression("@testBean").getValue(context); |
| |
| Object factoryBean = parser.parseExpression("&foo").getValue(context); |
三元运算符 (Ternary Operator (If-Then-Else))
| String falseString = parser.parseExpression( |
| "false ? 'trueExp' : 'falseExp'").getValue(String.class); |
| ExpressionParser parser = new SpelExpressionParser(); |
| String falseString = parser.parseExpression( |
| "false ? 'trueExp' : 'falseExp'").getValue(String.class); |
| |
| StandardEvaluationContext context = new StandardEvaluationContext(); |
| Society tesla = new Society(); |
| context.setRootObject(tesla); |
| parser.parseExpression("name").setValue(context, "IEEE"); |
| context.setVariable("queryName", "Nikola Tesla"); |
| |
| String expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + |
| "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; |
| |
| String queryResultString = parser.parseExpression(expression) |
| .getValue(context, tesla, String.class); |
| |
猫王运算符 (The Elvis Operator)
其实就是三木运算符一种情形下的简写, "name?:'Elvis Presley'"
等价于 "name!=null?name:'Elvis Presley'"
这种语法 Kotlin
已经支持了
| ExpressionParser parser = new SpelExpressionParser(); |
| EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); |
| |
| Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); |
| String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); |
| System.out.println(name); |
| |
| tesla.setName(null); |
| |
| name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); |
| System.out.println(name); |
安全操作运算符 (Safe Navigation Operator)
?
,这个符号在前端 FreeMarket语法中也有
在避免空指针异常的时候,不用每次都进行判空处理
| ExpressionParser parser = new SpelExpressionParser(); |
| EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); |
| |
| Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); |
| tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); |
| |
| String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); |
| System.out.println(city); |
| |
| tesla.setPlaceOfBirth(null); |
| city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); |
| System.out.println(city); |
集合选择(Collection Selection)
.?
组合符号
.?[selectionExpression] 取数据选择条件表达式(selectionExpression )的所有元素
. 取数据选择条件表达式(selectionExpression )的第一个元素
.$[selectionExpression] 取数据选择条件表达式(selectionExpression )的最后一个元素
| |
| List<Inventor> list = (List<Inventor>) parser.parseExpression("members.?[nationality == 'Serbian']").getValue(societyContext); |
| |
| |
| Map newMap = parser.parseExpression("map.?[value<27]").getValue(); |
集合映射 (Collection Projection)
目前只能由 List
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)