1. 程式人生 > >《Spring 5 官方文件》6.Spring表示式語言

《Spring 5 官方文件》6.Spring表示式語言

原文連結 譯者:何一昕

6.1 介紹

Spring Expression Language(簡稱SpEL)是一種功能強大的表示式語言、用於在執行時查詢和操作物件圖;語法上類似於Unified EL,但提供了更多的特性,特別是方法呼叫和基本字串模板函式。

雖然目前已經有許多其他的Java表示式語言,例如OGNL,MVEL和Jboss EL,SpEL的誕生是為了給Spring社群提供一種能夠與Spring生態系統所有產品無縫對接,能提供一站式支援的表示式語言。它的語言特性由Spring生態系統的實際專案需求驅動而來,比如基於eclipse的Spring Tool Suite(Spring開發工具集)中的程式碼補全工具需求。儘管如此、SpEL本身基於一套與具體實現技術無關的API,在需要的時候允許其他的表示式語言實現整合進來。

儘管SpEL在Spring產品中是作為表示式求值的核心基礎模組,本身可以脫離Spring獨立使用。為了體現它的獨立性,本章節中的許多例子都將SpEL作為獨立的表示式語言來使用。不過這樣就需要每次都先建立一些基礎框架類如解析器,而對於大多數Spring使用者來說並不需要去關注這些基礎框架類,僅僅只需要寫相應的字串求值表示式即可。一個典型的例子就是把SpEL整合進XML bean配置或者基於註解的Bean定義宣告中(詳見章節:Expression support for defining bean definitions

本章節包含SpEL的語言特性,它的API及語法。很多地方用到了Inventor類及相關的Society類作為表示式求值的操作例子物件,這幾個類的定義及操作它們的資料都列在本章的末尾.

6.2 功能特性

SpEL支援以下的一些特性:

  • 字元表示式
  • 布林和關係操作符
  • 正則表示式
  • 類表示式
  • 訪問properties,arrays,lists,maps等集合
  • 方法呼叫
  • 關係操作符
  • 賦值
  • 呼叫構造器
  • Bean物件引用
  • 建立陣列
  • 內聯lists
  • 內聯maps
  • 三元操作符
  • 變數
  • 使用者自定義函式
  • 集合投影
  • 集合選擇
  • 模板表示式

6.3 使用SpEL的介面進行表示式求值

本節介紹SpEL介面及其表示式語言的簡單使用方法。完整的語言文件見: Language Reference

下面程式碼介紹了使用SpEL API來解析字串表示式’Hello World’的示例

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();

最常用的SpEL類和介面都放在包org.springframework.expression及其子包和spel.support下

介面ExpressionParser用來解析一個字串表示式。在這個例子中字串表示式即用單引號括起來的字串.介面Expression用於對上面定義的字串表示式求值。 呼叫parser.parseExpression和exp.getValue分別可能丟擲ParseException和EvaluationException。

SpEL支援一系列廣泛的特性,例如方法呼叫,訪問屬性,呼叫建構函式等。

下面舉一個方法呼叫的例子,在String文字後面呼叫concat方法。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

message的值現在就是 ‘Hello World!’.

接下去是一個訪問JavaBean屬性的例子,String類的Bytes屬性通過以下的方法呼叫:

// 呼叫'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();

SpEL同時也支援級聯屬性呼叫、和標準的prop1.prop2.prop3方式是一樣的;同樣屬性值設定也是類似的方式.

公共方法也可以被訪問到:

ExpressionParser parser = new SpelExpressionParser();

// 呼叫 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = (Integer) exp.getValue();

除了使用字串表示式、也可以呼叫String的建構函式:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
String message = exp.getValue(String.class);

針對泛型方法的使用,例如:public <T> T getValue(Class<T> desiredResultType) 使用這樣的方法不需要將表示式的值轉換成具體的結果型別。如果具體的值型別或者使用型別轉換器都無法轉成對應的型別、會拋EvaluationException的異常

SpEL中更常見的用途是提供一個針對特定物件例項(叫做根物件)求值的表示式字串。使用方法有兩種,
具體用哪一種要看每次呼叫表示式求值時相應的物件例項是否每次都會變化。接下來我們分別舉兩個例子說明,
第一個例子我們要做的是從Inventor類的例項中解析name屬性:

// 建立並設定一個calendar例項
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// 構造器引數有: name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");

EvaluationContext context = new StandardEvaluationContext(tesla);
String name = (String) exp.getValue(context);

最後一行,字串變數name將會被設定成”Nikola Tesla”. 通過StandardEvaluationContext類你能指定哪個物件的”name”會被求值這種機制用於當根物件不會被改變的場景,在求值上下文中只會被設定一次。相反,如果根物件會經常改變,則需要在每次呼叫getValue的時候被設定,就像如下的示例程式碼:

// 建立並設定一個calendar例項
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// 構造器引數有: name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
String name = (String) exp.getValue(tesla);

在上面這個例子中inventor類例項tesla直接在getValue中設定,表示式解析器底層框架會自動建立和管理一個預設的求值上下文-不需要被顯式宣告

StandardEvaluationContext建立相對比較耗資源,在重複多次使用的場景下內部會快取部分中間狀態加快後續的表示式求值效率。因此建議在使用過程中儘可能被快取和重用,而不是每次在表示式求值時都重新建立一個物件。

在很多使用場景下理想的方式是事先配置好求值上下文,但是在實際呼叫中仍然可以給getValue設定一個不同的根物件。getValue允許在同一個呼叫中同時指定兩者,在這種場景下執行時傳入的根物件會覆蓋在求值上下文中事先指定的根物件。

Note:在單獨使用SpEL時需要建立解析器、解析表示式、以及求值上下文和對應的根物件。但是在實際使用過程中、更常用的使用方式是隻需要在配置檔案裡面配置SpEL字串表示式即可,例如針對Spring Bean或者Spring Web Flow的定義。在這種場景下解析器,求值上下文,根物件和任何事先定義的變數都會被容器預設建立好,使用者除了寫表示式不需要做任何其他事情。

作為最後一個介紹性的例子,我們沿用上面的例子寫一個使用布林操作符的求值例子

Expression exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(context, Boolean.class); // 求值結果是True

6.3.1 EvaluationContext介面

EvaluationContext介面在求值表示式中需要解析屬性,方法,欄位的值以及型別轉換中會用到。其預設實現類StandardEvaluationContext使用反射機制來操作物件。為獲得更好的效能快取了java.lang.reflect.Method, java.lang.reflect.Field, 和java.lang.reflect.Constructor例項

StandardEvaluationContext中你可以使用setRootObject()方法顯式設定根物件,或通過構造器直接傳入根物件。你還可以通過呼叫setVariable()和registerFunction()方法指定在表示式中用到的變數和函式。變數和函式的使用在語言參考的 Variables和Functions兩章節有詳細說明。使用StandardEvaluationContext你還可以註冊自定義的構造器解析器(ConstructorResolvers),方法解析器(MethodResolvers),和屬性存取器(PropertyAccessor)來擴充套件SpEL如何計算表示式,詳見具體類的JavaDoc文件。

型別轉換

SpEL預設使用Spring核心程式碼中的conversion service來做型別轉換(org.springframework.core.convert.ConversionService)。這個類本身內建了很多常用的轉換器,同時也可以擴充套件使用自定義的型別轉換器。另外一個核心功能是它可以識別泛型。這意味著當在表示式中使用泛型型別時、SpEL會確保任何處理的物件的型別正確性。

實際應用中這意味著什麼?這裡舉一個例子、拿賦值來說,比如使用setValue來設定List屬性。屬性的型別實際上是List<Boolean>,SpEL可以識別List中的元素型別並轉換成Boolean型別。下面是示例程式碼:

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

Simple simple = new Simple();

simple.booleanList.add(true);

StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple);

