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();
// 設置值字符串 "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
來表明一個變量.變量的實際賦值是通過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()) // "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