Spring 笔记——核心-SpEL 篇

SpEL

全称:Spring Expression Language (Spring 表达式语言)
定义:SpEL 是 Spring 定义的一套在 Spring 框架内运行的表达式语言,说是语言,理解为通过特定格式的字符串来让 Spring 框架解析出原来的含义,可简化很多对数据的操作动作。后端类似的有 OGNL, MVELJBoss EL。前端方面
官网地址
ThymeleafFreeMarker 的数据渲染语法也可以理解为一种表达式语言。

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 还有两种实现 MethodBasedEvaluationContextCacheEvaluationContext

类型转换

默认情况下: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();
// 设置值字符串 "false" ,发现需要的是 Boolean 类型的,就进行了类型转换变为布尔类型 false
// will recognize that it needs to be a Boolean and convert it accordingly.
ExpressionParser parser = new SpelExpressionParser();
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b is false
Boolean b = simple.booleanList.get(0);
}

解析器配置

可以使用 org.springframework.expression.spel.SpelParserConfiguration 来配置 SpEL 的数据解析器。如下案例

@Test
public void spelParserConfigurationTest() {
class Demo {
public List<String> list;
}
// Turn on:
// - 会自动调用默认初始化方法对相应的 null 进行初始化
// - 集合大小自增
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
ExpressionParser parser = new SpelExpressionParser(config);
Expression expression = parser.parseExpression("list[8]");
Demo demo = new Demo();
// 此时 demo.list == null
Object o = expression.getValue(demo);
// 此时 demo.list = ["","","",""]
}

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();
// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
// evals to 2147483647
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);
// json语法首字母小写
String city1 = (String) parser.parseExpression("placeOfBirth.city").getValue(context);
// json语法首字母大写
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();
// Inventions Array
// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String.class);
// Members List
// evaluates to "Nikola Tesla"
String name = parser.parseExpression("members[0].name").getValue(
context, ieee, String.class);
// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("members[0].inventions[6]").getValue(
context, ieee, String.class);

通过在括号内指定文字键值来获取映射(map)的内容。在下面的示例中,因为officers映射的键是字符串,我们可以指定字符串文字:

// Officer's Dictionary
Inventor pupin = parser.parseExpression("officers['president']").getValue(
societyContext, Inventor.class);
// evaluates to "Idvor"
String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
societyContext, String.class);
// setting values
parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue(
societyContext, "Croatia");

构建列表(Inline Lists)

{}表示一个空序列

// evaluates to a Java list containing the four numbers
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 的值包含英文点号 . ,不然书写时是不需要英文单引号 ' 括起来的,当然,你要给他加上也是没问题的。

// evaluates to a Java map containing the two entries
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);
// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

方法(Methods)

表达式通过java语法调用方法

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
// evaluates to true
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 遵循基础正则语法

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression(
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression(
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression(
"'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
  • 逻辑运算符

and (&&)or (||)not (!)

// -- AND --
// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- OR --
// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- NOT --
// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
  • 数学运算符

+ 可以对数字与字符串使用,-,*,/,取余运算符%,指数幂运算符^只可以对数字使用

// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2
String testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String.class); // 'test string'
// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4
double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000
// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0
// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0
// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3
int one1 = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1
// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21
  • 赋值运算符

= 赋值运算符号可以在调用 getValue 方法的时候触发

Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
// 直接调用 getValue 方法赋值
parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic");
// 调用 getValue 方法,通过触发赋值运算符 `=` 的方式赋值
// alternatively
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);
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);

变量(Variables)

在表达式中使用 #variableName 来表明一个变量.变量的实际赋值是通过EvaluationContextsetVariable()方法来赋值的.

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()) // "Mike Tesla"

#this#root变量

#this 变量始终被定义并引用当前的评估对象(针对哪些不合格的引用被解析)。#root变量始终被定义并引用根上下文对象。尽管#this评估表达式的组件时可能会有所不同,但 #root始终指的是根。以下示例显示了如何使用 #this 和 #root`变量:

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(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.forReadOnlyDataAccess();
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);

函数(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());
// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@testBean").getValue(context);
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
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);
// queryResultString = "Nikola Tesla is not a member of the IEEE Society"

猫王运算符 (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); // Nikola Tesla
tesla.setName(null);
// "name?:'Elvis Presley'" 等价于 "name!=null?name:'Elvis Presley'"
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis Presley

安全操作运算符 (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); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!

集合选择(Collection Selection)

.? 组合符号
.?[selectionExpression] 取数据选择条件表达式(selectionExpression )的所有元素
.[1] 取数据选择条件表达式(selectionExpression )的第一个元素
.$[selectionExpression] 取数据选择条件表达式(selectionExpression )的最后一个元素

// 在 `societyContext` 中的 `society实例` 的 `members` 属性上进行筛选,筛选出 `nationality == 'Serbian'` 的 `inventor实例`
List<Inventor> list = (List<Inventor>) parser.parseExpression("members.?[nationality == 'Serbian']").getValue(societyContext);
// 数组和任何实现了 `java.lang.Iterable` 或 `java.util.Map` 的对象实例都支持获取 `map` 对象的语法
// 从 `map` 里面筛选 `value 小于27` 的 `key-value` 构建一个 `newMap ` 返回
Map newMap = parser.parseExpression("map.?[value<27]").getValue();

集合映射 (Collection Projection)

目前只能由 List

posted @   临渊不羡渔  阅读(2745)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示