主頁 > 後端開發 > Spring系列19:SpEL詳解

Spring系列19:SpEL詳解

2022-02-23 06:48:27 後端開發

本文內容

  1. SpEL概念

  2. 快速入門

  3. 關鍵介面

  4. 全面用法

  5. bean定義中使用

SpEL概念

Spring 運算式語言(簡稱“SpEL”)是一種強大的運算式語言,支持在運行時查詢和操作物件圖,語言語法類似于 Unified EL,但提供了額外的功能,最值得注意的是方法呼叫和基本的字串模板功能,

雖然 SpEL 是 Spring 產品組合中運算式評估的基礎,但它不直接與 Spring 系結,可以獨立使用,

運算式語言支持以下功能:

  • 字面運算式
  • 布爾和關系運算子
  • 正則運算式
  • 類運算式
  • 訪問屬性、陣列、串列和映射
  • 方法呼叫
  • 關系運算子
  • 呼叫建構式
  • bean參考
  • 陣列構造
  • 行內的list
  • 行內的map
  • 三元運算子
  • 變數
  • 用戶自定義函式
  • 集合選擇
  • 模板化運算式

快速入門

通過幾個案例快速體驗SpEL運算式的使用,

案例1 Hello World

純字面意義的字串輸出,體驗使用的基本步驟,

    @Test
    public void test_hello() {
        // 1 定義決議器
        SpelExpressionParser parser = new SpelExpressionParser();
        // 2 使用決議器決議運算式
        Expression exp = parser.parseExpression("'Hello World'");
        // 3 獲取決議結果
        String value = https://www.cnblogs.com/kongbubihai/p/(String) exp.getValue();
        System.out.println(value);
    }
    
    // 結果 
    Hello World

案例2 字串方法的字面呼叫

在運算式中呼叫字串的普通方法和構造方法,

    @Test
    public void test_String_method() {
        // 1 定義決議器
        SpelExpressionParser parser = new SpelExpressionParser();
        // 2 使用決議器決議運算式
        Expression exp = parser.parseExpression("'Hello World'.concat('!')");
        // 3 獲取決議結果
        String value = https://www.cnblogs.com/kongbubihai/p/(String) exp.getValue();
        System.out.println(value);
        exp = parser.parseExpression("'Hello World'.bytes");
        byte[] bytes = (byte[]) exp.getValue();
        exp = parser.parseExpression("'Hello World'.bytes.length");
        int length = (Integer) exp.getValue();
        System.out.println("length: " + length);

        //  呼叫
        exp = parser.parseExpression("new String('hello world').toUpperCase()");
        System.out.println("大寫: " + exp.getValue());

    }

// 結果
Hello World!
length: 11
大寫: HELLO WORLD

案例3 針對特定物件決議運算式

SpEL 更常見的用法是提供針對特定物件實體(稱為根物件)進行評估的運算式字串,案例演示如何從 Inventor 類的實體中檢索名稱屬性或創建布爾條件,

Inventor相關類定義如下

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;
    // 省略其它方法
}
public class PlaceOfBirth {

    private String city;
    private String country;
     // 省略其它方法
}

運算式決議測驗

@Test
public void test_over_root() {
    // 創建  Inventor 物件
    GregorianCalendar c = new GregorianCalendar();
    c.set(1856, 7, 9);
    Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
    // 1 定義決議器
    ExpressionParser parser = new SpelExpressionParser();
    // 指定運算式
    Expression exp = parser.parseExpression("name");
    // 在 tesla物件上決議
    String name = (String) exp.getValue(tesla);
    System.out.println(name); // Nikola Tesla

    exp = parser.parseExpression("name == 'Nikola Tesla'");
    // 在 tesla物件上決議并指定回傳結果
    boolean result = exp.getValue(tesla, Boolean.class);
    System.out.println(result); // true
}

執行程序分析和關鍵介面

執行程序分析

上面的案例中SpEL運算式的使用步驟中涉及了幾個概念和介面:

  1. 用戶運算式:我們定義的運算式,如1+1!=2
  2. 決議器:ExpressionParser 介面,負責將用戶運算式決議成SpEL認識的運算式物件
  3. 運算式物件:Expression介面,SpEL的核心,運算式語言都是圍繞運算式進行的
  4. 評估背景關系:EvaluationContext 介面,表示當前運算式物件操作的物件,運算式的評估計算是在背景關系上進行的,

