開發與維運

Spring 5 中文解析核心篇-IoC容器之SpEL表達式

Spring表達式語言(簡稱SpEl)是非常強大的表達式語言,它支持在運行時查詢和手動操作對象圖。這個語言語法類似EL但是提供了額外的特性,最著名的是方法調用和基本字符串模板功能。

雖然還有其他幾種Java表達式語言OGNLMVELJBoss EL可用,但創建Spring表達式語言是為了向Spring社區提供一種受良好支持的表達式語言,可以跨Spring組合中的所有產品使用。它的語言特性是由Spring組合中的項目需求驅動的,包括用於Eclipse的Spring工具中的代碼完成支持的工具需求。也就是說,SpEL基於與技術無關的API,如果需要,可以將其他表達語言實現集成在一起。雖然SpEL是Spring產品組合中表達評估的基礎,但它並不直接與Spring綁定,可以獨立使用。為了自我包含,本章中的許多例子使用SpEL,好像它是一種獨立的表達語言。這需要創建一些引導基礎設施類,比如解析器。大多數Spring用戶不需要處理這個基礎設施,相反,而只需編寫表達式字符串進行評估。這種典型用法的一個示例是將SpEL集成到創建XML或基於註解的Bean定義中,如Expression支持中所示,用於定義bean定義。

本章介紹了表達語言,其API和語言語法的功能。在許多地方,InventorSociety類都用作表達評估的目標對象。這些類聲明和用於填充它們的數據在本章末尾列出(末尾給出示例代碼)。

表達式語言支持以下功能:

  • 文字表達式
  • 布爾運算符和關係運算符
  • 常用表達式
  • 類表達式
  • 訪問屬性,數組,列表和映射
  • 方法調用
  • 關係運算符
  • 分配
  • 調用構造函數
  • Bean引用
  • 數組構造
  • 內聯列表
  • 內聯Map
  • 三元運算符
  • 變量
  • 用戶定義的功能
  • 集合投影
  • 集合選擇
  • 模板表達式
4.1 評估

本節介紹SpEL接口及其表達語言的簡單用法。完整的語言參考可以在“語言參考”中找到。

以下代碼介紹了SpEL API,用於評估文字字符串表達式Hello World

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); //1
String message = (String) exp.getValue();
  1. 消息值變量使用'Hello World'

你最可能使用的SpEL類和接口位於org.springframework.expression包及其子包中,例如spel.support

ExpressionParser接口負責解析表達式字符串。在前面的示例中,表達式字符串是由周圍的單引號表示的字符串文字。Expression接口負責評估先前定義的表達式字符串。分別調用parser.parseExpressionexp.getValue時,可以引發兩個異常,ParseExceptionEvaluationException

SpEL支持多種功能,例如調用方法、訪問屬性和調用構造函數。

在以下方法調用示例中,我們在字符串文字上調用concat方法:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); //1
String message = (String) exp.getValue();
  1. 消息值 'Hello World!'是 'Hello World!'。
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); //1
byte[] bytes = (byte[]) exp.getValue();
  1. 此行將文字轉換為字節數組。

SpEL還通過使用標準的點符號(例如prop1.prop2.prop3)以及相應的屬性值設置值支持嵌套屬性。也可以訪問公共字段。

下面的示例演示如何使用點表示法獲取文字的長度:

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給出文字的長度。

可以調用String的構造函數,而不是使用字符串文字,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); //1
String message = exp.getValue(String.class);
  1. 從文字構造一個新的String並將其變為大寫。

注意通用方法的使用:

public <T> T getValue(Class<T> desiredResultType)。使用這個方法移除需要需要強制轉換表達式值為期望結果類型。如果這個值不能轉換為類型T或通過註冊的類型轉換器轉換,會拋出一個EvaluationException異常。

SpEL的更常見用法是提供一個針對特定對象實例(稱為根對象)進行評估的表達式字符串。以下示例顯示如何從Inventor類的實例檢索name屬性或如何創建布爾條件:

// 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);
4.1.1 理解EvaluationContext

