評估
本節介紹 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(…)
時可能丟擲的兩種異常分別是 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(…)
配置自定義訪問器,可以停用賦值,並透過 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
,則可以在建立提供給 SpelExpressionParser
的 SpelParserConfiguration
時指定自定義的 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
列舉值(off
、immediate
或 mixed
)之一。