Java公式:如何執行字串表示式?!

蜜汁微笑發表於2018-11-15

背景和簡介

在日常的開發中,偶爾會遇到執行字串表示式的情況,通常這樣的需求會對需求進行進一步分析,然後進行進一步 “特殊化”,最後直接寫到硬程式碼中,這樣做的話,就不太好擴充套件了;也有另外的處理方式是採用 Java 內建的 JavaScript 引擎等執行字串表示式,但是內建引擎也有弊端,比如頻繁執行片段式的字串的效率非常低,並且與 Java 之間的資料互動比較麻煩,於是,便產生了寫一個“字串表示式計算引擎”的想法...

寫的過程其實沒想象中那麼麻煩,最初版大概在今年 5 月底寫好,但是結構比較混亂,寫的時候基本上是一邊寫一邊修,最後 if...else...這樣的條件以及巢狀太多,以至於自己也無法完全理解,好在邏輯基本完善,執行也沒出現意料之外的情況(也許出現了,只是沒發現),並且是自己用,所以就沒太在意。

前兩個星期,又抽空重新整理了一遍,重新梳理了一下結構,擴充套件了一些功能,重新定義了一下各種符號的 “語義邊界”,儘可能保證運算子與 Java 本身運算子一致,邏輯結構也更清晰,不會產生意外情況等。

RunnerUtil 在語法上很大程度參考了 JavaScript 的語法,比如用花括號表示一個鍵值對“物件”(實際上會被解析成 HashMap),鍵名不必用單引號或雙引號包裹,單引號雙引號均表示普通字串,通過點號(.)和方括號鏈式取值等。這對於從事 JavaWeb 開發的同學來說,書寫起來也比較方便。現在已經實現了絕大部分功能,已實現的功能也經過一定測試,確保能“符合期望”的執行,如果有想法和建議也希望多多的提一下哈。

基本用法介紹

字串表示式通過一個叫 RunnerUtil 的靜態類執行,可以直接執行得到表示式結果,也可以解析一個表示式後在需要的時候執行,RunnerUtil 主要有以下幾個方法:

  • RunnerUtil.run(/* expression */); 直接執行表示式並得到結果;
RunnerUtil.run("1 + 1"); // 2
RunnerUtil.run(" 'Hello' + ' ' + 'World!' "); // "Hello World!"
複製程式碼
  • RunnerUtil.run(/* expression */, / * data */); 執行含有變數的表示式,後面的 data 是變數將要指向的“值”;
  • RunnerUtil.parseRun(/* expression */); 直接執行“另一種”表示式,並得到結果,如:
RunnerUtil.parseRun("Hello {{   'World!'   }}"); // "Hello World!"
複製程式碼

可見 #parseRun 是執行包含“插值語法”的表示式,被包裹的內容被作為一個表示式單獨執行;
字串中可以包含多個插值語法表示式,但不能巢狀和交叉,也可以執行含有變數的表示式。

  • Runner runner = RunnerUtil.parse(/* expression */);
// 更推薦這種先解析,再執行的方式
Runner runner = RunnerUtil.parse(" 1 + 2");
Integer result = runner.run(); // 3
複製程式碼

解析一個字串表示式,得到一個“字串表示式執行器” —— Runner,然後呼叫其 run(/ * data */) 方法執行並得到結果,多次執行推薦先解析 Runner,再執行的方式

語法及運算詳細介紹

作為一個具有一定“語言特點”的東西,它定義了一些自己的語法、資料型別、運算型別等,但大部分都與 Java 和 JavaScript 相容,相同符號具有相同或相似的語言意義。

資料型別:

  1. null:這是一個關鍵字,但因為它符合和變數的定義規則,所以需要注意一下,同樣被定義為關鍵字的還有 true 和 false。
  2. boolean:true 和 false
RunnerUtil.run(" null   "); // null
RunnerUtil.run("   true "); // true
RunnerUtil.run("false"); // false
// 表示式中多餘的空格自動忽略
複製程式碼
  1. 數字:這裡面的數字統一採用 Java 裡的 int 和 double 型資料,直接參與運算的也只有是這兩種型別,區別就是有沒有小數點。
RunnerUtil.run(" 12  "); // 12
RunnerUtil.run(" 12.5 "); // 12.5
// 表示數字必須是連續,中間不能有空格的
// 否則將丟擲異常,如
RunnerUtil.run(" 12. 5"); // 異常
RunnerUtil.run(" 1 2 "); // 異常
複製程式碼