// false is passed in here as a string. SpEL and the conversion service will
// correctly recognize that it needs to be a Boolean and convert it
parser.parseExpression("booleanList[0]").setValue(simpleContext, "false");

// b will be false
Boolean b = simple.booleanList.get(0);

6.3.2 解析器配置

可以通過使用一個解析器配置物件 (org.springframework.expression.spel.SpelParserConfiguration)來配置SpEL表示式解析器。這個配置物件可以控制一些表示式元件的行為。例如:陣列或者集合元素查詢的時候如果當前位置對應的物件是Null,可以通過事先配置來自動建立元素。這個在表示式是由多次屬性鏈式引用的時候比較重要。如果設定的陣列或者List位置越界時可以自動增加陣列或者List長度來相容.

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

SpEL表示式編譯器的行為也可以通過配置來實現

6.3.3 SpEL編譯

Spring4.1 包括了一個基本的表示式編譯器。表示式求值過程中往往可以利用動態解釋功能來提供很多靈活性,但是卻達不到理想的效能。當表示式不是被經常使用時是可以接受的,但是當被其他元件像Spring Integration使用時,效能往往更重要,這時動態性沒有太多的需求。

新的SpEL編譯器針對這樣的需求進行了定製。編譯器會在求值時同時建立一個真實的Java類包含表示式的行為,利用這種方式來達到更快的表示式求值速度。在編譯過程中、編譯器剛開始不知道表示式具體的型別、但它會在表示式求值過程中收集相應的資訊來確定最終的型別。例如,編譯器無法僅僅從表示式自身來分析出某個屬性引用的型別,但是在首次進行解釋求值時就可以確定了。當然,如果不同的表示式元素型別後續發生變化、基於這種資訊來編譯結果就不準確了。因此編譯最適合的場景是表示式在多次求值過程中型別資訊不會變化。