在評估表達式以解析屬性、方法或字段並幫助執行類型轉換時,使用EvaluationContext接口。Spring提供了兩種實現。

  • SimpleEvaluationContext: 針對不需要完全使用SpEL語言語法並且應該有意義地加以限制的表達式類別,公開基本SpEL語言特性和配置選項的子集。示例包括但不限於數據綁定表達式和基於屬性的過濾器。
  • StandardEvaluationContext: 公開SpEL語言功能和配置選項的全部集合。你可以使用它來指定默認的根對象並配置每個可用的評估相關策略。

SimpleEvaluationContext設計為僅支持SpEL語言語法的子集。它不包括Java類型引用、構造函數和Bean引用。它還要求你明確選擇對表達式中的屬性和方法的支持級別。默認情況下,create()靜態工廠方法僅啟用對屬性的讀取訪問。你還可以獲取構建器來配置所需的確切支持級別,並針對以下一種或某種組合:

  • 僅自定義PropertyAccessor(無反射)
  • 只讀訪問的數據綁定屬性
  • 讀寫的數據綁定屬性

類型轉換

默認情況下,SpEL使用Spring核心中可用的轉換服務(org.springframework.core.convert.ConversionService)。此轉換服務附帶許多內置轉換器,用於常見轉換,但也可以完全擴展,以及你可以在類型之間添加自定義轉換。此外,它是支持泛型的。這意味著,當你在表達式中使用泛型類型時,SpEL會嘗試進行轉換以維護遇到的任何對象的類型正確性。

實際上這是什麼意思?假設使用setValue()進行賦值來設置List屬性。 該屬性的類型實際上是List <Boolean>。SpEL識別到列表中的元素在放入列表之前需要轉換為Boolean。下面例子顯示怎樣去做:

class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

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);
4.1.2 解析配置

可以使用解析器配置對象(org.springframework.expression.spel.SpelParserConfiguration)配置SpEL表達式解析器。配置對象控制某些表達式組件的行為。例如,如果你索引到數組或集合中並且指定索引處的元素為null,則可以自動地創建該元素。當使用由屬性引用鏈組成的表達式時,這很有用。如果你索引到數組或列表中並指定了超出數組或列表當前大小末尾的索引時則可以自動增長數組或列表以容納該索引。下面的示例演示如何自動增加列表:

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
4.1.3 SpEL編譯器

Spring Framework 4.1包含一個基本的表達式編譯器。通常對表達式進行解釋,這樣可以在評估過程中提供很大的動態靈活性,但不能提供最佳性能。對於偶爾使用表達式,這很好,但是,當與其他組件(如Spring Integration)集成一起使用時,性能可能非常重要,並且不需要動態性。

SpEL編譯器旨在滿足這一需求。在評估過程中,編譯器會生成一個Java類,該類體現了運行時的表達式行為,並使用該類來實現更快的表達式評估。由於表達式周圍缺乏輸入,編譯器在執行編譯時使用在解釋表達式評估期間收集的信息。例如,它不僅僅從表達式中就知道屬性引用的類型,而是在第一次解釋評估時就知道它是什麼。當然,如果各種表達元素的類型隨時間變化,則基於此類派生信息進行編譯會在以後引起麻煩。因此,編譯最適合類型信息在重複評估時不會改變的表達式。

考慮下面簡單表達式:

someArray[0].someProperty.someOtherProperty < 0.1

由於前面的表達式涉及數組訪問,一些屬性取消引用和數字運算,因此性能提升可能非常明顯。在一個示例中,進行了50000次迭代的微基準測試,使用解釋器評估需要75毫秒,而使用表達式的編譯版本僅需要3毫秒。

編譯器配置

默認情況下不打開編譯器,但是你可以通過兩種不同的方式之一來打開它。當SpEL用法嵌入到另一個組件中時,可以使用解析器配置處理(前面討論過)或使用系統屬性來打開它。本節討論這兩個選項。

編譯器可以在org.springframework.expression.spel.SpelCompilerMode枚舉中捕獲的三種模式之一進行操作。模式如下:

  • OFF(default):編譯器被關閉
  • IMMEDIATE:在立即模式下,表達式將盡快編譯。通常是在第一次解釋評估之後。如果編譯的表達式失敗(通常是由於類型更改,如前所述),則表達式評估的調用者將收到異常。
  • MIXED:在混合模式下,表達式會隨著時間靜默在解釋模式和編譯模式之間切換。經過一定數量的解釋運行後,它們會切換到編譯形式,如果編譯形式出了問題(例如,如前面所述的類型更改),則表達式會自動再次切換回解釋形式。一段時間後,它可能會生成另一個已編譯的格式並切換到它。基本上,用戶在IMMEDIATE模式下獲得的異常是在內部處理的。

