評估
本節介紹了 SpEL 介面及其表示式語言的程式設計使用。完整的語言參考可在語言參考中找到。
以下程式碼演示瞭如何使用 SpEL API 來評估字面字串表示式 Hello World。
-
Java
-
Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
| 1 | 訊息變數的值是 "Hello World"。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
| 1 | 訊息變數的值是 "Hello World"。 |
您最可能使用的 SpEL 類和介面位於 org.springframework.expression 包及其子包中,例如 spel.support。
ExpressionParser 介面負責解析表示式字串。在前面的例子中,表示式字串是用單引號括起來的字串字面量。Expression 介面負責評估定義的表示式字串。呼叫 parser.parseExpression(…) 和 exp.getValue(…) 時可能丟擲的兩種異常分別是 ParseException 和 EvaluationException。
SpEL 支援廣泛的功能,例如呼叫方法、訪問屬性和呼叫建構函式。
在以下方法呼叫示例中,我們對字串字面量 Hello World 呼叫 concat 方法。
-
Java
-
Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
| 1 | message 的值現在是 "Hello World!"。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") (1)
val message = exp.value as String
| 1 | message 的值現在是 "Hello World!"。 |
以下示例演示瞭如何訪問字串字面量 Hello World 的 Bytes JavaBean 屬性。
-
Java
-
Kotlin
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
| 1 | 此行將字面量轉換為位元組陣列。 |
val parser = SpelExpressionParser()
// invokes 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") (1)
val bytes = exp.value as ByteArray
| 1 | 此行將字面量轉換為位元組陣列。 |
SpEL 還支援使用標準點號表示法(例如 prop1.prop2.prop3)的巢狀屬性以及相應的屬性值設定。也可以訪問公共欄位。
以下示例展示瞭如何使用點號表示法獲取字串字面量的長度。
-
Java
-
Kotlin
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
| 1 | 'Hello World'.bytes.length 給出字面量的長度。 |
val parser = SpelExpressionParser()
// invokes 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") (1)
val length = exp.value as Int
| 1 | 'Hello World'.bytes.length 給出字面量的長度。 |
可以呼叫 String 的建構函式而不是使用字串字面量,如下例所示。
-
Java
-
Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
| 1 | 從字面量構造一個新的 String 並將其轉換為大寫。 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()") (1)
val message = exp.getValue(String::class.java)
| 1 | 從字面量構造一個新的 String 並將其轉換為大寫。 |
請注意通用方法的使用:public <T> T getValue(Class<T> desiredResultType)。使用此方法無需將表示式的值轉換為所需的返回型別。如果無法將值轉換為型別 T 或使用已註冊的型別轉換器進行轉換,則會丟擲 EvaluationException。
SpEL 更常見的用法是提供一個表示式字串,該字串根據特定的物件例項(稱為根物件)進行評估。以下示例展示瞭如何從 Inventor 類的例項中檢索 name 屬性,以及如何在布林表示式中引用 name 屬性。
-
Java
-
Kotlin
// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
// Create and set a calendar
val c = GregorianCalendar()
c.set(1856, 7, 9)
// The constructor arguments are name, birthday, and nationality.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")
val parser = SpelExpressionParser()
var exp = parser.parseExpression("name") // Parse name as an expression
val name = exp.getValue(tesla) as String
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true
理解 EvaluationContext
在評估表示式時,EvaluationContext API 用於解析屬性、方法或欄位,並幫助執行型別轉換。Spring 提供了兩種實現。
SimpleEvaluationContext-
公開了 SpEL 語言特性和配置選項的一個子集,適用於不需要完整 SpEL 語言語法且應受到有意義限制的表示式類別。示例包括但不限於資料繫結表示式和基於屬性的過濾器。
StandardEvaluationContext-
公開了完整的 SpEL 語言特性和配置選項。您可以使用它指定預設根物件並配置所有可用的評估相關策略。
SimpleEvaluationContext 旨在僅支援 SpEL 語言語法的一個子集。例如,它排除了 Java 型別引用、建構函式和 bean 引用。它還要求您明確選擇表示式中屬性和方法的支援級別。建立 SimpleEvaluationContext 時,您需要選擇在 SpEL 表示式中資料繫結所需的支援級別。
-
資料繫結的只讀訪問
-
資料繫結的讀寫訪問
-
自定義
PropertyAccessor(通常不是基於反射的),可能與DataBindingPropertyAccessor結合使用
方便的是,SimpleEvaluationContext.forReadOnlyDataBinding() 透過 DataBindingPropertyAccessor 啟用對屬性的只讀訪問。類似地,SimpleEvaluationContext.forReadWriteDataBinding() 啟用對屬性的讀寫訪問。或者,透過構建器配置自定義訪問器(透過 SimpleEvaluationContext.forPropertyAccessors(…)),可能停用賦值,並可選地透過構建器啟用方法解析和/或型別轉換器。
型別轉換
預設情況下,SpEL 使用 Spring Core 中可用的轉換服務(org.springframework.core.convert.ConversionService)。此轉換服務附帶許多用於常見轉換的內建轉換器,但它也完全可擴充套件,因此您可以在型別之間新增自定義轉換。此外,它支援泛型。這意味著,當您在表示式中使用泛型型別時,SpEL 會嘗試進行轉換以維護它遇到的任何物件的型別正確性。
這在實踐中意味著什麼?假設使用 setValue() 進行賦值以設定 List 屬性。該屬性的實際型別是 List<Boolean>。SpEL 識別出列表的元素需要在放入其中之前轉換為 Boolean。以下示例展示瞭如何做到這一點。
-
Java
-
Kotlin
class Simple {
public List<Boolean> booleanList = new ArrayList<>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b is false
Boolean b = simple.booleanList.get(0);
class Simple {
var booleanList: MutableList<Boolean> = ArrayList()
}
val simple = Simple()
simple.booleanList.add(true)
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")
// b is false
val b = simple.booleanList[0]
解析器配置
可以透過使用解析器配置物件(org.springframework.expression.spel.SpelParserConfiguration)來配置 SpEL 表示式解析器。配置物件控制某些表示式元件的行為。例如,如果您索引到集合中,並且指定索引處的元素是 null,SpEL 可以自動建立該元素。這在使用由屬性引用鏈組成的表示式時非常有用。同樣,如果您索引到集合中並指定一個大於集合當前大小的索引,SpEL 可以自動增長集合以適應該索引。為了在指定索引處新增元素,SpEL 將嘗試使用元素型別的預設建構函式建立元素,然後設定指定值。如果元素型別沒有預設建構函式,則會將 null 新增到集合中。如果沒有內建轉換器或自定義轉換器知道如何設定值,則 null 將保留在集合中指定索引處。以下示例演示瞭如何自動增長 List。
-
Java
-
Kotlin
class Demo {
public List<String> list;
}
// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
ExpressionParser parser = new SpelExpressionParser(config);
Expression expression = parser.parseExpression("list[3]");
Demo demo = new Demo();
Object o = expression.getValue(demo);
// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String
class Demo {
var list: List<String>? = null
}
// Turn on:
// - auto null reference initialization
// - auto collection growing
val config = SpelParserConfiguration(true, true)
val parser = SpelExpressionParser(config)
val expression = parser.parseExpression("list[3]")
val demo = Demo()
val o = expression.getValue(demo)
// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String
預設情況下,SpEL 表示式不能包含超過 10,000 個字元;但是,maxExpressionLength 是可配置的。如果您以程式設計方式建立 SpelExpressionParser,則可以在建立提供給 SpelExpressionParser 的 SpelParserConfiguration 時指定自定義的 maxExpressionLength。如果您希望設定在 ApplicationContext 中解析 SpEL 表示式時使用的 maxExpressionLength(例如,在 XML bean 定義、@Value 等中),您可以將名為 spring.context.expression.maxLength 的 JVM 系統屬性或 Spring 屬性設定為您的應用程式所需的最大表達式長度(請參閱支援的 Spring 屬性)。
SpEL 編譯
Spring 為 SpEL 表示式提供了一個基本編譯器。表示式通常是解釋執行的,這在評估期間提供了很大的動態靈活性,但不能提供最佳效能。對於偶爾使用表示式來說,這很好,但是,當被 Spring Integration 等其他元件使用時,效能可能非常重要,並且對動態性沒有實際需求。
SpEL 編譯器旨在解決此需求。在評估期間,編譯器會生成一個 Java 類,該類在執行時體現表示式行為,並使用該類實現更快的表示式評估。由於表示式周圍缺乏型別,編譯器在執行編譯時使用從表示式的解釋評估中收集的資訊。例如,它純粹從表示式中不知道屬性引用的型別,但在第一次解釋評估期間,它會發現它是哪種型別。當然,基於此類派生資訊進行編譯可能會導致以後出現問題,如果各種表示式元素的型別隨時間變化。因此,編譯最適合那些型別資訊在重複評估時不會更改的表示式。
考慮以下基本表示式。
someArray[0].someProperty.someOtherProperty < 0.1
由於上述表示式涉及陣列訪問、一些屬性解引用和數值運算,效能提升可能非常明顯。在一個示例微基準測試中,執行 50,000 次迭代,使用直譯器評估需要 75 毫秒,而使用表示式的編譯版本僅需要 3 毫秒。
編譯器配置
編譯器預設不開啟,但您可以透過兩種不同的方式開啟它。您可以透過解析器配置過程(前面討論過)開啟它,或者在 SpEL 用法嵌入到另一個元件中時使用 Spring 屬性。本節討論這兩種選項。
編譯器可以在三種模式之一中執行,這些模式捕獲在 org.springframework.expression.spel.SpelCompilerMode 列舉中。模式如下。
OFF-
編譯器已關閉,所有表示式都將在 解釋 模式下進行評估。這是預設模式。
IMMEDIATE-
在即時模式下,表示式會盡快編譯,通常在第一次解釋評估之後。如果編譯後的表示式評估失敗(例如,由於型別更改,如前所述),表示式評估的呼叫者會收到異常。如果各種表示式元素的型別隨時間變化,請考慮切換到
MIXED模式或關閉編譯器。 MIXED-
在混合模式下,表示式評估會隨著時間在 解釋 和 編譯 之間悄無聲息地切換。在幾次成功的解釋執行後,表示式會被編譯。如果編譯後的表示式評估失敗(例如,由於型別更改),該失敗將在內部捕獲,並且系統將切換回給定表示式的解釋模式。基本上,呼叫者在
IMMEDIATE模式下收到的異常將改為在內部處理。稍後,編譯器可能會生成另一個編譯形式並切換到它。這種在解釋和編譯模式之間切換的迴圈將繼續,直到系統確定繼續嘗試沒有意義——例如,當達到某個失敗閾值時——此時系統將永久切換到給定表示式的解釋模式。
存在 IMMEDIATE 模式是因為 MIXED 模式可能導致具有副作用的表示式出現問題。如果編譯後的表示式在部分成功後崩潰,它可能已經做了影響系統狀態的事情。如果發生這種情況,呼叫者可能不希望它在解釋模式下悄無聲息地重新執行,因為表示式的一部分可能已經運行了兩次。
選擇模式後,使用 SpelParserConfiguration 配置解析器。以下示例展示瞭如何做到這一點。
-
Java
-
Kotlin
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);
val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
this.javaClass.classLoader)
val parser = SpelExpressionParser(config)
val expr = parser.parseExpression("payload")
val message = MyMessage()
val payload = expr.getValue(message)
當您指定編譯器模式時,您還可以指定一個 ClassLoader(允許傳入 null)。編譯後的表示式在提供的任何 ClassLoader 下建立的子 ClassLoader 中定義。重要的是要確保,如果指定了 ClassLoader,它能夠看到表示式評估過程中涉及的所有型別。如果您不指定 ClassLoader,則使用預設的 ClassLoader(通常是表示式評估期間執行的執行緒的上下文 ClassLoader)。
配置編譯器的第二種方式是在 SpEL 嵌入到其他元件中且無法透過配置物件進行配置時使用。在這種情況下,可以透過 JVM 系統屬性(或透過SpringProperties機制)將 spring.expression.compiler.mode 屬性設定為 SpelCompilerMode 列舉值之一(off、immediate 或 mixed)。