表示數字的字元之間應該是連續的,如:25、36.9 等;如果是不連續的會丟擲異常,如:2 5、36 .9 等;

  1. 字串:Java 裡的字串用雙引號包裹,在這裡還將表示字元的單引號“徵用”,雙引號單引號包裹的都表示普通字串的直接值,這樣做也是為了書寫方便(與 JavaScript 相似),同時也就沒有了 char 型別資料啦啦啦……
RunnerUtil.run(" 'abcdef'  "); // "abcdef"
RunnerUtil.run(" \"abcdef\"  "); // "abcdef"
RunnerUtil.run(" 'abc   def'  "); // "abc   def"
複製程式碼
  1. List:實際上是 ArrayList,對應 JavaScript 裡面的陣列。Java 的陣列也對應 JavaScript 陣列。
RunnerUtil.run(" { } "); 
// 總是返回一個空ArrayList

RunnerUtil.run(" {1,2,,4, } "); 
// 總是返回一個包含:1、2、null、4 這幾項的 ArrayList

// 可以看出最後一個逗號之後如果是結束符號會自動忽略
// 中間的逗號與逗號之間若沒有其他非空白符號會插入一個 null 值
複製程式碼
  1. Map:實際上是 HashMap,對應 JavaScript 裡的物件。同樣對應 JavaScript 物件的還有普通 POJO。

Map 對應的是 JavaScript 裡的物件,但是在這裡 Map 的鍵可以是這些資料型別:

null、true / false、數字(int / double)、字串,不能再是其他 Java 物件了

RunnerUtil.run(" {:} "); // 總是返回一個空 HashMap,
// 注意與空 List 的異同,都是用花括號表示
// 但空 Map 裡面需要有一個冒號,否則就是 List

RunnerUtil.run(" {key: 'value'}");
// 總是返回包含一個鍵值對的 HashMap
// 可以看出,物件的鍵名是字串的話可以不用引號包裹
// 但是值必須被包裹
RunnerUtil.run(" {true: 'value'}"); // 鍵是 true
/*
 * 這裡的 true 不是字串,而是 boolean。
 * 同樣,未被引號包裹的 null、false、數字都是對應型別的資料,而不是字串
 * 其他符合變數命名規則的鍵都是普通字串,被單引號或雙引號包裹的也是
 */
RunnerUtil.run(" {'true': 'value', 25: false, 'name': \"張三\"}");
複製程式碼

運算支援的型別:

  1. 普通四則混合運算:+、-、*、/、%、()
RunnerUtil.run(" 1 + 1 "); // 2
RunnerUtil.run(" 1 + (3 * 4)) "); // 13
RunnerUtil.run(" 'Hello ' + \"World!\" ");  // "Hello World!"
RunnerUtil.run(" true + false "); // "truefalse"
/*
 * true+false 在 Java 中是不允許的
 * 但如果是“+”運算的話,這裡均作為普通字串;
 * 相當於呼叫了 toString 方法
 */
複製程式碼
  1. 位運算:&、|、^、<<、>>
RunnerUtil.run(" 1 ^ 1 "); 
RunnerUtil.run(" 1 & 1 "); 
RunnerUtil.run(" 1 | 1 "); 
RunnerUtil.run(" 1 << 1 "); 
RunnerUtil.run(" 1 >> 1 ");
複製程式碼
  1. 比較運算:>、>=、==、<=、<
RunnerUtil.run(" 1 + 1 == 2 "); // true
RunnerUtil.run(" 1 + 1 < 2 "); // false
複製程式碼
  1. 邏輯運算:&&、||、!
RunnerUtil.run("1+1==2 && 5 > 4"); // true
複製程式碼
  1. 三元運算:assertExpression ? trueExpression : falseExpression
RunnerUtil.run("true ? 'name' : 'age'"); // name
RunnerUtil.run("false ? 'name' : 'age'"); // age
RunnerUtil.run("1 > 2 ? 'name' : 'age'"); // age
RunnerUtil.run("1 < 2 ? 'name' : 'age'"); // name
複製程式碼
  1. 變數:命名規則與 Java 變數命名規則相同,同時 null、true、false 不能作為變數

表示式中包含變數就代表這個表示式在執行得到結果時需要從外部獲取資料,如果不能正確的從資料來源讀取到資料,執行就會丟擲異常;

RunnerUtil.run(" 'Hello, ' + name "); // 丟擲異常

Map data = new HashMap();
data.put("name", "Li Lei!");

RunnerUtil.run(" 'Hello, ' + name ", data); // "Hello, Li Lei!"
複製程式碼
  1. 鏈式取值:鏈式語法與 JavaScript 很相似