存在IMMEDIATE模式是因為MIXED模式可能會導致具有副作用的表達式出現問題。如果編譯表達式在部分成功之後崩潰,那麼它可能已經做了一些影響系統狀態的事情。如果發生這種情況,調用者可能不希望它在解釋模式下靜默地重新運行,因為表達式的一部分可能運行了兩次。

選擇模式後,使用SpelParserConfiguration配置解析器。以下示例顯示瞭如何執行此操作:

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);

當指定編譯器模式時,還可以指定一個類加載器(允許傳遞null)。編譯後的表達式定義在提供的任意下創建的子類加載器中。重要的是要確保,如果指定了類加載器,則它可以查看表達式評估過程中涉及的所有類型。如果未指定類加載器,則使用默認的類加載器(通常是在表達式評估期間運行的線程的上下文類加載器)。

第二種配置編譯器的方法是將SpEL嵌入到其他組件中,並且可能無法通過配製對象進行配置。在這些情況下,可以使用系統屬性。你可以將spring.expression.compiler.mode屬性設置為SpelCompilerMode枚舉值之一(OFFIMMEDIATEMIXED)。

編譯器限制

從Spring Framework 4.1開始,已經有了基本的編譯框架。但是,該框架尚不支持編譯每種表達式。最初的重點是可能在性能關鍵型上下文中使用的通用表達式。

目前無法編譯以下類型的表達式:

  • 涉及賦值的表達
  • 表達式依賴轉換服務
  • 使用自定義解析器或訪問器的表達式
  • 使用選擇或投影的表達式
4.2 在bean定義中的表達式

你可以將SpEL表達式與基於XML或基於註解的配置元數據一起使用,以定義BeanDefinition實例。在這兩種情況下,用於定義表達式的語法都採用#{<表達式字符串>}的形式。

4.2.1 XML配置

可以使用表達式來設置屬性或構造函數參數值,如以下示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

systemProperties變量是預定義的,因此你可以在表達式中使用它,如以下示例顯示:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>

請注意,在這種情況下,不必在預定義變量前加上#符號。

你還可以按名稱引用其他bean屬性,如以下示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

    <!-- other properties -->
</bean>
4.2.2 註解配置

若要指定默認值,可以將@Value註解放置在字段、方法以及方法或構造函數參數上。

下面的示例設置字段變量的默認值:

public class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}

以下示例顯示了等效的但使用屬性設置器方法的示例:

public class PropertyValueTestBean {

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}

自動裝配的方法和構造函數也可以使用@Value註解,如以下示例所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;
    private String defaultLocale;

    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
4.3 語法手冊

本節描述了Spring Expression Language的工作方式。它涵蓋以下主題:

4.3.1 文字表達式

支持的文字表達式的類型為字符串、數值(int,實數,十六進制)、布爾和null。字符串由單引號引起來。要將單引號本身放在字符串中,請使用兩個單引號字符。

以下清單顯示了字符串的簡單用法。通常,它們不是像這樣孤立地使用,而是作為更復雜的表達式的一部分使用-例如,在邏輯比較運算符的一側使用文字。

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

數字支持使用負號,指數符號和小數點。默認情況下,使用Double.parseDouble()解析實數。

參考代碼:com.liyong.ioccontainer.service.expression.LiteralExpr

4.3.2 屬性、數組、列表、Map和索引器

使用屬性引用進行導航很容易。為此,請使用句點來指示嵌套的屬性值。Inventor類的實例pupintesla填充有示例部分使用的類中列出的數據。要向下導航並獲取Tesla的出生年份和Pupin的出生城市,我們使用以下表達式:

// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

屬性名稱的首字母允許不區分大小寫。數組和列表的內容通過使用方括號表示法獲得,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions Array

// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
        context, tesla, String.class);

// Members List

// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
        context, ieee, String.class);

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        context, ieee, String.class);

