1 SpEL简介
SpEL(Spring Expression Language)是一种用于在Spring框架中进行数据绑定和执行业务逻辑的表达式语言。Spring EL提供了一种简洁、灵活和强大的方式来访问对象的属性、调用方法、执行运算和逻辑判断等操作。
官方文档:https://docs.spring.io/spring-framework/reference/core/expressions.html
2 基本使用
2.1 通过@Value注解使用
可以将@Value注解加在字段,方法,方法参数,构造方法参数上使用,${…}表示直接读取上下文的属性值,而#{…}就表示使用SpEL进行运算。
字段示例:
public class FieldValueTestBean {@Value("#{systemProperties['user.region'] }")private String defaultLocale;private String defaultTimezone;public void setDefaultLocale(String defaultLocale) {this.defaultLocale = 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;}
}
@Autowired方法示例:
public class SimpleMovieLister {private MovieFinder movieFinder;private String defaultLocale;@Autowiredpublic 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;}// ...
}
2.2 编程方式执行SpEL
2.2.1 基本使用
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue(); // "Hello World"
2.2.2 设置根对象
Inventor tesla = new Inventor();
tesla.setName("Nikola Tesla");ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla); // "Nikola Tesla"
2.2.3 设置上下文类型
以上的例子中,会创建一个上下文(EvaluationContext),默认实现类为StandardEvaluationContext
上下文接口有两种实现:
StandardEvaluationContext
完整的上下文功能SimpleEvaluationContext
精简版的上下文,去除了Java类型参照、构造器、Bean参照等功能
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();Inventor tesla = new Inventor();
tesla.setName("Nikola Tesla");ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(context, tesla); // "Nikola Tesla"
2.2.4 SpEL配置
可通过SpelParserConfiguration的构造方法,设置SpEL相关的配置, 例如:
class Demo {public List<String> list;
}// 开启空对象自动初始化,开启集合自动增长
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
ExpressionParser parser = new SpelExpressionParser(config);ExpressionParser parser = new SpelExpressionParser(config);
Expression expression = parser.parseExpression("list[3]");
Demo demo = new Demo();
Object o = expression.getValue(demo); // 将不会报空指针异常,list会自动初始化, 并且将包含4个元素, 每个元素为空字符串
可以配置的功能有:
autoGrowNullReferences
开启自动生成对象,默认 falseautoGrowCollections
开启集合自动增长,默认 falsemaximumAutoGrowSize
集合增长最大长度,默认 Integer.MAX_VALUEmaximumExpressionLength
表达式最大长度,默认 1000compilerMode
开启预编译, 默认 OFF
2.2.4 编译SpEL
SpEL编译功能默认是关闭的,如果对性能有要求,那么可以开启预编译功能,
SpelCompilerMode.OFF
默认关闭SpelCompilerMode.IMMEDIATE
立即编译,在第一次使用时就会编译SpelCompilerMode.MIXED
混合编译,再使用几次后才会尝试编译,如果编译出错,就会用上次的编译成功的结果
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);
3 表达式用法
3.1 字面量
String
使用单引号包裹Number
使用int、long、float、double表示Boolean
true/falseNull
null
xpressionParser parser = new SpelExpressionParser();// evaluates to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();// evaluates to "Tony's Pizza"
String pizzaParlor = (String) parser.parseExpression("'Tony''s Pizza'").getValue();double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();// evaluates to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();boolean trueValue = (Boolean) parser.parseExpression("true").getValue();Object nullValue = parser.parseExpression("null").getValue();
3.2 属性、数组、列表、字典、索引
3.2.1 直接访问
通过对象.属性即可快速访问属性
// evaluates to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);
属性访问对首字母大小写是不敏感的,所以上面的表达式也可以写成"Birthdate.Year + 1900",“PlaceOfBirth.City”。
另外可以通过getter来访问,比如 “getPlaceOfBirth().getCity()”
通过中括号加数字来访问数组、列表的指定的元素
xpressionParser 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);
通过中括号加键名访问字典的元素
// Officer's DictionaryInventor 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");
3.2.2 集合、字典过滤
使用.?[选择表达式]
可以方便的对数组、集合、字典进行过滤,类似于Java Stream的filter方法
List<Inventor> list = (List<Inventor>) parser.parseExpression("members.?[nationality == 'Serbian']").getValue(societyContext);Map newMap = parser.parseExpression("#map.?[value < 27]").getValue(Map.class);
.^[选择表达式]
表示只取第一个,.$[选择表达式]
表示只取最后一个
Inventor first = (Inventor) parser.parseExpression("members.^[nationality == 'Serbian']").getValue(societyContext);Inventor last= (Inventor) parser.parseExpression("members.$[nationality == 'Serbian']").getValue(societyContext);
3.2.3 集合投影
使用.![投影表达式]
可以方便的对数组、集合、字典进行转换成别的集合,类似于Java Stream的map方法
// evaluates to ["Smiljan", "Idvor"]
List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]").getValue(societyContext, List.class);
3.3 定义列表、字典、数组
定义列表
// 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);
定义字典
// 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("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
定义数组
//
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);
初始化数组的表达式是无法预编译的
3.4 执行方法
和调用Java的方法一样,在SpEL中也是可以直接调用对象的方法的
// 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);
3.5 运算符
3.5.1 关系运算
// 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 true
boolean falseValue = parser.parseExpression("'abc' > null").getValue(Boolean.class);// uses CustomValue:::compareTo
boolean trueValue = parser.parseExpression("new CustomValue(2) > new CustomValue(1)").getValue(Boolean.class);
除了用符号外,还可以用文本代替:
lt
(<)gt
(>)le
(<=)ge
(>=)eq
(==)ne
(!=)
除了以上的关系运算,还支持between
, instanceof
, matches
关系运算
boolean result;// evaluates to true
result = parser.parseExpression("1 between {1, 5}").getValue(Boolean.class);// evaluates to true
result = parser.parseExpression("'elephant' between {'aardvark', 'zebra'}").getValue(Boolean.class);// evaluates to true
result = parser.parseExpression("123 instanceof T(Integer)").getValue(Boolean.class);// evaluates to true
result = parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
3.5.2 逻辑运算
SpEL 支持一下逻辑运算
- 与 (
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);
3.5.3 字符串操作
+ 号表示字符串拼接,- 号表示单字符的ASCII码减去后的新值, * 号表示重复
// -- Concatenation --// evaluates to "hello world"
String helloWorld = parser.parseExpression("'hello' + ' ' + 'world'").getValue(String.class);// -- Character Subtraction --// evaluates to 'a'
char ch = parser.parseExpression("'d' - 3").getValue(char.class);// -- Repeat --// evaluates to "abcabc"
String repeated = parser.parseExpression("'abc' * 2").getValue(String.class);
3.5.4 数学运算
SpEL中支持以下数学运算
- 加 (
+
) - 减(
-
) - 自增 (
++
) - 自减 (
--
) - 乘 (
*
) - 除 (
/
,div
) - 取模 (
%
,mod
) - 指数幂 (
^
)
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();// -- Addition --int two = parser.parseExpression("1 + 1").getValue(int.class); // 2// -- Subtraction --int four = parser.parseExpression("1 - -3").getValue(int.class); // 4double d = parser.parseExpression("1000.00 - 1e4").getValue(double.class); // -9000// -- Increment --// The counter property in Inventor has an initial value of 0.// evaluates to 2; counter is now 1
two = parser.parseExpression("counter++ + 2").getValue(context, inventor, int.class);// evaluates to 5; counter is now 2
int five = parser.parseExpression("3 + ++counter").getValue(context, inventor, int.class);// -- Decrement --// The counter property in Inventor has a value of 2.// evaluates to 6; counter is now 1
int six = parser.parseExpression("counter-- + 4").getValue(context, inventor, int.class);// evaluates to 5; counter is now 0
five = parser.parseExpression("5 + --counter").getValue(context, inventor, int.class);// -- Multiplication --six = parser.parseExpression("-2 * -3").getValue(int.class); // 6double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(double.class); // 24.0// -- Division --int minusTwo = parser.parseExpression("6 / -3").getValue(int.class); // -2double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(double.class); // 1.0// -- Modulus --int three = parser.parseExpression("7 % 4").getValue(int.class); // 3int oneInt = parser.parseExpression("8 / 5 % 2").getValue(int.class); // 1// -- Exponential power --int maxInt = parser.parseExpression("(2^31) - 1").getValue(int.class); // Integer.MAX_VALUEint minInt = parser.parseExpression("-2^31").getValue(int.class); // Integer.MIN_VALUE// -- Operator precedence --int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(int.class); // -21
3.5.5 赋值运算
在SpEL中也可以通过=
表达式或者setValue
方法执行对某个变量赋值的操作
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();// 通过setValue方法赋值
parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic");// 通过 = 在表达式中赋值
String aleks = parser.parseExpression("name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);
3.5.6 三元运算
SpEL也支持 boolean ? trueValue : falseValue
形式的三元运算
String falseString = parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(String.class);
3.5.7 Elvis(埃尔维斯) 运算
埃尔维斯运算符是三元运算符语法的一种简化形式,以下示例中当name为null或者空字符串时,返回"Unknown"
ExpressionParser parser = new SpelExpressionParser();String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name); // 'Unknown'
3.5.8 Safe Navigation(安全导航)
Safe Navigation Operator(安全导航操作符)?.
是一种用于处理可能为 null 的对象引用的操作符。它可以在访问可能为 null 的对象成员时避免空指针异常。
// evaluates to null - does not throw NullPointerException
city = parser.parseExpression("placeOfBirth?.city") .getValue(context, tesla, String.class);
也可用于数组、集合、字典的顾虑和投影
- 安全过滤:
?.?[过滤表达式]
- 安全首位:
?.\^[过滤表达式]
- 安全末位:
?.$[过滤表达式]
- 安全投影:
?.![投影表达式]
安全导航还可以连用
#person?.address?.city
3.5.9 运算符重载
默认情况下,在 SpEL 的操作枚举(加法、减法、除法、乘法、取模和幂)中定义的数学运算支持像数字这样的简单类型。通过提供一个OperatorOverloader的实现,表达式语言可以在其他类型上支持这些操作。
例如,如果我们想要重载加法运算符,以便使用+符号将两个列表连接起来,我们可以如下实现一个自定义的OperatorOverloader。
pubic class ListConcatenation implements OperatorOverloader {@Overridepublic boolean overridesOperation(Operation operation, Object left, Object right) {return (operation == Operation.ADD &&left instanceof List && right instanceof List);}@Overridepublic Object operate(Operation operation, Object left, Object right) {if (operation == Operation.ADD &&left instanceof List list1 && right instanceof List list2) {List result = new ArrayList(list1);result.addAll(list2);return result;}throw new UnsupportedOperationException("No overload for operation %s and operands [%s] and [%s]".formatted(operation, left, right));}
}StandardEvaluationContext context = new StandardEvaluationContext();
context.setOperatorOverloader(new ListConcatenation());// evaluates to a new list: [1, 2, 3, 4, 5]
parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List.class);
一个操作符重载器不会改变一个操作符的默认语义。例如,在上述例子中 2+2 仍然计算结果为 4。
3.6 类型
可以使用特殊的 T
操作符来指定一个Java类的实例。静态方法也通过使用这个操作符来调用。如果使用StandardEvaluationContext
,那么 java.lang 包内的类型的 T()引用不需要是写完整的包名,但所有其他类型引用必须写完整。
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);
如果使用自定义EvaluationContext,则需要手动配置一个带有特定ClassLoader的StandardTypeLocator,以确保 SpEL 表达式解析器能够定位用户类型。
例如,spring-context模块中的StandardBeanExpressionResolver使用相应BeanFactory的 beanClassLoader来配置StandardTypeLocator。
3.7 对象构造
需要构造对象的时候,需要用 new 关键字并且要写完整的包名+类名 (除了java.lang包)
Inventor einstein = p.parseExpression("new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')").getValue(Inventor.class);
3.8 变量
在EvaluationContext
中使用setVariable()
方法设置变量,在表达式中使用#变量名
来获取变量的值, 变量名的命名规范遵循Java语言变量名的命名规范。
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
变量总是被定义并且指代根上下文对象。
// Create a list of prime integers.
List<Integer> primes = List.of(2, 3, 5, 7, 11, 13, 17);// Create parser and set variable 'primes' as the list of integers.
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("primes", primes);// Select all prime numbers > 10 from the list (using selection ?{...}).
String expression = "#primes.?[#this > 10]";// Evaluates to a list containing [11, 13, 17].
List<Integer> primesGreaterThanTen =parser.parseExpression(expression).getValue(context, List.class);
#this
和#root
共用:
// Create parser and evaluation context.
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();// Create an inventor to use as the root context object.
Inventor tesla = new Inventor("Nikola Tesla");
tesla.setInventions("Telephone repeater", "Tesla coil transformer");// Iterate over all inventions of the Inventor referenced as the #root
// object, and generate a list of strings whose contents take the form
// "<inventor's name> invented the <invention>." (using projection !{...}).
String expression = "#root.inventions.![#root.name + ' invented the ' + #this + '.']";// Evaluates to a list containing:
// "Nikola Tesla invented the Telephone repeater."
// "Nikola Tesla invented the Tesla coil transformer."
List<String> results = parser.parseExpression(expression).getValue(context, tesla, List.class);
3.9 方法
通过注册用户定义的函数来扩展 SpEL,这些函数可以在表达式中通过使用#functionName(…)
语法来调用。函数可以通过setVariable()
方法在EvaluationContext
实现中作为变量进行注册。
StandardEvaluationContext
还定义了registerFunction(…)
方法,这些方法提供了一种方便的方式来将一个函数注册为java.lang.reflect.Method
或java.lang.invoke.MethodHandle
。
注册一个Mehtod:
public abstract class StringUtils {public static String reverseString(String input) {return new StringBuilder(input).reverse().toString();}
}ExpressionParser parser = new SpelExpressionParser();EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",StringUtils.class.getMethod("reverseString", String.class));// evaluates to "olleh"
String helloWorldReversed = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);
注册一个MethodHandle:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",MethodType.methodType(String.class, Object[].class));
context.setVariable("message", mh);// evaluates to "Simple message: <Hello World>"
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')").getValue(context, String.class);
如果目标和所有的参数都被绑定,这很可能会有更好的性能。在那种情况下,在 SpEL 表达式中不需要任何参数
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();String template = "This is a %s message with %s words: <%s>";
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",MethodType.methodType(String.class, Object[].class)).bindTo(template).bindTo(varargs); //here we have to provide arguments in a single array binding
context.setVariable("message", mh);// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
String message = parser.parseExpression("#message()").getValue(context, String.class);
3.10 Bean参照
如果上下文已经使用一个 Bean 解析器进行了配置,你可以通过使用@
符号从表达式中查找 beans。
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("@something").getValue(context);
要访问工厂 bean 本身,应该在 bean 名称前加上一个&
符号
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);
4 模板表达式
模板表达式允许将字面文本与一个或多个求值块混合。每个求值块用你可以定义的前缀和后缀字符来界定。一个常见的选择是使用#{ }作为界定符。
String randomPhrase = parser.parseExpression("random number is #{T(java.lang.Math).random()}",new TemplateParserContext()).getValue(String.class);// evaluates to "random number is 0.7038186818312008"
TemplateParserContext
是ParserContext
接口的实现类,定义了用#{ }
包裹SpEL内容。