通過下面的簡單案例debug分析執行程序,

    @Test
    public void test_debug(){
        SpelExpressionParser parser = new SpelExpressionParser();
        SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        Boolean value = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("1+1!=2").getValue(context, Boolean.class);
        System.out.println(value);
    }

原始碼debug如下,分2大階段,建議自行debug一次:

決議階段:InternalSpelExpressionParser#doParseExpression() 無關原始碼已經洗掉

// 用戶提供的運算式1+1!=2
private String expressionString = "";

// 分詞流
private List<Token> tokenStream = Collections.emptyList();

@Override
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
      throws ParseException {

   try {
      // 1 讀取到用戶的運算式 1+1!=2
      this.expressionString = expressionString;
      // 2.1 定義分詞器Tokenizer
      Tokenizer tokenizer = new Tokenizer(expressionString);
      // 2.2 分詞器將字串拆分為分詞流
      this.tokenStream = tokenizer.process();
      this.tokenStreamLength = this.tokenStream.size();
      this.tokenStreamPointer = 0;
      this.constructedNodes.clear();
      // 3 將分詞流決議成抽象語法樹 表示為SpelNode介面
      SpelNodeImpl ast = eatExpression();
      Assert.state(ast != null, "No node");
	  // 4、將抽象語法樹包裝成 Expression 運算式物件
      return new SpelExpression(expressionString, ast, this.configuration);
   }
   catch (InternalParseException ex) {
      throw ex.getCause();
   }
}

評估求值階段:SpelExpression#getValue(),無關原始碼已經洗掉

// 決議階段生成的抽象語法樹物件 SpelNodeImpl
private final SpelNodeImpl ast;

	public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType) throws EvaluationException {
		Assert.notNull(context, "EvaluationContext is required");
		// ...
		
        // 6.1 應用活動背景關系和決議器的配置
		ExpressionState expressionState = new ExpressionState(context, this.configuration);
        // 6.2 在上下中抽象語法樹進行評估求值
		TypedValue typedResultValue = https://www.cnblogs.com/kongbubihai/p/this.ast.getTypedValue(expressionState);
		checkCompile(expressionState);
        // 6.3 將結果進行型別轉換
		return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType);
	}	

方便理解,流程圖如下圖:

image-20220128150451306

匯總下執行程序:

  1. 決議器 SpelExpressionParser 讀取用戶提供的運算式1+1!=2
  2. 詞法分析:決議器 SpelExpressionParser 使用分詞器拆分用戶字串運算式成分詞流
  3. 語法分析:決議器 SpelExpressionParser 將分詞流生成內部的抽象語法樹
  4. 包裝運算式:對外提供Expression介面來簡化表示抽象語法樹,從而隱藏內部實作細節,并提供getValue簡單方法用于獲取運算式
  5. 用戶提供運算式背景關系物件(非必須),SpEL使用EvaluationContext介面表示背景關系物件,用于設定根物件、自定義變數、自定義函式、型別轉換器等
  6. 在運算式背景關系中呼叫內部抽象語法樹進行評估求值并轉換結果型別到目標型別,

ExpressionParser 介面

ExpressionParser 介面將運算式字串決議為可以計算的編譯運算式,支持決議模板以及標準運算式字串,

關鍵方法parseExpressio(),在決議失敗時拋出 ParseException 例外,

public interface ExpressionParser {

	// 決議運算式字串并回傳一個可用于重復評估的運算式物件,
	Expression parseExpression(String expressionString) throws ParseException;

	// 決議運算式字串并回傳一個可用于重復評估的運算式物件, 指定決議評估背景關系
	Expression parseExpression(String expressionString, ParserContext context) throws ParseException;

}

實作類 TemplateAwareExpressionParser 增加了對模板的決議支持,

常用的實作類 SpelExpressionParser 增加了 SpelParserConfiguration 決議器配置,實體是可重用和執行緒安全的,

image-20220127134850417

Expression 介面

image-20220127140527026

Expression 指能夠根據背景關系物件評估自身的運算式,封裝先前決議的運算式字串的詳細資訊,為運算式求值提供通用抽象,

