一、背景介紹
由阿里的電商業務規則、表示式(布林組合)、特殊數學公式計算(高精度)、語法分析、指令碼二次定製等強需求而設計的一門動態指令碼引擎解析工具。 在阿里集團有很強的影響力,同時為了自身不斷最佳化、發揚開源貢獻精神,於2012年開源。
QLExpress指令碼引擎被廣泛應用在阿里的電商業務場景,具有以下的一些特性:
1、執行緒安全,引擎運算過程中的產生的臨時變數都是threadlocal型別。2、高效執行,比較耗時的指令碼編譯過程可以快取在本地機器,執行時的臨時變數建立採用了緩衝池的技術,和groovy效能相當。3、弱型別指令碼語言,和groovy,javascript語法類似,雖然比強型別指令碼語言要慢一些,但是使業務的靈活度大大增強。4、安全控制,可以透過設定相關執行引數,預防死迴圈、高危系統api呼叫等情況。5、程式碼精簡,依賴最小,250k的jar包適合所有java的執行環境,在android系統的低端pos機也得到廣泛運用。二、依賴和呼叫說明<dependency> <groupId>com.alibaba</groupId> <artifactId>QLExpress</artifactId> <version>3.2.0</version></dependency>
ExpressRunner runner = new ExpressRunner();DefaultContext<String, Object> context = new DefaultContext<String, Object>();context.put("a",1);context.put("b",2);context.put("c",3);String express = "a+b*c";Object r = runner.execute(express, context, null, true, false);System.out.println(r);
三、語法介紹1、運算子和java物件操作普通java語法
//支援 +,-,*,/,<,>,<=,>=,==,!=,<>【等同於!=】,%,mod【取模等同於%】,++,--,//in【類似sql】,like【sql語法】,&&,||,!,等運算子//支援for,break、continue、if then else 等標準的程式控制邏輯n=10;for(sum=0,i=0;i<n;i++){sum=sum+i;}return sum; //邏輯三元操作a=1;b=2;maxnum = a>b?a:b;
和java語法相比,要避免的一些ql寫法錯誤不支援try{}catch{}不支援java8的lambda表示式不支援for迴圈集合操作for (GRCRouteLineResultDTO item : list)弱型別語言,請不要定義型別宣告,更不要用Templete(Map<String,List>之類的)array的宣告不一樣min,max,round,print,println,like,in 都是系統預設函式的關鍵字,請不要作為變數名//java語法:使用泛型來提醒開發者檢查型別keys = new ArrayList<String>();deviceName2Value = new HashMap<String,String>(7);String[] deviceNames = {"ng","si","umid","ut","mac","imsi","imei"};int[] mins = {5,30};//ql寫法:keys = new ArrayList();deviceName2Value = new HashMap();deviceNames = ["ng","si","umid","ut","mac","imsi","imei"];mins = [5,30];//java語法:物件型別宣告FocFulfillDecisionReqDTO reqDTO = param.getReqDTO();//ql寫法:reqDTO = param.getReqDTO();//java語法:陣列遍歷for(GRCRouteLineResultDTO item : list) {}//ql寫法:for(i=0;i<list.size();i++){item = list.get(i);}//java語法:map遍歷for(String key : map.keySet()) { System.out.println(map.get(key));}//ql寫法: keySet = map.keySet(); objArr = keySet.toArray(); for (i=0;i<objArr.length;i++) { key = objArr[i]; System.out.println(map.get(key)); }
java的物件操作import com.ql.util.express.test.OrderQuery;//系統自動會import java.lang.*,import java.util.*;query = new OrderQuery();//建立class例項,會根據classLoader資訊,自動補全類路徑query.setCreateDate(new Date());//設定屬性query.buyer = "張三";//呼叫屬性,預設會轉化為setBuyer("張三")result = bizOrderDAO.query(query);//呼叫bean物件的方法System.out.println(result.getId());//靜態方法
2、指令碼中定義function
function add(int a,int b){ return a+b;};function sub(int a,int b){ return a - b;};a=10;return add(a,4) + sub(a,9);
3、擴充套件運算子:Operator替換if then else 等關鍵字
runner.addOperatorWithAlias("如果", "if",null);runner.addOperatorWithAlias("則", "then",null);runner.addOperatorWithAlias("否則", "else",null);exp = "如果 (語文+數學+英語>270) 則 {return 1;} 否則 {return 0;}";DefaultContext<String, Object> context = new DefaultContext<String, Object>();runner.execute(exp,context,null,false,false,null);
如何自定義Operator//定義一個繼承自com.ql.util.express.Operator的運算子public class JoinOperator extends Operator{ public Object executeInner(Object[] list) throws Exception { Object opdata1 = list[0]; Object opdata2 = list[1]; if(opdata1 instanceof java.util.List){ ((java.util.List)opdata1).add(opdata2); return opdata1; }else{ java.util.List result = new java.util.ArrayList(); result.add(opdata1); result.add(opdata2); return result; } }}
如何使用Operator
//(1)addOperatorExpressRunner runner = new ExpressRunner();DefaultContext<String, Object> context = new DefaultContext<String, Object>();runner.addOperator("join",new JoinOperator());Object r = runner.execute("1 join 2 join 3", context, null, false, false);System.out.println(r);//返回結果 [1, 2, 3]//(2)replaceOperatorExpressRunner runner = new ExpressRunner();DefaultContext<String, Object> context = new DefaultContext<String, Object>();runner.replaceOperator("+",new JoinOperator());Object r = runner.execute("1 + 2 + 3", context, null, false, false);System.out.println(r);//返回結果 [1, 2, 3]//(3)addFunctionExpressRunner runner = new ExpressRunner();DefaultContext<String, Object> context = new DefaultContext<String, Object>();runner.addFunction("join",new JoinOperator());Object r = runner.execute("join(1,2,3)", context, null, false, false);System.out.println(r);//返回結果 [1, 2, 3]
4、繫結java類或者物件的methodaddFunctionOfClassMethod+addFunctionOfServiceMethod
public class BeanExample { public static String upper(String abc) { return abc.toUpperCase(); } public boolean anyContains(String str, String searchStr) { char[] s = str.toCharArray(); for (char c : s) { if (searchStr.contains(c+"")) { return true; } } return false; }}runner.addFunctionOfClassMethod("取絕對值", Math.class.getName(), "abs", new String[] { "double" }, null);runner.addFunctionOfClassMethod("轉換為大寫", BeanExample.class.getName(), "upper", new String[] { "String" }, null);runner.addFunctionOfServiceMethod("列印", System.out, "println",new String[] { "String" }, null);runner.addFunctionOfServiceMethod("contains", new BeanExample(), "anyContains", new Class[] { String.class, String.class }, null);String exp = “取絕對值(-100);轉換為大寫(\"hello world\");列印(\"你好嗎?\");contains("helloworld",\"aeiou\")”;runner.execute(exp, context, null, false, false);
5、macro 宏定義runner.addMacro("計算平均成績", "(語文+數學+英語)/3.0");runner.addMacro("是否優秀", "計算平均成績>90");IExpressContext<String, Object> context =new DefaultContext<String, Object>();context.put("語文", 88);context.put("數學", 99);context.put("英語", 95);Object result = runner.execute("是否優秀", context, null, false, false);System.out.println(r);//返回結果true
6、編譯指令碼,查詢外部需要定義的變數和函式。
注意以下指令碼int和沒有int的區別
String express = "int 平均分 = (語文+數學+英語+綜合考試.科目2)/4.0;return 平均分";ExpressRunner runner = new ExpressRunner(true,true);String[] names = runner.getOutVarNames(express);for(String s:names){ System.out.println("var : " + s);}//輸出結果:var : 數學var : 綜合考試var : 英語var : 語文
7、關於不定引數的使用 @Test public void testMethodReplace() throws Exception { ExpressRunner runner = new ExpressRunner(); IExpressContext<String,Object> expressContext = new DefaultContext<String,Object>(); runner.addFunctionOfServiceMethod("getTemplate", this, "getTemplate", new Class[]{Object[].class}, null); //(1)預設的不定引數可以使用陣列來代替 Object r = runner.execute("getTemplate([11,'22',33L,true])", expressContext, null,false, false); System.out.println(r); //(2)像java一樣,支援函式動態引數呼叫,需要開啟以下全域性開關,否則以下呼叫會失敗 DynamicParamsUtil.supportDynamicParams = true; r = runner.execute("getTemplate(11,'22',33L,true)", expressContext, null,false, false); System.out.println(r); } //等價於getTemplate(Object[] params) public Object getTemplate(Object... params) throws Exception{ String result = ""; for(Object obj:params){ result = result+obj+","; } return result; }
8、關於集合的快捷寫法 @Test public void testSet() throws Exception { ExpressRunner runner = new ExpressRunner(false,false); DefaultContext<String, Object> context = new DefaultContext<String, Object>(); String express = "abc = NewMap(1:1,2:2); return abc.get(1) + abc.get(2);"; Object r = runner.execute(express, context, null, false, false); System.out.println(r); express = "abc = NewList(1,2,3); return abc.get(1)+abc.get(2)"; r = runner.execute(express, context, null, false, false); System.out.println(r); express = "abc = [1,2,3]; return abc[1]+abc[2];"; r = runner.execute(express, context, null, false, false); System.out.println(r); }
9、集合的遍歷其實類似java的語法,只是ql不支援for(obj:list){}的語法,只能透過下標訪問。
//遍歷map map = new HashMap(); map.put("a", "a_value"); map.put("b", "b_value"); keySet = map.keySet(); objArr = keySet.toArray(); for (i=0;i<objArr.length;i++) { key = objArr[i]; System.out.println(map.get(key)); }
四、執行引數和API列表介紹
QLExpressRunner如下圖所示,從語法樹分析、上下文、執行過程三個方面提供二次定製的功能擴充套件。
1、屬性開關isPrecise /** * 是否需要高精度計算 */ private boolean isPrecise = false;
高精度計算在會計財務中非常重要,java的float、double、int、long存在很多隱式轉換,做四則運算和比較的時候其實存在非常多的安全隱患。 所以類似匯金的系統中,會有很多BigDecimal轉換程式碼。而使用QLExpress,你只要關注數學公式本身 訂單總價 = 單價 * 數量 + 首重價格 + ( 總重量 - 首重) * 續重單價 ,然後設定這個屬性即可,所有的中間運算過程都會保證不丟失精度。
isShortCircuit /** * 是否使用邏輯短路特性 */ private boolean isShortCircuit = true;
在很多業務決策系統中,往往需要對布林條件表示式進行分析輸出,普通的java運算一般會透過邏輯短路來減少效能的消耗。例如規則公式: star>10000 and shoptype in('tmall','juhuasuan') and price between (100,900) 假設第一個條件 star>10000 不滿足就停止運算。但業務系統卻還是希望把後面的邏輯都能夠運算一遍,並且輸出中間過程,保證更快更好的做出決策。
參照單元測試:ShortCircuitLogicTest.java
isTrace /** * 是否輸出所有的跟蹤資訊,同時還需要log級別是DEBUG級別 */ private boolean isTrace = false;
這個主要是是否輸出指令碼的編譯解析過程,一般對於業務系統來說關閉之後會提高效能。
2、呼叫入參/** * 執行一段文字 * @param expressString 程式文字 * @param context 執行上下文,可以擴充套件為包含ApplicationContext * @param errorList 輸出的錯誤資訊List * @param isCache 是否使用Cache中的指令集,建議為true * @param isTrace 是否輸出詳細的執行指令資訊,建議為false * @param aLog 輸出的log * @return * @throws Exception */ Object execute(String expressString, IExpressContext<String,Object> context,List<String> errorList, boolean isCache, boolean isTrace, Log aLog);
3、功能擴充套件API列表QLExpress主要透過子類實現Operator.java提供的以下方法來最簡單的運算子定義,然後可以被透過addFunction或者addOperator的方式注入到ExpressRunner中。
public abstract Object executeInner(Object[] list) throws Exception;
比如我們幾行程式碼就可以實現一個功能超級強大、非常好用的join運算子:
list = 1 join 2 join 3; -> [1,2,3] list = join(list,4,5,6); -> [1,2,3,4,5,6]
public class JoinOperator extends Operator{ public Object executeInner(Object[] list) throws Exception { java.util.List result = new java.util.ArrayList(); Object opdata1 = list[0]; if(opdata1 instanceof java.util.List){ result.addAll((java.util.List)opdata1); }else{ result.add(opdata1); } for(int i=1;i<list.length;i++){ result.add(list[i]); } return result; }}
如果你使用Operator的基類OperatorBase.java將獲得更強大的能力,基本能夠滿足所有的要求。
(1)function相關API//透過name獲取function的定義OperatorBase getFunciton(String name);//透過自定義的Operator來實現類似:fun(a,b,c)void addFunction(String name, OperatorBase op);//fun(a,b,c) 繫結 object.function(a,b,c)物件方法void addFunctionOfServiceMethod(String name, Object aServiceObject, String aFunctionName, Class<?>[] aParameterClassTypes, String errorInfo);//fun(a,b,c) 繫結 Class.function(a,b,c)類方法void addFunctionOfClassMethod(String name, String aClassName, String aFunctionName, Class<?>[] aParameterClassTypes, String errorInfo);//給Class增加或者替換method,同時 支援a.fun(b) ,fun(a,b) 兩種方法呼叫//比如擴充套件String.class的isBlank方法:“abc”.isBlank()和isBlank("abc")都可以呼叫void addFunctionAndClassMethod(String name,Class<?>bindingClass, OperatorBase op);
(2)Operator相關API提到指令碼語言的運算子,優先順序、運算的目數、覆蓋原始的運算子(+,-,*,/等等)都是需要考慮的問題,QLExpress統統幫你搞定了。
//新增運算子號,可以設定優先順序void addOperator(String name,Operator op);void addOperator(String name,String aRefOpername,Operator op); //替換運算子處理OperatorBase replaceOperator(String name,OperatorBase op); //新增運算子和關鍵字的別名,比如 if..then..else -> 如果。。那麼。。否則。。void addOperatorWithAlias(String keyWordName, String realKeyWordName, String errorInfo);
(3)宏定義相關API
QLExpress的宏定義比較簡單,就是簡單的用一個變數替換一段文字,和傳統的函式替換有所區別。
//比如addMacro("天貓賣家","userDO.userTag &1024 ==1024")void addMacro(String macroName,String express)
(4)java class的相關api
QLExpress可以透過給java類增加或者改寫一些method和field,比如 鏈式呼叫:"list.join("1").join("2")",比如中文屬性:"list.長度"。
//新增類的屬性欄位void addClassField(String field,Class<?>bindingClass,Class<?>returnType,Operator op);//新增類的方法void addClassMethod(String name,Class<?>bindingClass,OperatorBase op);
注意,這些類的欄位和方法是執行器透過解析語法執行的,而不是透過位元組碼增強等技術,所以只在指令碼執行期間生效,不會對jvm整體的執行產生任何影響,所以是絕對安全的。
(4)語法樹解析變數、函式的API這些介面主要是對一個指令碼內容的靜態分析,可以作為上下文建立的依據,也可以用於系統的業務處理。 比如:計算 “a+fun1(a)+fun2(a+b)+c.getName()” 包含的變數:a,b,c 包含的函式:fun1,fun2
//獲取一個表示式需要的外部變數名稱列表String[] getOutVarNames(String express);String[] getOutFunctionNames(String express);
(5)語法解析校驗api
指令碼語法是否正確,可以透過ExpressRunner編譯指令集的介面來完成。
String expressString = "for(i=0;i<10;i++){sum=i+1}return sum;";InstructionSet instructionSet = expressRunner.parseInstructionSet(expressString);//如果呼叫過程不出現異常,指令集instructionSet就是可以被載入執行(execute)了!
(6)指令集快取相關的api因為QLExpress對文字到指令集做了一個本地HashMap快取,通常情況下一個設計合理的應用指令碼數量應該是有限的,快取是安全穩定的,但是也提供了一些介面進行管理。
//優先從本地指令集快取獲取指令集,沒有的話生成並且快取在本地 InstructionSet getInstructionSetFromLocalCache(String expressString); //清除快取 void clearExpressCache();
(7)安全風險控制7.1 防止死迴圈
try { express = "sum=0;for(i=0;i<1000000000;i++){sum=sum+i;}return sum;"; //可透過timeoutMillis引數設定指令碼的執行超時時間:1000ms Object r = runner.execute(express, context, null, true, false, 1000); System.out.println(r); throw new Exception("沒有捕獲到超時異常"); } catch (QLTimeOutException e) { System.out.println(e); }
7.1 防止呼叫不安全的系統api
ExpressRunner runner = new ExpressRunner(); QLExpressRunStrategy.setForbiddenInvokeSecurityRiskMethods(true); DefaultContext<String, Object> context = new DefaultContext<String, Object>(); try { express = "System.exit(1);"; Object r = runner.execute(express, context, null, true, false); System.out.println(r); throw new Exception("沒有捕獲到不安全的方法"); } catch (QLException e) { System.out.println(e); }
(8)增強上下文引數Context相關的api8.1 與spring框架的無縫整合上下文引數 IExpressContext context 非常有用,它允許put任何變數,然後在指令碼中識別出來。
public class QLExpressContext extends HashMap<String, Object> implements IExpressContext<String, Object> { private ApplicationContext context; //建構函式,傳入context和 ApplicationContext public QLExpressContext(Map<String, Object> map, ApplicationContext aContext) { super(map); this.context = aContext; } /** * 抽象方法:根據名稱從屬性列表中提取屬性值 */ public Object get(Object name) { Object result = null; result = super.get(name); try { if (result == null && this.context != null && this.context.containsBean((String) name)) { // 如果在Spring容器中包含bean,則返回String的Bean result = this.context.getBean((String) name); } } catch (Exception e) { throw new RuntimeException(e); } return result; } public Object put(String name, Object object) { return super.put(name, object); }}
完整的demo參照 SpringDemoTest.java
8.2 自定義函式運算子獲取原始的context控制上下文自定義的Operator需要直接繼承OperatorBase,獲取到parent即可,可以用於在執行一組指令碼的時候,直接編輯上下文資訊,業務邏輯處理上也非常有用。
public class ContextMessagePutTest { class OperatorContextPut extends OperatorBase { public OperatorContextPut(String aName) { this.name = aName; } @Override public OperateData executeInner(InstructionSetContext parent, ArraySwap list) throws Exception { String key = list.get(0).toString(); Object value = list.get(1); parent.put(key,value); return null; } } @Test public void test() throws Exception{ ExpressRunner runner = new ExpressRunner(); OperatorBase op = new OperatorContextPut("contextPut"); runner.addFunction("contextPut",op); String exp = "contextPut('success','false');contextPut('error','錯誤資訊');contextPut('warning','提醒資訊')"; IExpressContext<String, Object> context = new DefaultContext<String, Object>(); context.put("success","true"); Object result = runner.execute(exp,context,null,false,true); System.out.println(result); System.out.println(context); }}
原文地址:https://github.com/alibaba/QLExpress