評估

本節介紹 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(…​) 組態自訂存取器,可能停用指派,並選擇性地透過建構器啟動方法解析和/或類型轉換器。

類型轉換

預設情況下,SpEL 使用 Spring 核心中可用的轉換服務 (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(通常是在運算式評估期間執行的執行緒的 Context ClassLoader)。

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

編譯器限制

Spring 不支援編譯每種類型的運算式。主要重點是可能在效能關鍵環境中使用的常見運算式。以下類型的運算式無法編譯。

  • 涉及賦值的運算式

  • 仰賴轉換服務的運算式

  • 使用自訂解析器的運算式

  • 使用多載運算子的運算式

  • 使用陣列建構語法的運算式

  • 使用選取或投影的運算式

  • 使用 Bean 參考的運算式

未來可能會支援編譯其他種類的運算式。