關鍵方法如下:

getValue()在決議計算失敗會拋出 EvaluationException 例外

public interface Expression {
    // 獲取原始運算式
    String getExpressionString();
    
    // 獲取運算式計算值 默認背景關系 默認型別
    Object getValue() throws EvaluationException;
    
    // 獲取運算式計算值 指定背景關系、根物件、期望回傳值型別 
	<T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType)
			throws EvaluationException;
    // 在提供的背景關系中將此運算式設定為提供的值,
    void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException;
}

SpelExpression 介面表示已準備好在指定背景關系中評估的已決議(有效)運算式,運算式可以獨立評估,也可以在指定的背景關系中評估,在運算式評估期間,可能會要求背景關系決議對型別、bean、屬性和方法的參考,

ParserContext 介面

ParserContext介面代表提供給運算式決議器的輸入,可以影響運算式決議和編譯,

原始碼如下:

public interface ParserContext {

   // 是否是模板
   boolean isTemplate();

   // 模板運算式的前綴
   String getExpressionPrefix();

   //  模板運算式的后綴
   String getExpressionSuffix();

   // 啟用模板運算式決議模式的默認 ParserContext 實作,運算式前綴是“#{”,運算式后綴是“}”,
   ParserContext TEMPLATE_EXPRESSION = new ParserContext() {

      @Override
      public boolean isTemplate() {
         return true;
      }

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

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

}

EvaluationContext 介面

EvaluationContext 介面評估運算式以決議屬性、方法或欄位并幫助執行型別轉換,運算式是在在評估背景關系中執行的,遇到參考時使用背景關系來決議,

image-20220127144807458

原始碼及關鍵方法如下:

public interface EvaluationContext {

   // 回傳默認的根背景關系物件,可以在評估運算式時被覆
   TypedValue getRootObject();

   // 回傳訪問器串列用于屬性的讀寫訪問
   List<PropertyAccessor> getPropertyAccessors();

   // 回傳決議器串列用于定位建構式,
   List<ConstructorResolver> getConstructorResolvers();

   // 回傳方法決議器以查找方法
   List<MethodResolver> getMethodResolvers();

   // 回傳 bean決議器以通過名稱查找bean
   @Nullable
   BeanResolver getBeanResolver();

   // 回傳型別定位器用于查找型別,支持簡單型別名稱和全程
   TypeLocator getTypeLocator();

   // 回傳型別轉換器用于型別轉換
   TypeConverter getTypeConverter();

   // 回傳一個型別比較器,用于比較物件對是否相等
   TypeComparator getTypeComparator();

   // 回傳一個運算子多載器,該運算子多載器可能支持多個標準型別集之間的數學操作,
   OperatorOverloader getOperatorOverloader();

   // 將此評估背景關系中的命名變數設定為指定值,
   void setVariable(String name, @Nullable Object value);

   // 在此求值背景關系中查找指定變數,
   @Nullable
   Object lookupVariable(String name);

}

Spring 中提供了2個實作類:

  • StandardEvaluationContext

    公開全套 SpEL 語言功能和配置選項,可以使用它來指定默認根物件并配置每個可用的評估相關策略,功能強大且高度可配置,此背景關系使用所有適用策略的標準實作,基于反射來決議屬性、方法和欄位,

  • SimpleEvaluationContext

    側重于基本 SpEL 功能和自定義選項的子集,針對簡單的條件評估和特定的資料系結場景,

    SimpleEvaluationContext 旨在僅支持 SpEL 語言語法的子集,它不包括 Java 型別參考、建構式和 bean 參考,要求明確選擇對運算式中的屬性和方法的支持級別,默認情況下,create() 靜態工廠方法只允許對屬性進行讀取訪問,獲取構建器以配置所需的確切支持級別,針對以下一項或某種組合:

    • 僅自定義 PropertyAccessor(無反射)
    • 只讀訪問的資料系結屬性
    • 用于讀取和寫入的資料系結屬性

SpEl用法一網打盡

基本字面運算式

支持的文字運算式型別是字串、數值(int、real、hex)、boolean 和 null,字串由單引號分隔,要將單引號本身放在字串中,使用兩個單引號字符,數字支持使用負號、指數表示法和小數點,默認情況下,使用 Double.parseDouble() 決議實數,

