評估

本節介紹 SpEL 介面的程式設計式用法及其表示式語言。完整的語言參考可在語言參考中找到。

以下程式碼演示瞭如何使用 SpEL API 來評估字串字面量表達式 Hello World

  • Java

  • Kotlin

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 message 變數的值是 "Hello World"
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
1 message 變數的值是 "Hello World"

您最有可能使用的 SpEL 類和介面位於 org.springframework.expression 包及其子包中,例如 spel.support

ExpressionParser 介面負責解析表示式字串。在前面的示例中,表示式字串是由單引號包圍的字串字面量。Expression 介面負責評估定義的表示式字串。呼叫 parser.parseExpression(…​)exp.getValue(…​) 時可能丟擲的兩種異常分別是 ParseExceptionEvaluationException

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 WorldBytes 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(…​) 配置自定義訪問器,可以停用賦值,並透過 builder 可選地啟用方法解析和/或型別轉換器。

型別轉換

預設情況下,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,則可以在建立提供給 SpelExpressionParserSpelParserConfiguration 時指定自定義的 maxExpressionLength。如果您希望設定用於在 ApplicationContext 中解析 SpEL 表示式的 maxExpressionLength — 例如,在 XML Bean 定義、@Value 等 — 您可以設定一個 JVM 系統屬性或名為 spring.context.expression.maxLength 的 Spring 屬性為您應用所需的表示式最大長度(參閱支援的 Spring 屬性)。

SpEL 編譯

Spring 為 SpEL 表示式提供了一個基本編譯器。表示式通常被解釋執行,這在評估期間提供了很大的動態靈活性,但未提供最佳效能。對於偶爾使用表示式來說,這很好,但是當由 Spring Integration 等其他元件使用時,效能可能非常重要,而對動態性沒有實際需求。

SpEL 編譯器旨在解決這一需求。在評估期間,編譯器生成一個 Java 類,該類在執行時體現表示式行為,並使用該類實現更快的表示式評估。由於表示式缺乏型別資訊,編譯器在執行編譯時會使用在解釋執行表示式期間收集的資訊。例如,它純粹從表示式中不知道屬性引用的型別,但在第一次解釋評估期間,它會找出它的型別。當然,基於此類派生資訊進行編譯可能會導致後續問題,如果各種表示式元素的型別隨時間發生變化。因此,編譯最適合那些型別資訊在重複評估時不會改變的表示式。

考慮以下基本表示式。

someArray[0].someProperty.someOtherProperty < 0.1

由於前面的表示式涉及陣列訪問、一些屬性解引用和數值運算,因此效能提升會非常明顯。在一個包含 50,000 次迭代的微基準測試示例執行中,使用直譯器評估耗時 75ms,而使用表示式的編譯版本僅需 3ms。

編譯器配置

編譯器預設不開啟,但您可以透過兩種不同的方式開啟。您可以透過解析器配置過程(前面討論過)開啟它,或者在 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(通常是表示式評估期間執行執行緒的上下文 ClassLoader)。

配置編譯器的第二種方法適用於 SpEL 嵌入在其他元件中,且無法透過配置物件進行配置的情況。在這種情況下,可以透過 JVM 系統屬性(或透過 SpringProperties 機制)將 spring.expression.compiler.mode 屬性設定為 SpelCompilerMode 列舉值(offimmediatemixed)之一。

編譯器限制

Spring 不支援編譯所有型別的表示式。主要關注點在於可能在效能關鍵場景中使用的常見表示式。以下型別的表示式無法編譯。

  • 涉及賦值的表示式

  • 依賴於轉換服務的表示式

  • 使用自定義解析器的表示式

  • 使用過載運算子的表示式

  • 使用陣列構建語法的表示式

  • 使用選擇或投影的表示式

  • 使用 bean 引用的表示式

未來可能支援編譯更多型別的表示式。