通過在方括號內指定文字鍵值可以獲取映射的內容。在下面的示例中,由於Officers的鍵是字符串,因此我們可以指定字符串文字:

// Officer's Dictionary

Inventor pupin = parser.parseExpression("Officers['president']").getValue(
        societyContext, Inventor.class);

// evaluates to "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
        societyContext, String.class);

// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
        societyContext, "Croatia");
4.3.3 內聯列表

你可以使用{}表示法在表達式中直接表達列表。

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{}本身表示一個空列表。出於性能原因,如果列表本身完全由固定文字組成,則會創建一個常量列表來表示該表達式(而不是在每次求值時都建立一個新列表)。

4.3.4 內聯Map

你也可以使用{key:value}表示法在表達式中直接表達Map。以下示例顯示瞭如何執行此操作:

// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}本身意味著一個空的Map。出於性能原因,如果Map本身由固定的文字或其他嵌套的常量結構(列表或映射圖)組成,則會創建一個常量Map來表示該表達式(而不是在每次求值時都構建一個新的Map)。Map鍵的引號是可選的。上面的示例不使用帶引號的鍵。

4.3.5 數組構造

你可以使用熟悉的Java語法來構建數組,可以選擇提供一個初始化程序,以在構造時填充該數組。以下示例顯示瞭如何執行此操作:

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

構造多維數組時,當前無法提供初始化程序。

4.3.6 方法

你可以使用典型的Java編程語法來調用方法。你還可以在文字上調用方法。還支持變量參數。下面的示例演示如何調用方法:

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);

// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
        societyContext, Boolean.class);
4.3.7 操作符

Spring表達式語言支持以下幾種運算符:

關係運算符

使用標準運算符表示法支持關係運算符(等於、不等於、小於、小於或等等、大於和大於或等於)。以下清單顯示了一些運算符示例:

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

null的大於和小於比較遵循一個簡單的規則:null被視為無(不是零)。結果,任何其他值始終大於null(X> null始終為true),並且其他任何值都不小於零(X <null始終為false)。如果你更喜歡數字比較,請避免使用基於數字的空比較,而建議使用零進行比較(例如,X>0或X<0)。

除了標準的關係運算符外,SpEL還支持instanceof和基於正則表達式的matches運算符。以下清單顯示了兩個示例:

// evaluates to false
boolean falseValue = parser.parseExpression(
        "'xyz' instanceof T(Integer)").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression(
        "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

//evaluates to false
boolean falseValue = parser.parseExpression(
        "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

使用基本類型時要小心,因為它們會立即被封裝到包裝器類型中,所以如預期的,1 instanceof T(int)評估值為false,而1 instanceof T(Integer)評估值為true。

每個符號運算符也可以指定為純字母等效項。這樣可以避免使用的符號對於嵌入表達式的文檔類型具有特殊含義的問題(例如在XML文檔中)。等效的文字是:

  • lt (<)
  • gt (>)
  • le (<=)
  • ge (>=)
  • eq (==)
  • ne (!=)
  • div (/)
  • mod (%)
  • not (!)

所有的文本運算符都不區分大小寫。

參考代碼:com.liyong.ioccontainer.service.expression.Operators

邏輯操作符

SpEL支持以下邏輯運算符:

  • and (&&)
  • or (||)
  • not (!)

下面的示例演示如何使用邏輯運算符:

// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

數字運算符

你可以在數字和字符串上使用加法運算符。你只能對數字使用減法、乘法和除法運算符。你還可以使用模數(%)和指數冪(^)運算符。強制執行標準運算符優先級。以下示例顯示了正在使用的數學運算符:

// 加法
int two = parser.parseExpression("1 + 1").getValue(Integer.class);  // 2

String testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String.class);  // 'test string'

// 減法
int four = parser.parseExpression("1 - -3").getValue(Integer.class);  // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);  // -9000

// 乘法
int six = parser.parseExpression("-2 * -3").getValue(Integer.class);  // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);  // 24.0

// 除法
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class);  // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);  // 1.0

// 取模
int three = parser.parseExpression("7 % 4").getValue(Integer.class);  // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1

// 操作符優先級
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21

賦值運算符

要設置屬性,請使用賦值運算符(=)。這通常在對setValue的調用內完成,但也可以在對getValue的調用內完成。下面的清單顯示了使用賦值運算符的兩種方法:

Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");

