評估

本節介紹了 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(…​) 時可能丟擲的兩種異常分別是 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(…​)),可能停用賦值,並可選地透過構建器啟用方法解析和/或型別轉換器。

型別轉換

預設情況下,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 等中),您可以將名為 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 列舉值之一(offimmediatemixed)。

編譯器限制

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

  • 涉及賦值的表示式

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

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

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

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

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

  • 使用 bean 引用的表示式

未來可能支援編譯其他型別的表示式。

© . This site is unofficial and not affiliated with VMware.