例如一個簡單的表示式如下:

someArray[0].someProperty.someOtherProperty < 0.1

這個表示式包含陣列訪問,屬性引用和數值運算,其效能開銷不可忽視。
在一次50000次迴圈的效能基準評測中,如果只用解析器需要耗時75毫秒,但如果使用已編譯的版本則只需要3毫秒

編譯器配置

編譯器模式不是預設開啟的,有兩種方法可以開啟。一種是前面已經提到過的解析器配置的時候,另一種是當SpEL整合到其他元件時通過設定系統屬性的方式開啟。本節會同時介紹兩種方式.

首先比較重要的是編譯器本身有幾種操作模式,都定義在列舉類(org.springframework.expression.spel.SpelCompilerMode)中。所有的模式如下:

OFF-編譯器關閉;預設是關閉的
IMMEDIATE-即時生效模式,表示式會盡快的被編譯。基本是在第一次求值後馬上就會執行。如果編譯表示式出錯(往往都是因為上面提到的型別發生改變的情況)則表示式求值的呼叫點會丟擲異常。
MIXED-混合模式,在混合模式中表達式會自動在直譯器模式和編譯器模式之間切換。在發生了幾次解釋處理後會切換到編譯模式,如果編譯模式哪裡出錯了(像上面提到的型別發生變化)則表示式會自動切換回直譯器模式。過一段時間如果運用正常又會切換回編譯模式。基本上像在IMMEDIATE模式下會丟擲的那些異常都會被內部處理掉。

即時生效模式之所以會存在是因為混合模式會帶來副作用。如果一個已編譯的表示式在部分執行成功後發生錯誤的話,有可能已經導致系統的狀態發生變化。這種場景下呼叫方並不希望表示式在解釋模式下重新執行一遍、因為這意味著部分表示式會被執行兩遍。

在選擇了一個模式後,使用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列舉值的其中一種(off, immediate, 或者 mixed)。

編譯器的侷限性

在Srping4.1中基礎的編譯框架就已經有了。但框架還不支援編譯所有型別的表示式。最初的設計初衷主要在於先集中優化比較耗效能且又經常使用的表示式。以下型別的表示式目前還不能被編譯:

涉及到賦值的表示式
依賴於轉換服務的表示式
使用到自定義解析器或者存取器的表示式
使用到選擇器或者投影的表示式

更多型別的表示式將來都會被支援編譯

6.4 Bean定義時使用表示式
無論XML還是註解型別的Bean定義都可以使用SpEL表示式。在兩種方式下定義的表示式語法都是一樣的,即:#{ }

6.4.1 XML型別的配置
Bean屬性或者建構函式使用表示式的方式如下:

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

你還可以通過Name注入的方式使用其他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>

6.4.2 基於註解的配置
@Value可以在屬性欄位,方法和構造器變數中使用,指定一個預設值。

下面的例子中給屬性欄位設定預設值:

public static class FieldValueTestBean

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

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

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

}

通過Set方法設定預設值:

public static class PropertyValueTestBean

	private String defaultLocale;

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

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

}

使用Autowired註解的方法和構造器也可以使用@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;

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

	// ...
}