// alternatively
String aleks = parser.parseExpression(
        "Name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);
4.3.8 類型

你可以使用特殊的T運算符來指定java.lang.Class(類型)的實例。靜態方法也可以通過使用此運算符來調用。StandardEvaluationContext使用TypeLocator查找類型,而StandardTypeLocator(可以替換)在瞭解java.lang包的情況下構建。這意味著對java.lang中的類型的T()引用不需要完全限定,但是所有其他類型的引用必須是(備註:在java.lang包中不需要指定java.lang)。下面的示例演示如何使用T運算符:

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean.class);
4.3.9 構造函數

你可以使用new運算符來調用構造函數。除基本類型(int,float等)和String以外的所有其他類都應使用完全限定的類名。下面的示例演示如何使用new運算符調用構造函數:

Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

//create new inventor instance within add method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);
4.3.10 變量

你可以使用#variableName語法在表達式中引用變量。通過在EvaluationContext實現上使用setVariable方法設置變量。

有效的變量名稱必須由以下一個或多個受支持的字符組成。

  • 字母:A到Z和a到z
  • 數組:0到9
  • 下劃線:_
  • 美元符號:$

以下示例顯示瞭如何使用變量。

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName())  // "Mike Tesla"

#this#root變量

#this變量始終是定義的,並且引用當前的評估對象(針對解決不合格的引用)。同時也始終定義#root變量,並引用根上下文對象。儘管#this可能隨表達式的組成部分的評估而變化,但#root始終引用根。以下示例說明如何使用#this#root變量:

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);
4.3.11 函數

你可以通過註冊可以在表達式字符串中調用的用戶定義函數來擴展SpEL。該函數通過EvaluationContext註冊。下面的示例顯示如何註冊用戶定義的函數:

Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

例如,考慮以下用於反轉字符串的實用程序方法:

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++) {
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

然後,你可以註冊並使用前面的方法,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
        StringUtils.class.getDeclaredMethod("reverseString", String.class));

String helloWorldReversed = parser.parseExpression(
        "#reverseString('hello')").getValue(context, String.class);

參考代碼:com.liyong.ioccontainer.service.expression.PropertiesArraysAndSoOn

4.3.12 Bean引用

如果評估上下文已使用bean解析器配置,則可以使用@符號從表達式中查找bean。 以下示例顯示瞭如何執行此操作:

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

要訪問工廠bean本身,你應該在bean名稱前加上符號。以下示例顯示瞭如何執行此操作:

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);

參考代碼:com.liyong.ioccontainer.service.expression.BeanRef

4.3.13 三元運算符

你可以使用三元運算符在表達式內部執行if-then-else條件邏輯。以下清單顯示了一個最小的示例:

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

在這種情況下,布爾值false導致返回字符串值'falseExp'。一個更現實的示例如下:

parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
        "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

有關三元運算符的更短語法,請參閱關於Elvis運算符的下一部分。

4.3.14 Elvis運算符

Elvis運算符是三元運算符語法的簡化,並且在Groovy語言中使用。使用三元運算符語法,通常必須將變量重複兩次,如以下示例所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

相反,你可以使用Elvis運算符。以下示例顯示瞭如何使用Elvis運算符:

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name);  // 'Unknown'

以下清單顯示了一個更復雜的示例:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Nikola Tesla

tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley

可以使用Elvis運算符在表達式中應用默認值。以下示例顯示瞭如何在@Value表達式中使用Elvis運算符:

@Value("#{systemProperties['pop3.port'] ?: 25}")

如果定義,將注入系統屬性pop3.port,否則將注入25

4.3.15 安全導航操作符

安全導航運算符用於避免NullPointerException,它來自Groovy語言。通常,當你引用一個對象時,可能需要在訪問該對象的方法或屬性之前驗證其是否為null。為了避免這種情況,安全導航運算符返回null而不是引發異常。下面的示例演示如何使用安全導航操作符:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // Smiljan

tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // null - 不拋出 NullPointerException!!!

參考代碼:com.liyong.ioccontainer.service.expression.SafeNavigation

4.3.16 集合選擇

選擇是一種強大的表達語言功能,可讓你通過從源集合中進行選擇來將其轉換為另一個集合。

