天天看點

五分鐘學會Spring表達式語言SpEL,不但學會使用也知道底層原理?

作者:Java猿

#頭條群星9月榜#

五分鐘學會Spring表達式語言SpEL,不但學會使用也知道底層原理?

sprign 表達式

SpEL:Spring Expression Language,支援在運作時查詢和操作對象圖的一種強大的表達式語言。該語言的文法類似于Unified EL,但提供了額外的特性,最顯著的是方法調用和基本的字元串模闆功能。SpEL為Spring社群提供表達式語言的支援,但是并沒有和Spring綁定,可以單獨使用。

Spring 表達式中的幾個接口類

五分鐘學會Spring表達式語言SpEL,不但學會使用也知道底層原理?

幾個重要接口類

接口ExpressionParser類,這個接口主要用來解析表達式字元串,并且傳回一個表達式對象Expression,這個接口ExpressionParser有個安全并且可以重用的實作類SpelExpressionParser。

package org.springframework.expression;

public interface ExpressionParser {

	/**
	 * 解析表達式字元串并且傳回一個可重複擷取傳回值的表達式對象
	 */
	Expression parseExpression(String expressionString) throws ParseException;

	/**
	 *  解析表達式字元串并且傳回一個可重複擷取傳回值的表達式對象,這個方法傳遞一個Context對象
   * Context對象為表達式對象提供輸入
	 */
	Expression parseExpression(String expressionString, ParserContext context) throws ParseException;

}           

通過一個例子了解解析字元串字面量(string literal),比如表達式'Hello World',程式執行完輸出字元串Hello World,這個字元串需要使用單引号括起來,先看看測試程式,後續再了解字元串字面量的文法,代碼如下:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();
System.out.printf(message);
//輸出:Hello World           

如果不使用單引号,程式則抛出SpelParseException異常,從異常資訊看是解析完表達式後面還有資料導緻,主要是字元串字面量中有空格導緻,如果字元串改變成'Hello_World'則會正常解析,測試結果如下:

Expression exp = parser.parseExpression("Hello World");
//結果如下:
Exception in thread "main" org.springframework.expression.spel.SpelParseException: EL1041E: After parsing a valid expression, there is still more data in the expression: 'World'
	at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:141)
	at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:61)
	at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:33)
	at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:52)
	at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:43)
	at antlr4.TestSPEL.main(TestSPEL.java:12)           

再簡單分析類Expression,這個類會根據上下文對自身求值,并封裝了求值的公共方法。常用的方法是getValue(),這個方法有多個重載的版本,如果沒有傳參desiredResultType(預期的結果類型),怎需要對結果類型進行轉換,如果類型不能轉換則抛出SpelEvaluationException異常。

@Nullable
	Object getValue() throws EvaluationException;

	@Nullable
	<T> T getValue(@Nullable Class<T> desiredResultType) throws EvaluationException;

	@Nullable
	Object getValue(@Nullable Object rootObject) throws EvaluationException;

	@Nullable
	<T> T getValue(@Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException;

	@Nullable
	Object getValue(EvaluationContext context) throws EvaluationException;

	@Nullable
	Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException;

	@Nullable
	<T> T getValue(EvaluationContext context, @Nullable Class<T> desiredResultType) throws EvaluationException;

	@Nullable
	<T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType)
			throws EvaluationException;           

最後了解一下接口EvaluationContext,這個接口用來解析屬性、方法、字段,并幫助執行類型轉換。在類定義了很多get方法,例如擷取rootObject,MethodResolvers,BeanResolver等,并且有個setVariable方法,用于設定變量。這個接口的實作類有三個StandardEvaluationContext、SimpleEvaluationContext和MethodBasedEvaluationContext。

使用Spring的表達式接口來表達式求值

五分鐘學會Spring表達式語言SpEL,不但學會使用也知道底層原理?

表達式求值

1、字面量表達式,支類型包括字元串、數值(int、real、十六進制)、布爾值和null。字元串需要使用單引号括起來,如果字元中有單引号需要使用兩個單引号。下面是幾個例子:

// 這個Hello 'World中帶有一個單引号
String helloWorld = (String) parser.parseExpression("'Hello ''World'").getValue();
// 輸出:Hello 'World
double avogadrosNumber = (Double) parser.parseExpression("3.0221415E+23").getValue();
// 輸出:3.0221415E23
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
// 輸出:2147483647
boolean falseValue = (Boolean) parser.parseExpression("false").getValue();
//  輸出:false
 Object nullValue = parser.parseExpression("null").getValue();
