首頁>技術>

一、背景介紹

由阿里的電商業務規則、表示式(布林組合)、特殊數學公式計算(高精度)、語法分析、指令碼二次定製等強需求而設計的一門動態指令碼引擎解析工具。 在阿里集團有很強的影響力,同時為了自身不斷最佳化、發揚開源貢獻精神,於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類或者物件的method

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

10
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • SPL 中呼叫 Python 程式