6.5 語言參考
6.5.1 字元表示式
字串表示式型別支援strings,數值型別 (int, real, hex),布林和null.strings值通過單引號引用。如果字串裡面又包含字串,通過雙引號引用。

下面列出了字串表示式的常用例子。通常它們不會被單獨使用,而是結合一個更復雜的表示式一起使用,例如,在邏輯比較運算子中使用表示式:

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()解析。

6.5.2 屬性, 陣列, 列表, Maps, 索引

屬性引用比較簡單:只需要用點號(.)標識級聯的各個屬性值。Inventor類的例項:pupin,和tesla,所用到的資料在Classes used in the examples一節有列出。
下面的表示式示例用來解析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();

// Inventions Array
StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla);

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

// Members List
StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee);

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

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

Maps的值由方括號內指定字串的Key來標識引用。在下面這個例子中,因為Officers map的Key是string型別,我們可以用過字串常量指定。

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

6.5.3 內聯列表
列表(Lists)可以用過大括號{}直接引用

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

{}本身代表一個空list.因為效能的關係,如果列表本身完全由固定的常量值組成,這個時候會建立一個常量列表來代替表示式,而不是每次在求值的時候建立一個新的列表。

6.5.4 內聯Maps
Maps也可以直接通過{key:value}標記的方式在表示式中使用

// 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本身包含固定的常量或者其他級聯的常量結構(lists或者maps)則一個常量Map會建立來代表表示式,而不是每次求值的時候都建立一個新的Map.Map的Key並不一定需要引號引用、比如上面的例子就沒有引用。

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

在建立多維陣列時還不支援事先指定初始化的值。

6.5.6 方法

方法可以使用典型的Java程式設計語法來呼叫。方法可以直接在字串常量上呼叫。可變引數也是支援的。

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

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

6.5.7 運算子
關係運算符
關係運算符;包括==,<>,<,<=,>,>=等標準運算子都是可以直接支援的

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

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

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

Note:>/<運算子和null做比較時遵循一個簡單的規則:null代表什麼都沒有(不代表0).因此,所有的值總是大於null(X>null總是true)
也就是沒有一個值會小於什麼都沒有(x<null總是返回false).儘量不要在數值比較中使用null,而是和0做比較(例如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 (!). 這裡不區分大小寫。

邏輯運算子
支援的邏輯運算子有: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);

算術運算子
加號運算子可以同時用於數字和字串。減號,乘法和除法只能用於數字。其他支援的算術運算子有取模(%)和指數(^).遵循標準運算子優先順序。下面是一些例子:

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

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

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

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

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

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

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

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

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

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

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21

6.5.8 賦值

屬性可以使用賦值運算子設定。可以通過呼叫setValue設定。但是也可以在getValue方法中設定

Inventor inventor = new Inventor();
StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor);

parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2");

// alternatively

String aleks = parser.parseExpression(
		"Name = 'Alexandar Seovic'").getValue(inventorContext, String.class);

6.5.9 型別
T操作符是一個特殊的操作符、可以同於指定java.lang.Class的例項(型別)。靜態方法也可以通過這個操作符呼叫。
StandardEvaluationContext使用TypeLocator來查詢型別,其中StandardTypeLocator(這個可以被替換使用其他類)預設對java.lang包裡的型別可見。也就是說 T()引用java.lang包裡面的型別不需要限定包全名,但是其他型別的引用必須要。

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 &amp;lt; T(java.math.RoundingMode).FLOOR")
		.getValue(Boolean.class);

6.5.10 構造器
構造器可以使用new操作符來呼叫。除了元資料型別和String(比如int,float等可以直接使用)都需要限定類的全名。

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

6.5.11 變數
表示式中的變數可以通過語法#變數名使用。變數可以在StandardEvaluationContext中通過方法setVariable設定。

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context);

System.out.println(tesla.getName()) // "Mike Tesla"

#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();
StandardEvaluationContext context = new StandardEvaluationContext();
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);

6.5.12 函式
你可以擴充套件SpEL,在表示式字串中使用自定義函式。這些自定義函式是通過StandardEvaluationContext的registerFunction來註冊的

public void registerFunction(String name, Method m)

首先定義一個Java方法作為函式的實現、例如下面是一個將字串反轉的方法。

public abstract class StringUtils {

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

然後將這個方法註冊到求值上下文中就可以應用到表示式字串中。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

context.registerFunction("reverseString",
	StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class }));

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