// 輸出:null           

2、擷取對象屬性,數組,清單,映射,索引:首先定義一個測試類,類中有數組、清單和映射(Map)等

class SpringTestObject {
    public String[] item = new String[]{"蘋果", "香蕉", "梨", "西瓜"};
    public List<String> list = Arrays.asList("礦泉水", "雪碧", "可樂", "牛奶");
    public Map<String,String> map = new HashMap() {
        {
            put("man", "男");
            put("woman", "女");
            put("other", "中性");
        }
    };
    public String property = "測試類";
    public String getListItem(int index){
        return list.get(index);
    }
}           

首先測試擷取對象的屬性,測試前需要簡單介紹下rootObject,從名字了解就是根對象,Spring會從這個對象中讀取屬性或者調用對象的方法。

ExpressionParser parser = new SpelExpressionParser();
SpringTestObject rootObj = new SpringTestObject();
//調用方法getListItem參數是1
Object value = parser.parseExpression("getListItem(1)").getValue(rootObj);
System.out.println(""+value);
// 輸出結果:雪碧
String property = (String) parser.parseExpression("property").getValue(rootObj);
System.out.println(property);
// 輸出結果:測試類           

擷取數組中的值和設定新的值,設定使用setValue()方法,第一個參數是rootObject,第二個參數是新的值。

ExpressionParser parser = new SpelExpressionParser();
SpringTestObject rootObj = new SpringTestObject();
// 擷取item數組中的第二個值
String item = (String) parser.parseExpression("item[1]").getValue(rootObj);
 System.out.println(item);
// 輸出結果:香蕉
// 重新設定數組中第二個值為榴蓮
parser.parseExpression("item[1]").setValue(rootObj,"榴蓮");
System.out.println(rootObj.item[1]);
// 輸出:榴蓮           

清單,映射和數組是類似的就不單獨解釋了,示例如下:

ExpressionParser parser = new SpelExpressionParser();
SpringTestObject rootObj = new SpringTestObject();
// 擷取item清單中的第二個值
String list = (String) parser.parseExpression("list[1]").getValue(rootObj);
System.out.println(list);
// 輸出結果:雪碧
// 重新設定數組中第二個值為脈動
parser.parseExpression("list[1]").setValue(rootObj,"脈動");
System.out.println(""+rootObj.list.get(1));
// 輸出結果:脈動

String map = (String) parser.parseExpression("map['man']").getValue(rootObj);
System.out.println(map);
// 輸出結果:男
parser.parseExpression("map['man']").setValue(rootObj,"男人");
System.out.println(""+rootObj.map.get("man"));
// 輸出結果:男人           

3、建立數組、映射和清單:建立映射和清單使用大括号開始和結尾,映射使用key:value形式,清單使用逗号分隔,他們都可以嵌套。

//Map對象
Map map = (Map) parser.parseExpression("{'key':'value'}").getValue();
//清單
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue();
//嵌套清單
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue();
//一維數組
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue();
//二維數組
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue();           

4、方法調用:對于字元串可以直接調用字元串的方法,比如length()、size()方法等,對于rootObject可以直接調用它的方法。

ExpressionParser parser = new SpelExpressionParser();
SpringTestObject rootObject = new SpringTestObject();
Integer length = (Integer) parser.parseExpression("'hello'.length()").getValue();
 String property = (String) parser.parseExpression("getListItem(0)").getValue(rootObject);
           

另一種方法調用,注冊一個方法到StandardEvaluationContext上,registerFunction接受兩個參數,第一個是方法名,第二個是Method對象,這種方法調用支援靜态方法,表達式需要再前面加個#。

//首先把getListItem改成靜态方法
    public static String getListItem(int index){
        return list.get(index);
    }
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.registerFunction("getListItem",
        SpringTestObject.class.getDeclaredMethod("getListItem", new Class[] { int.class }));
String item = parser.parseExpression("#getListItem(1)").getValue(context,String.class);
           

5、關系運算、邏輯運算和算術運算:關系運算符有lt ('<'), gt ('>'), le ('<='), ge ('>='), eq ('=='), ne ('!='), div ('/'), mod ('%'), not ('!');邏輯運算符有and, or和not;算術運算有加減乘除和求餘等。

ExpressionParser parser = new SpelExpressionParser();
Boolean falseValue = parser.parseExpression("1 > 3").getValue(Boolean.class);
Boolean trueValue1 = parser.parseExpression("10 gt null and 6 < 10").getValue(Boolean.class);
Integer intValue = parser.parseExpression("10 - 2").getValue(Integer.class);
//正規表達式
boolean trueValue = parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?#39;").getValue(Boolean.class);
           