HashMap data = new HashMap(); 

ArrayList list = new ArrayList(); 
list.add(true); 
list.add(false); 
list.add(25); 
list.add('隔壁老王'); 

HashMap map = new HashMap(); 
map.put("name", "小四"); 
map.put("index", 2); 
map.put(true, "true 是 Boolean 型別作為鍵"); 

data.put("list", list); 
data.put("map", map); 

RunnerUtil.run("map.name", data); // "小四"

RunnerUtil.run("map['name']", data); 
// "小四" (也可以這樣取值)

RunnerUtil.run("list[ 2 ]", data);
// 25 (索引取值需要用方括號包裹) 

RunnerUtil.run("list[3]", data);
// "隔壁老王" (索引取值需要用方括號包裹) 

RunnerUtil.run("list[map.index]", data); // 25
// (這是高階點的用法,方括號包含另一個表示式
// 返回值是一個索引,然後返回索引指向的值)

RunnerUtil.run("[true]", data); // "true 是 Boolean 型別作為鍵"
// 如果不用方括號包括,true 就是一個直接值,返回 true
// 那麼問題來了:
// 如果傳入的資料不是 Map 或 POJO,而是 List 或陣列怎麼辦呢?
RunnerUtil.run(" [1] ", list); // false
// 啊……唐宗宋祖,略顯風騷!

// 這種鏈式語法與 JavaScript 很相似
複製程式碼
  1. 執行方法:目前只能執行無參和一個引數的方法,變長引數的方法支援不完善,慎用。
// 這裡的資料 data 繼續用上一條的 data,具體資料不寫了

RunnerUtil.run("map.size()", data); // 3
RunnerUtil.run("map.get('name')", data); // "小四" 
RunnerUtil.run("map.get('name').length()", data); // 2
RunnerUtil.run("map.name.length()", data); // 2
RunnerUtil.run(" [3].length() ", list); // 4
// 唐宗宋祖,又顯風騷!
複製程式碼
  1. 執行靜態方法: @ ;執行靜態方法需要用到“@”符號作為標記。目前也不支援多引數方法呼叫。

當你開啟原始碼會發現這是一整個獨立的工具庫,很多方法和 commons-lang 包內容相似(個人認為不是重複造輪子,也有很多不同的和不如的)...,執行靜態方法也可以執行這個工具庫內的所有工具方法,暫時未將 RunnerUtil 剝離出來,也還不支援自定義的靜態方法呼叫,不過這個工具庫所提供的功能

RunnerUtil.run("@System.currentTimeMillis() ");
// 15.....(一個毫秒數)
RunnerUtil.run("@Objects.toString(25) "); // "25"
複製程式碼
  1. 自定義 List 型別、Map 型別、靜態方法呼叫類。

public static class InnerObjects {
    public static String toString(Object o){
        return "123";
    }
}

RunnerSettings settings = RunnerSettings.builder()
    // 自定義生成的陣列是 LinkedList,預設 ArrayList
    .setArrCreator(LinkedList::new)
    // 自定義生成的陣列是 TreeMap,預設是 HashMap
    .setArrCreator(TreeMap::new)
    // 自定義靜態方法類 Objects,將覆蓋 java.util.Objects
    .addCaller("Objects", InnerObjects.class)
    .build();

Runner runner0 = RunnerUtil.parse("@Objects.toString('juejin shequ')");
Runner runner1 = RunnerUtil.parse("@Objects.toString('juejin shequ')", settings);

Object result0 = runner0.run(); // "juejin shequ"
Object result1 = runner1.run(); // "123"
複製程式碼

綜上,就是這個 RunnerUtil 所支援的公式運算了,以上所列舉的運算可以巢狀、連線、但是不能交叉的進行運算。還有很多複雜的公式沒在這這裡貼出來,目標是能執行任何符合 Java 表示式規則的公,。

RunnerUtil 原本是為了一個通用 Excel 匯出工具而寫的,現在匯出功能已經實現,匯入功能還在實現中,工具在也在同一個 Github 中,裡面包含一個實用例子,和一個匯出資料測試表。

接下來要做的是加入的功能是多引數方法呼叫,順便問一下需要將 RunnerUtil 剝離出來的嗎?如果有,請留言一下,會盡快抽時間剝離,否則得過一段時間了。

希望對大家的日常開發有所幫助,也希望大家給點意見,如出現 BUG 一定在最快的時間內修改,謝謝大家啦啦啦!!

更多示例請移步:

  1. RunnerUtilTestTest
  2. ParseDelimitersTestTest
  3. ParseCoreTestTest

相關文章