6.5.13 Bean引用
如果求值上下文已設定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);

如果是訪問工廠Bean,bean名字前需要新增字首(&)符號

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

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

6.5.14 三元操作符 (If-Then-Else)
你可以在表示式中使用三元操作符來實現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'";

6.5.15 Elvis運算子
Elvis運算子可以簡化Java的三元操作符,是Groovy中使用的一種操作符。如果使用三元操作符語法你通常需要重複寫兩次變數名,例如:

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

使用Elvis運算子可以簡化寫法,這個符號的名字由來是它很像Elvis的髮型(譯者注:Elvis=Elvis Presley,貓王,著名搖滾歌手)

ExpressionParser parser = new SpelExpressionParser();

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

System.out.println(name); // 'Unknown'

下面是一個複雜一點的例子:

ExpressionParser parser = new SpelExpressionParser();

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

String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);

System.out.println(name); // Nikola Tesla

tesla.setName(null);

name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);

System.out.println(name); // Elvis Presley

6.5.16 安全引用運算子
安全引用運算子主要為了避免空指標,源於Groovy語言。很多時候你引用一個物件的方法或者屬性時都需要做非空校驗。為了避免此類問題、使用安全引用運算子只會返回null而不是丟擲一個異常。

ExpressionParser parser = new SpelExpressionParser();

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

StandardEvaluationContext context = new StandardEvaluationContext(tesla);

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

tesla.setPlaceOfBirth(null);

city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);

System.out.println(city); // null - does not throw NullPointerException!!!

備註:Elvis操作符可以在表示式中賦預設值,例如。在一個@Value表示式中:@Value(“#{systemProperties[‘pop3.port’] ?: 25}”)
上面的例子如果系統屬性pop3.port已定義會直接注入,如果未定義,則返回預設值25.

6.5.17 集合篩選

該功能是SpEL中一項強大的語言特性,允許你將源集合選擇其中的某幾項生成另外一個集合。選擇器使用語法.?[selectionExpression].通過該表示式可以過濾集合並返回原集合中的子集。例如,下面的例子我們返回inventors物件中的國籍為塞爾維亞的子集:

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

篩選可以同時在list和maps上面使用。對於list來說是選擇的標準是針對單個列表的每一項來比對求值,對於map來說選擇的標準是針對Map的每一項(型別為Java的Map.Entry)。Map項的Key和alue都可以作為篩選的比較選項

下面的例子中表達式會返回一個新的map,包含原map中值小於27的所有子項。

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

除了可以返回所有被選擇的元素,也可以只返回第一或者最後一項。返回第一項的選擇語法是:
^[…​],返回最後一項的選擇語法是 $[…​].
6.5.18 集合投影
投影使得一個集合通過子表示式求值,並返回一個新的結果。投影的語法是 ![projectionExpression]. 舉一個通俗易懂的例子,假設我們有一個inventors 物件列表,但是我們想返回每一個inventor出生的城市列表。我們需要遍歷inventor的每一項,通過 ‘placeOfBirth.city’來求值。下面是具體的程式碼例子:

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

也可以在Map上使用投影、在這種場景下投影表示式會作用於Map的每一項(型別為Java的Map.Entry)。Map項的Key和alue都可以作為選擇器的比較選項Map投影的結果是一個list,包含map每一項被投影表示式求值後的結果。
6.5.19 表示式模板
表示式模板執行在一段文字中混合包含一個或多個求值表示式模組。各個求值塊都通過可被自定義的前後綴字元分隔,一個通用的選擇是使用#{ }作為分隔符。例如:

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 number is’以及#{}分隔符中的表示式求值結果拼接起來的,在這個例子中就是呼叫random()的結果。方法parseExpression()的第二個傳入引數型別是ParserContext。ParserContext介面用來確定表示式該如何被解析、從而支援表示式的模板功能。其實現類TemplateParserContext的定義如下:

public class TemplateParserContext implements ParserContext {

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

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

	public boolean isTemplate() {
		return true;
	}
}

6.6 本章節例子中使用的類
Inventor.java

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

PlaceOfBirth.java

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

}

Society.java

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&amp;lt;Inventor&amp;gt; members = new ArrayList&amp;lt;Inventor&amp;gt;();
	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;
	}

}