對于和'null'比較,null代表設麼也沒有,其他和null大于比較總傳回true。

6、擷取類型和調用構造方法:擷取類型使用T操作符,這個操作符也可以調用靜态方法。對于在java.lang包中的類可以忽略包名,比如T(int),對于其他包需要使用全名T(java.util.Date)。

ExpressionParser parser = new SpelExpressionParser();
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
// 輸出:class java.lang.String
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
// 輸出:class java.util.Date
String strValue = parser.parseExpression("T(antlr4.SpringTestObject).getListItem(1)").getValue(String.class);
//輸出:雪碧
String hw = parser.parseExpression("new String('Hello World')").getValue(String.class);
//輸出:Hello World           

7、Bean引用:需要在StandardEvaluationContext上設定BeanResolver的執行個體,BeanResolver意識就是從什麼地方擷取bean,在Spring項目上可以注入BeanFactory,在非Spring項目中可以自定義一個BeanResolver的實作。表達式中使用@字元擷取bean。以下是一個Spring項目中擷取bean的方式。

StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
List list = parser.parseExpression("@shoppingCart.getItems()").getValue(context,List.class);
           

自定義MyBeanResolver實作接口BeanResolver,并實作resolve方法。

class MyBeanResolver implements BeanResolver{
    Map<String,Object> beans = new HashMap(){{
        put("test",new SpringTestObject());
    }};
    @Override
    public Object resolve(EvaluationContext context, String beanName) throws AccessException {
        return beans.get(beanName);
    }
}

//使用自定義的MyBeanResolver
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
String invokeBeanMethod = parser.parseExpression("@test.getListItem(2)").getValue(context, String.class);
 // 輸出:可樂           

8、三元運算符和Elvis運算符:學過程式設計的都熟悉三元運算符,因為在很多語言中都存在,三元運算符使用類似if-then-else條件邏輯計算,在表達式中類似這樣的"true ? 'trueExp' : 'falseExp'"。

Boolean value = parser.parseExpression("3 gt 1? 'true':'false'").getValue(Boolean.class);
//輸出:true           

Elvis運算符在Groovy中使用,為了簡化三元運算符,文法為'notnull'?:null

String name = parser.parseExpression("'notnull'?:'null'").getValue(String.class);
//輸出:notnull           

9、安全的導航操作符:從一個對象中擷取另一個對象并使用它屬性的時候,為了防止NullPointerException會對每個對象判空,在Groovy語言中可以使用安全的導航避免空指針,文法如下:obj1?.obj2?.field。在Spring表達式中可以使用類似的文法。

StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
String invokeBeanMethod = parser.parseExpression("@notTest?.getListItem(2)").getValue(context, String.class);
// notTest并不存在,結果為null,并不會抛出SpelEvaluationException           

10、集合選擇:從集合中選擇一部分資料是比較常用的,在Java中可以循環對集合每個元素處理,在Spring 表達式中可以使用文法?[selectionExpression]擷取全部符合條件的資料,使用^[selectionExpression]擷取第一個元素,使用$[selectionExpression]擷取最後一個元素。

ExpressionParser parser = new SpelExpressionParser();
List list  = parser.parseExpression("{1,2,3,4,5,6,7,8,9}.?[#this>3]").getValue(List.class);
// [4, 5, 6, 7, 8, 9]
List firstlist  = parser.parseExpression("{1,2,3,4,5,6,7,8,9}.^[#this>3]").getValue(List.class);
// [4]
List lasttlist  = parser.parseExpression("{1,2,3,4,5,6,7,8,9}.$[#this>3]").getValue(List.class);
// [9]           

11、表達式模闆:在一個字元串中可以使用#{ }在文本中使用表達式,在解析的方法中需要傳遞TemplateParserContext。

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

Spring SpEL的原理

在spring-expression-5.3.26.jar包中有個SpringExpresions.g檔案,這個檔案就是SpEL的文法檔案,通過文法檔案解析輸入的表達式生成文法樹,在文法正确的前提下計算表達式的值。

五分鐘學會Spring表達式語言SpEL,不但學會使用也知道底層原理?

spring的文法檔案

在類InternalSpelExpressionParser中處理輸入的表達式,處理成TokenStream,最後生成一顆編譯的文法樹。

五分鐘學會Spring表達式語言SpEL,不但學會使用也知道底層原理?

解析表達式