選擇使用.?[selectionExpression]的語法。它過濾集合並返回一個包含原始元素子集的新集合。例如,通過選擇,我們可以輕鬆地獲得發明者的列表,如以下示例所示:

List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "Members.?[Nationality == 'Serbian']").getValue(societyContext);

在列表和Map上都可以選擇。對於列表,將針對每個單獨的列表元素評估選擇標準。針對Map,針對每個Map實體(Java類型Map.Entry的對象)評估選擇標準。每個Map實體都有其鍵和值,可作為屬性訪問以供選擇。

以下表達式返回一個新Map,該Map由原始Map中實體值小於27的那些元素組成:

Map newMap = parser.parseExpression("map.?[value<27]").getValue();

除了返回所有選定的元素外,你可以檢索第一個或最後一個值。要獲得與選擇匹配的第一個條目,語法為.^ [selectionExpression]。要獲取最後一個匹配選擇,語法為.$ [selectionExpression]

參考代碼:com.liyong.ioccontainer.service.expression.CollectionSelected

4.3.17 集合投影

投影使集合可以驅動子表達式的求值,結果是一個新的集合。投影的語法為.![projectionExpression]。例如,假設我們有一個發明家列表,但是想要他們出生的城市列表。實際上,我們希望為發明人列表中的每個實體評估“placeOfBirth.city”。下面的示例使用投影來做到這一點:

// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

你還可以使用Map來驅動投影,在這種情況下,將針對Map中的每個實體(表示為Java Map.Entry)對投影表達式進行評估。跨Map的投影結果是一個列表,其中包含針對每個Map實體的投影表達式評估。

參考代碼:com.liyong.ioccontainer.service.expression.CollectionProjection

4.3.18 表達式模版

表達式模板允許將文字文本與一個或多個評估塊混合。每個評估塊均以你可以定義的前綴和後綴字符分隔。常見的選擇是使用#{}作為分隔符,如以下示例所示:

String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

通過將文字文本“隨機數”與評估#{}分隔符內的表達式的結果(在本例中為調用那個random()方法的結果)相連接來評估字符串。parseExpression()方法的第二個參數的類型為ParserContextParserContext接口用於影響表達式的解析方式,以支持表達式模板功能。TemplateParserContext的定義如下:

public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}
4.4 示例中使用的類

本節列出了本章示例中使用的類。

package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;

    public Inventor(String name, String nationality) {
        GregorianCalendar c= new GregorianCalendar();
        this.name = name;
        this.nationality = nationality;
        this.birthdate = c.getTime();
    }

    public Inventor(String name, Date birthdate, String nationality) {
        this.name = name;
        this.nationality = nationality;
        this.birthdate = birthdate;
    }

    public Inventor() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Date getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }

    public PlaceOfBirth getPlaceOfBirth() {
        return placeOfBirth;
    }

    public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
        this.placeOfBirth = placeOfBirth;
    }

    public void setInventions(String[] inventions) {
        this.inventions = inventions;
    }

    public String[] getInventions() {
        return inventions;
    }
}
package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

    private String city;
    private String country;

    public PlaceOfBirth(String city) {
        this.city=city;
    }

    public PlaceOfBirth(String city, String country) {
        this(city);
        this.country = country;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String s) {
        this.city = s;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }
}
package org.spring.samples.spel.inventor;

import java.util.*;

public class Society {

    private String name;

    public static String Advisors = "advisors";
    public static String President = "president";

    private List<Inventor> members = new ArrayList<Inventor>();
    private Map officers = new HashMap();

    public List getMembers() {
        return members;
    }

    public Map getOfficers() {
        return officers;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMember(String name) {
        for (Inventor inventor : members) {
            if (inventor.getName().equals(name)) {
                return true;
            }
        }
        return false;
    }
}

作者

個人從事金融行業,就職過易極付、思建科技、某網約車平臺等重慶一流技術團隊,目前就職於某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大數據、數據存儲、自動化集成和部署、分佈式微服務、響應式編程、人工智能等領域。同時也熱衷於技術分享創立公眾號和博客站點對知識體系進行分享。關注公眾號訂閱最新資訊!!!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公眾號:

技術交流群:

Leave a Reply

Your email address will not be published. Required fields are marked *