前言
時(shí)隔多年重新開始學(xué)習(xí)Spring,之前只用過(guò)Spring 2失暴,掰掰指頭已經(jīng)過(guò)了快10年了。
看到書里提到了SpEL微饥,一頭霧水逗扒,在2的時(shí)代好像從來(lái)沒(méi)見過(guò)。把學(xué)習(xí)到的做個(gè)總結(jié)畜号。
SpEL概述
SpEL是Spring內(nèi)置的表達(dá)式語(yǔ)言缴阎,語(yǔ)法與OGNL等其他表達(dá)式語(yǔ)言十分類似。SpEL設(shè)計(jì)之初就是朝著做一個(gè)表達(dá)式語(yǔ)言的通用框架简软,可以獨(dú)立運(yùn)行蛮拔。
SpEL的主要相關(guān)類
SpEL對(duì)表達(dá)式語(yǔ)法解析過(guò)程進(jìn)行了很高的抽象,抽象出解析器痹升、表達(dá)式建炫、解析上下文、估值(Evaluate)上下文等對(duì)象疼蛾,非常優(yōu)雅的表達(dá)了解析邏輯肛跌。主要的對(duì)象如下:
類名 | 說(shuō)明 |
---|---|
ExpressionParser | 表達(dá)式解析器接口,包含了(Expression) parseExpression(String) , (Expression) parseExpression(String, ParserContext) 兩個(gè)接口方法 |
ParserContext | 解析器上下文接口察郁,主要是對(duì)解析器Token的抽象類衍慎,包含3個(gè)方法:getExpressionPrefix ,getExpressionSuffix 和isTemplate ,就是表示表達(dá)式從什么符號(hào)開始什么符號(hào)結(jié)束皮钠,是否是作為模板(包含字面量和表達(dá)式)解析稳捆。一般保持默認(rèn)。 |
Expression | 表達(dá)式的抽象麦轰,是經(jīng)過(guò)解析后的字符串表達(dá)式的形式表示乔夯。通過(guò)expressionInstance.getValue 方法,可以獲取表示式的值款侵。也可以通過(guò)調(diào)用getValue(EvaluationContext) 末荐,從評(píng)估(evaluation)上下文中獲取表達(dá)式對(duì)于當(dāng)前上下文的值 |
EvaluationContext | 估值上下文接口,只有一個(gè)setter方法:setVariable(String, Object) 新锈,通過(guò)調(diào)用該方法甲脏,可以為evaluation提供上下文變量 |
完整的例子:
public static void main(String[] args) {
String greetingExp = "Hello, #{ #user }"; (1)
ExpressionParser parser = new SpelExpressionParser(); (2)
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", "Gangyou"); (3)
Expression expression = parser.parseExpression(greetingExp,
new TemplateParserContext()); (4)
System.out.println(expression.getValue(context, String.class)); (5)
}
代碼解釋:
- 創(chuàng)建一個(gè)模板表達(dá)式,所謂模板就是帶字面量和表達(dá)式的字符串。其中#{}表示表達(dá)式的起止剃幌,
#user
是表達(dá)式字符串聋涨,表示引用一個(gè)變量。 - 創(chuàng)建表達(dá)式解析器负乡,SpEL框架創(chuàng)建了一個(gè)語(yǔ)言無(wú)關(guān)的處理框架牍白,所以對(duì)于其他的表達(dá)式語(yǔ)言,完全可以創(chuàng)建不同的ExpressionParser抖棘。在這里我們學(xué)習(xí)的是SpEL所以使用
SpelExpressionParser()
- 通過(guò)
evaluationContext.setVariable
可以在上下文中設(shè)定變量茂腥。 - 解析表達(dá)式,如果表達(dá)式是一個(gè)模板表達(dá)式切省,需要為解析傳入模板解析器上下文最岗。如果不傳入模板解析器上下文,指定表達(dá)式為模板朝捆,那么表達(dá)式字符串
Hello, #{ #user }
般渡,解析器會(huì)首先去嘗試解析Hello
。例子中的模板表達(dá)式芙盘,與'Hello, ' + #user
是等價(jià)的驯用。 - 使用
Expression.getValue()
獲取表達(dá)式的值,這里傳入了Evalution上下文儒老,第二個(gè)參數(shù)是類型參數(shù)蝴乔,表示返回值的類型。
SpEL語(yǔ)法
本節(jié)介紹下SpEL中的各類語(yǔ)法驮樊,在語(yǔ)法中會(huì)發(fā)現(xiàn)很多熟悉的影子薇正,比如Java,JavaScript, Groovy等等囚衔。依次介紹SpEL中的
- 字面量
- 數(shù)組和Map字面量
- 二元操作符和三元操作符
- 來(lái)自Groovy的操作符
- 數(shù)組列表和Map的訪問(wèn)
- Java Bean屬性訪問(wèn)
- Java Bean方法調(diào)用
- Java 類型訪問(wèn)
- Java 實(shí)例訪問(wèn)
字面量
SpEL支持如下類型的字面量:
類型 | 說(shuō)明 | 示例 |
---|---|---|
數(shù)字 | 支持任何數(shù)字類型挖腰,包括了各種進(jìn)制的數(shù)字,科學(xué)計(jì)數(shù)法等 | 6.0221415E+23练湿,0xFFFFFFFF |
布爾量 | true, false | |
字符串 | 用單引號(hào)包圍的字符串猴仑,單引號(hào)要用2個(gè)單引號(hào)轉(zhuǎn)義 | 'Gangyou''s, Blog' |
日期 | 沒(méi)寫成功 //TODO | |
null | 直接轉(zhuǎn)成字符串null | null |
屬性
SpEL對(duì)屬性的訪問(wèn)遵循Java Bean語(yǔ)法,對(duì)于表達(dá)式中的屬性都要提供相應(yīng)的setter鞠鲜。
比如這樣一個(gè)Bean:
private static class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
表達(dá)式firstName + ' ' + lastName
就等價(jià)于person.getFirstName() + " " + person.getLastName()
。如果屬性本身是對(duì)象断国,還支持嵌套屬性贤姆,如person.address.city
,就等價(jià)于person.getAddress().getCity()
數(shù)組稳衬、列表和Map
數(shù)組和列表都可以用過(guò)數(shù)字下標(biāo)進(jìn)行訪問(wèn)霞捡,比如list[0]
。
Map的訪問(wèn)就類似JavaScript的訪問(wèn)方式薄疚,使用key訪問(wèn)碧信。例如java代碼map.put("name", "gangyou")
其中的map對(duì)象赊琳,可以通過(guò)map['name']
獲取到字符串gangyou。
內(nèi)聯(lián)的數(shù)組和Map
Spel允許在表達(dá)式內(nèi)創(chuàng)建數(shù)組(列表)和Map砰碴,如{1,2,3,4}
,{'firstName': 'Gang', 'lastName': 'You'}
躏筏。
方法調(diào)用
SpEL使用java的相同語(yǔ)法進(jìn)行方法調(diào)用,如'Hello
.concat(', World!')`呈枉,輸出** Hello, World!**趁尼。
類型
SpEL中可以使用特定的Java類型,經(jīng)常用來(lái)訪問(wèn)Java類型中的靜態(tài)屬性或靜態(tài)方法猖辫,需要用T()
操作符進(jìn)行聲明酥泞。括號(hào)中需要包含類名的全限定名,也就是包名加上類名啃憎。唯一例外的是芝囤,SpEL內(nèi)置了java.lang
包下的類聲明,也就是說(shuō)java.lang.String
可以通過(guò)T(String)
訪問(wèn)辛萍,而不需要使用全限定名悯姊。
兩個(gè)例子: T(Math).random()
或 T(java.lang.Math).random()
創(chuàng)建實(shí)例
使用new可以直接在SpEL中創(chuàng)建實(shí)例,需要?jiǎng)?chuàng)建實(shí)例的類要通過(guò)全限定名進(jìn)行訪問(wèn)叹阔,如:new java.util.Date().now()
二元操作符
SpEL中的二元操作符同Java中的二元操作符挠轴,包括了數(shù)學(xué)運(yùn)算符、位運(yùn)算符耳幢、關(guān)系運(yùn)算符等等岸晦。
三元操作符
SpEL的三元操作符主要是 ** if else then ** 操作符 condition ? true statement : false statement
與Java中的一致。
另外一個(gè)就是借鑒自Groovy的 Elvis 操作符?:
睛藻,Elvis就是貓王启上!
這個(gè)操作符的樣子就跟貓王的發(fā)型一樣:)。
舉一個(gè)例子
String expressionStr1 = " name != null ? name : 'Default Value'";
String expressionStr2 = "name ?: 'Default Value'";
上面的兩個(gè)表達(dá)式都是先判斷 getName()
的返回值是不是null店印,如果是就返回Default Value
冈在,通過(guò)下面的Elvis操作符可以讓代碼更加的清晰。
安全訪問(wèn)符
同樣借鑒自Groovy按摘,在SpEL中引入了安全訪問(wèn)符Safe Navigator Operator——.?
包券,解決了很大問(wèn)題。相信每個(gè)Javaer都遇到過(guò)NullPointException的運(yùn)行時(shí)異常炫贤,通常是對(duì)象還未實(shí)例化或者找不到對(duì)象溅固,卻訪問(wèn)對(duì)象屬性造成的。通過(guò)安全訪問(wèn)符就可以避免這樣的問(wèn)題兰珍。
String expStr = "thisMayBeNull.?property"
這句表達(dá)式在求值的時(shí)候侍郭,不會(huì)因?yàn)?code>thisMayBeNull是Null值而拋出NullPointException,而是會(huì)簡(jiǎn)單的返回null。個(gè)人認(rèn)為此處結(jié)合Elvis操作符亮元,是一個(gè)很完善的處理方式猛计。
集合選擇
同樣借鑒自Groovy,SpEL提供了集合選擇語(yǔ)法爆捞,如下面的例子:
List<Inventor> list = (List<Inventor>) parser.parseExpression(
"Members.?[Nationality == 'Serbian']").getValue(societyContext);
如果Members List不是Null奉瘤,則在列表中選擇getNationality() == 'Serbian'
的對(duì)象集合返回。這個(gè)很類似于Java 8中的stream and filter
方式嵌削,只是更加的簡(jiǎn)潔毛好。
在[]
中間可以利用任何的布爾表達(dá)式,創(chuàng)建篩選條件苛秕。例如age > 18
肌访,name.startsWith('You')
等等。
自定義函數(shù)
SpEL提供了Java的基礎(chǔ)功能艇劫,也引入了3個(gè)借鑒自Groovy的特性語(yǔ)法提供更為簡(jiǎn)潔的表達(dá)能力吼驶。作為一款設(shè)計(jì)為框架的語(yǔ)言,更提供了自定義的擴(kuò)展能力店煞。
比如下面的例子:
public abstract class StringUtils {
public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder();
for (int i = 0; i < input.length(); i++)
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
public static void main(String[] args){
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.registerFunction("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class }));
String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);
}
}
通過(guò)使用StandardEvalutinContext.registerFunction
可以注冊(cè)自定義的函數(shù)蟹演,唯一的一點(diǎn)要求就是需要在表達(dá)式中通過(guò)#注冊(cè)函數(shù)名
的方式引用函數(shù)。