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內容。