    @Test
    public void test_literal() {
        ExpressionParser parser = new SpelExpressionParser();

        // 字串 "Hello World"
        String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
        System.out.println(helloWorld);

        double num = (Double) parser.parseExpression("6.0221415E+23").getValue();
        System.out.println(num);

        // int  2147483647
        int maxValue = https://www.cnblogs.com/kongbubihai/p/(Integer) parser.parseExpression("0x7FFFFFFF").getValue();
        System.out.println(maxValue);
        
        // 負數
        System.out.println((Integer) parser.parseExpression("-100").getValue());

        // boolean
        boolean trueValue = https://www.cnblogs.com/kongbubihai/p/(Boolean) parser.parseExpression("true").getValue();
        System.out.println(trueValue);

        // null
        Object nullValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("null").getValue();
        System.out.println(nullValue);
    }
// 結果
Hello World
6.0221415E23
2147483647
-100
true
null

屬性、陣列、串列、Map

屬性: 指定屬性名,通過"."支持多級嵌套,

陣列:[index] 方式

串列:[index] 方式

Map:['key'] 方式

直接看案例,

通用的物件,后面案例通用

public class SpELTest2 {
    // 決議器
    SpelExpressionParser parser;
    // 評估背景關系
    SimpleEvaluationContext context;

    @Before
    public void before() {
        parser = new SpelExpressionParser();
        context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    }
}
    /**
     * 屬性 陣列 串列 map 索引
     */
    @Test
    public void test2(){
        Inventor inventor = new Inventor("發明家1", "中國");
        // 發明作品陣列
        inventor.setInventions(new String[] {"發明1","發明2","發明3","發明4"});

        // 1 屬性
        String name = parser.parseExpression("name").getValue(context, inventor, String.class);
        System.out.println("屬性: " + name);
        // 屬性: 發明家1

        // 2 陣列運算式
        String invention = parser.parseExpression("inventions[3]").getValue(context, inventor, String.class);
        System.out.println("陣列運算式: " + invention);
        // 陣列運算式: 發明4

        // 3 List
        List strList = Arrays.asList("str1", "str2", "str3");
        String str = parser.parseExpression("[0]").getValue(context, strList, String.class);
        System.out.println(str);
        // str1

        // 4 map
        Map map = new HashMap<String, String>();
        map.put("xxx", "ooo");
        map.put("xoo", "oxx");
        String value = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("['xoo']").getValue(context, map, String.class);
        System.out.println(value);
        // oxx
    }

行內List

使用 {} 表示法直接在運算式中表示串列

// 行內List
@Test
public void test3() {
    List numbers = (List) parser.parseExpression("{1,3,5,7}").getValue(context);
    System.out.println(numbers);
    //[1, 3, 5, 7]
    List listOfList = (List) parser.parseExpression("{{1,3,5,7},{0,2,4,6}}").getValue(context);
    System.out.println(listOfList);
    // [[1, 3, 5, 7], [0, 2, 4, 6]]
}

行內Map

使用 {key:value} 表示法直接在運算式中表示映射

/**
 * 4 行內Map
 */
@Test
public void test4(){
    Map<String, Object> infoMap =
            (Map<String, Object>) parser.parseExpression("{'name':'name', password:'111'}").getValue();
    System.out.println(infoMap);
    //{name=name, password=111}
    
    Map mapOfMap =
            (Map) parser.parseExpression("{name:{first:'xxx', last:'ooo'}, password:'111'}").getValue(context);
    System.out.println(mapOfMap);
    // {name={first=xxx, last=ooo}, password=111}
}

集合選擇

選擇是一種強大的運算式語言功能,通過從其元素中進行選擇將源集合轉換為另一個集合,

Map 篩選的元素是 Map.Entry,可以使用 keyvalue 來篩選,

3種用法:

  • 從集合按條件篩選生成新集合:.?[selectionExpression]
  • 從集合按條件篩選后取第一個元素:.?[selectionExpression]
  • 從集合按條件篩選后取最后一個元素:.?[selectionExpression]
/**
 * 集合選擇
 */
@Test
public void test15(){
    Society society = new Society();
    // 發明者串列
    for (int i = 0; i < 5; i++) {
        Inventor inventor = new Inventor("發明家" + i, i % 2 == 0 ? "中國" : "外國");
        society.getMembers().add(inventor);
    }
    // 1、 List 篩選 .?[selectionExpression]
    List<Inventor> list = (List<Inventor>) parser.parseExpression("members.?[nationality == '中國']").getValue(society);
    list.forEach(item -> {
        System.out.println(item.getName() + " : " + item.getNationality());
    });
    // 發明家0 : 中國
    // 發明家2 : 中國
    // 發明家4 : 中國

    // 2、 List  取第一個.^[selectionExpression]  取最后一個.$[selectionExpression]
    Inventor first = parser.parseExpression("members.^[nationality == '中國']").getValue(society, Inventor.class);
    Inventor last = parser.parseExpression("members.$[nationality == '中國']").getValue(society, Inventor.class);
    System.out.println(first.getName() + " : " + first.getNationality());// 發明家0 : 中國
    System.out.println(last.getName() + " : " + last.getNationality()); // 發明家4 : 中國

    // 3 Map 篩選維度是 Map.Entry,其鍵和值可作為用于選擇的屬性訪問
    society.getOfficers().put("1", 100);
    society.getOfficers().put("2", 200);
    society.getOfficers().put("3", 300);
    Map mapNew = (Map) parser.parseExpression("officers.?[value>100]").getValue(society);
    System.out.println(mapNew); // {2=200, 3=300}
}

集合映射

一個集合通過映射的方式轉換成新的集合,如從 Map 映射成 List,語法是: .![projectionExpression]

/**
 * 集合映射
 */
@Test
public void test16(){
    Society society = new Society();
    // 發明者串列
    for (int i = 0; i < 5; i++) {
        Inventor inventor = new Inventor("發明家" + i, i % 2 == 0 ? "中國" : "外國");
        society.getMembers().add(inventor);
    }
    // 1、 List<Inventor> 映射到 List<String> 只要name
    List<String> nameList = (List<String>) parser.parseExpression("members.![name]").getValue(society);
    System.out.println(nameList); // [發明家0, 發明家1, 發明家2, 發明家3, 發明家4]

    // 2 Map映射到List
    society.getOfficers().put("1", 100);
    society.getOfficers().put("2", 200);
    society.getOfficers().put("3", 300);
    List<String> kvList= (List<String>) parser.parseExpression("officers.![key + '-' + value]").getValue(society);
    System.out.println(kvList); // [1-100, 2-200, 3-300]
}

陣列定義

直接使用 new 方式 ,注意: 多維陣列不可以初始化,

/**
 * 陣列生成
 */
@Test
public void test5(){
    int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

    // 一維陣列可以初始化
    int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

    // 多維陣列不可以初始化
    int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
}

關系運算子

  1. 使用標準運算子表示法支持關系運算子(等于、不等于、小于、小于或等于、大于和大于或等于)和等價的英文字符縮寫表示,

    標準符號 等價英文縮寫
    < lt
    > gt
    <= le
    >= ge
    == eq
    != ne
    / div
    % mod
    ! not

    注意特殊的 null比任何比較都小,所以 -1 < nullfalse0 > nullfalse,如果數字比較使用0代替null更好,

  2. 支持 instanceof

    小心原始型別,因為它們會立即裝箱到包裝器型別,因此 1 instanceof T(int) 的計算結果為 false,而 1 instanceof T(Integer) 的計算結果為 true

  3. 通過 matches 支持正則運算式

/**
 * 關系運算子
 */
@Test
public void test() {
    // true
    boolean trueValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("2 == 2").getValue(Boolean.class);
    // false
    boolean falseValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("2 < -5.0").getValue(Boolean.class);
    // false
    boolean falseValue2 = parser.parseExpression("2 gt -5.0").getValue(Boolean.class);
    // true
    boolean trueValue2 = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

    // null 比任何比較數小
    // true
    Boolean value = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("100 > null").getValue(boolean.class);
    // false
    Boolean value2 = parser.parseExpression("-1 < null").getValue(boolean.class);
    System.out.println(value);
    System.out.println(value2);


    // instanceof 支持
    // false
    Boolean aBoolean = parser.parseExpression("'xxx' instanceof T(Integer)").getValue(Boolean.class);
    System.out.println(aBoolean);

    // 支持正則運算式 matches
    // true
    Boolean match = parser.parseExpression(
            "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
    // false
    Boolean notMatch = parser.parseExpression(
            "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
    System.out.println(match);
    System.out.println(notMatch);
}

邏輯運算子

支持標準符號和英文字符縮寫:

  • and (&&)
  • or (||)
  • not (!)
/**
 * 邏輯運算子
 */
@Test
public void test8() {
    Society societyContext = new Society();

    // -- AND --
    // false
    boolean falseValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("true and false").getValue(Boolean.class);
    // true
    String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
    boolean trueValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression(expression).getValue(societyContext, Boolean.class);

    // -- OR --

    // true
    boolean trueValue2 = parser.parseExpression("true or false").getValue(Boolean.class);
    // true
    expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
    boolean trueValue3 = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

    // -- NOT --

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

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

}

數學運算子

可以對數字和字串使用加法運算子,字串只支持"+",

/**
 * 數學運算子
 */
@Test
public void test9(){
    // 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 value = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1

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

}

賦值運算子

賦值運算子 =用于設定屬性,通常在對 setValue 的呼叫中完成,但也可以在對 getValue 的呼叫中完成

/**
 * 賦值操作
 */
@Test
public void test(){
    Inventor inventor = new Inventor();
    EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
    // setValue 中
    parser.parseExpression("Name").setValue(context, inventor, "xxx");

    //  等價于在 getValue 賦值
    String name = parser.parseExpression(
            "Name = 'xxx'").getValue(context, inventor, String.class);

    System.out.println(name); // xxx
}

三目運算與 Elvis 運算子

三元運算子表示執行 if-then-else 條件邏輯,

parser.parseExpression("name != null ? name : 'null name'").

使用三目運算子語法,通常必須重復一個變數兩次如上面的nameElvis 運算子是三元運算子語法的縮寫,借鑒了Groovy 語言的語法,

parser.parseExpression("name?:'null name'")

為什么?:Elvis 運算子 ? 之前挺納悶的,后來發現是與美國搖滾歌星Elvis (貓王)的發型相似而得名,

案例如下:

/**
 *  三目運算和簡化
 */
@Test
public void test17(){
    Inventor inventor = new Inventor("not null name", "");
    String name = (String) parser.parseExpression("name != null ? name : 'null name'").getValue(inventor);
    System.out.println("三目:" + name);

    // 使用 Elvis運算子
    name = (String) parser.parseExpression("name?:'null name'").getValue(inventor);
    System.out.println("Elvis運算子:" + name);
}

嵌套屬性安全訪問?.

多級屬性訪問如國家城市城鎮nation.city.town三級訪問,如果中間的 citynull則會拋出 NullPointerException 例外,為了避免這種情況的例外,SpEL借鑒了Groovy的語法?.,如果中間屬性為null不會拋出例外而是回傳null

/**
 * 多級屬性安全訪問
 */
@Test
public void test18(){
    Inventor inventor = new Inventor("xx", "oo");
    inventor.setPlaceOfBirth(new PlaceOfBirth("北京", "中國"));

    // 正常訪問
    String city = parser.parseExpression("PlaceOfBirth?.city").getValue(context, inventor, String.class);
    System.out.println(city); // 北京

    // placeOfBirth為null
    inventor.setPlaceOfBirth(null);
    String city1 = parser.parseExpression("PlaceOfBirth?.city").getValue(context, inventor, String.class);
    System.out.println(city1); // null

    // 非安全訪問 例外
    String city3 = parser.parseExpression("PlaceOfBirth.city").getValue(context, inventor, String.class);
    System.out.println(city3); // 拋出例外
}

方法呼叫

使用典型的 Java 編程語法來呼叫方法,可以在字面上呼叫方法,還支持可變引數,

    /**
     * 方法呼叫
     */
    @Test
    public void test6(){
        String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
        System.out.println(bc);
        // bc
        
        Society societyContext = new Society();
        // 傳遞引數
        boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
                societyContext, Boolean.class);
        System.out.println(isMember);
        // false
    }

構造方法new

使用 new 運算子呼叫建構式,對除原始型別(intfloat 等)和 String 之外的所有型別使用完全限定的類名,

/**
 * new 呼叫構造方法
 */
@Test
public void test12(){
    Inventor value =
            https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("new com.crab.spring.ioc.demo20.Inventor('ooo','xxx')").getValue(Inventor.class);
    System.out.println(value.getName() + " " + value.getNationality()); // ooo xxx

    String value1 = parser.parseExpression("new String('xxxxoo')").getValue(String.class);
    System.out.println(value1); // xxxxoo
}

型別別T

使用特殊的 T 運算子指定 java.lang.Class 的實體(型別),

類中的靜態變數、靜態方法屬于Class, 可以通過T(xxx).xxx呼叫,

@Test
public void test11(){
    // 1 獲取類的Class java.lang包下的類可以不指定全路徑
    Class value = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("T(String)").getValue(Class.class);
    System.out.println(value);

    // 2 獲取類的Class 非java.lang包下的類必須指定全路徑
    Class dateValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("T(java.util.Date)").getValue(Class.class);
    System.out.println(dateValue);

    // 3 類中的靜態變數 靜態方法屬于Class 通過T(xxx)呼叫
    boolean trueValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
            .getValue(Boolean.class); // true
    System.out.println(trueValue);
    Long longValue = https://www.cnblogs.com/kongbubihai/p/parser.parseExpression("T(Long).parseLong('9999')").getValue(Long.class);
    System.out.println(longValue);// 9999
}

運算式模板 #{}

運算式模板允許將文字文本與一個或多個評估塊混合,每個評估塊都由前綴和后綴字符分隔,默認是#{ },支持實作介面ParserContext自定義前后綴,

呼叫parseExpression()時指定 ParserContext引數如new TemplateParserContext()

/**
 * 運算式模板 #{}
 */
@Test
public void test19() {
    String randomStr = parser.parseExpression("亂數字是: #{T(java.lang.Math).random()}", new TemplateParserContext())
            .getValue(String.class);
    System.out.println(randomStr);
}

定義和使用變數

可以使用#variableName 語法來參考運算式中的變數,通過在 EvaluationContext 實作上使用 setVariable 方法設定變數,

/**
 * 變數 #
 */
@Test
public void test13() {
    Inventor inventor = new Inventor("xxx", "xxx");
    SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
    context.setVariable("newName", "new ooo");
    // 使用預先的變數賦值 Name 屬性
    parser.parseExpression("Name = #newName").getValue(context, inventor);
    System.out.println(inventor.getName()); // new ooo
}
#this#root 變數

#this 變數始終被定義并參考當前評估物件(針對那些非限定參考被決議),

#root 變數始終被定義并參考根背景關系物件,

注冊和使用自定義方法

函式可以當做一種變數來注冊和使用的,2種方式注冊:

  • 按變數設定方式 EvaluationContext#setVariable(String name, @Nullable Object value)
  • 按明確的方法設定方式 StandardEvaluationContext#public void registerFunction(String name, Method method) ,其實底下也是按照變數處理,
/**
 * 方法注冊和使用
 */
@Test
public void test20() throws NoSuchMethodException {
    // 注冊 org.springframework.util.StringUtils.startsWithIgnoreCase(String str,String prefix)
    Method method = StringUtils.class.getDeclaredMethod("startsWithIgnoreCase",String.class,String.class);

    // 方式1 變數方式
    SimpleEvaluationContext simpleEvaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    simpleEvaluationContext.setVariable("startsWithIgnoreCase" ,method);
    Boolean startWith = parser.parseExpression("#startsWithIgnoreCase('123', '111')").getValue(simpleEvaluationContext,
            Boolean.class);
    System.out.println("方式1: " + startWith);

    // 方式2 明確方法方式
    StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext();
    standardEvaluationContext.registerFunction("startsWithIgnoreCase" ,method);
    Boolean startWit2 =
            parser.parseExpression("#startsWithIgnoreCase('123', '111')").getValue(simpleEvaluationContext,
            Boolean.class);
    System.out.println("方式2: " + startWit2);
}

bean參考

如果評估背景關系已經配置了 bean 決議器,可以使用 @ 符號從運算式中查找 bean,直接看案例,

@Configuration
@ComponentScan
public class BeanReferencesTest {
    // 注入一個bean
    @Component("myService")
    static class MyService{
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(BeanReferencesTest.class);
        SpelExpressionParser parser = new SpelExpressionParser();
        // 使用 StandardEvaluationContext
        StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext();
        // 需要注入一個BeanResolver來決議bean參考,此處注入 BeanFactoryResolver
        standardEvaluationContext.setBeanResolver(new BeanFactoryResolver(applicationContext));
        // 使用 @ 來參考bean
        MyService myService = parser.parseExpression("@myService").getValue(standardEvaluationContext, MyService.class);
        System.out.println(myService);
    }
}

思考下 FactoryBean 如何參考?

Spring bean定義中使用

基于 XML 或基于注釋的配置元資料的 SpEL 運算式來定義 BeanDefinition 實體的語法都是 #{ <expression string> }

應用程式背景關系中的所有 bean 都可以作為具有公共 bean 名稱的預定義變數使用,常用的包括但限于:

  • 標準背景關系環境 environment,型別為 org.springframework.core.env.Environment
  • JVM系統屬性systemProperties,型別為 Map<String, Object>
  • 系統環境變數systemEnvironment,型別為 Map<String, Object>

注意: 作為預定義變數的不需要使用 #前綴,

xml 方式

可以使用運算式設定屬性或建構式引數值,直接上案例,

組態檔

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean  id="myBean">
        <!--SpeL呼叫類靜態方法-->
        <property name="randomNumber" value="https://www.cnblogs.com/kongbubihai/p/#{ T(java.lang.Math).random() * 100.0 }"/>
        <!--SpeL讀取系統屬性中的用戶名-->
        <property name="name" value="https://www.cnblogs.com/kongbubihai/p/#{ systemProperties['user.name']}"/>
    </bean>

    <!-- 參考別的bean的屬性-->
    <bean  id="myBean2">
        <!--@參考bean實體 其實所有注入容器的bean都是預定義變數,不需要@也行-->
        <property name="name" value="https://www.cnblogs.com/kongbubihai/p/#{@myBean.randomNumber}"/>
        <property name="randomNumber" value="https://www.cnblogs.com/kongbubihai/p/#{myBean.randomNumber}"/>
    </bean>
</beans>

測驗程式和結果

/**
 * xml方式在bean定義中使用SpEL
 */
@Test
public void test() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo20/spring.xml");
    Map<String, MyBean> beansOfType = context.getBeansOfType(MyBean.class);
    beansOfType.entrySet().forEach(entry -> System.out.println(entry.getKey() + " : " + entry.getValue()));
    context.close();
    // myBean : MyBean{randomNumber=72.45707551702549, name='dell'}
    // myBean2 : MyBean{randomNumber='dell', name='72.45707551702549'}
}

注解方式

要指定默認值,可以將 @Value 注釋放在欄位、方法以及方法或建構式引數上,直接看案例,

@Component
@ComponentScan
public class MyComponent {

    private String language;

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

    private String name;

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

    @Autowired
    public MyComponent(@Value("#{ systemProperties['user.language']}") String language) {
        this.language = language;
    }
    // ...
}

測驗,觀察輸出結果,

@Test
public void test(){
    AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(MyComponent.class);
    MyComponent bean = context.getBean(MyComponent.class);
    System.out.println(bean);
    // MyComponent{language='zh', locale='zh', name='dell'}
}

總結

本文從原理到實戰案例詳解介紹了SpEL運算式,紙上得來終覺淺,絕知此事要躬行,案例比較多,好好消化,本文也可以作為SpEL使用手冊來使用,

本篇原始碼地址: https://github.com/kongxubihai/pdf-spring-series/tree/main/spring-series-ioc/src/main/java/com/crab/spring/ioc/demo20

知識分享,轉載請注明出處,學無先后,達者為先!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/430990.html

標籤:Java

上一篇:2022年【米哈游】 金三銀四 三月內推開始啦!不加班福利好,200+個崗位任你挑選,趕快來看吧!

下一篇:類的主動使用和被動使用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more