背景和特性
背景
由阿里的電商業(yè)務(wù)規(guī)則、表達(dá)式(布爾組合)、特殊數(shù)學(xué)公式計(jì)算(高精度)、語(yǔ)法分析、腳本二次定制等強(qiáng)需求而設(shè)計(jì)的一門動(dòng)態(tài)腳本引擎解析工具。 在阿里集團(tuán)有很強(qiáng)的影響力,同時(shí)為了自身不斷優(yōu)化、發(fā)揚(yáng)開(kāi)源貢獻(xiàn)精神,于2012年開(kāi)源。
特性
1、線程安全,引擎運(yùn)算過(guò)程中的產(chǎn)生的臨時(shí)變量都是threadlocal類型。
2、高效執(zhí)行,比較耗時(shí)的腳本編譯過(guò)程可以緩存在本地機(jī)器,運(yùn)行時(shí)的臨時(shí)變量創(chuàng)建采用了緩沖池的技術(shù),和groovy性能相當(dāng)。
3、弱類型腳本語(yǔ)言,和groovy,javascript語(yǔ)法類似,雖然比強(qiáng)類型腳本語(yǔ)言要慢一些,但是使業(yè)務(wù)的靈活度大大增強(qiáng)。
4、安全控制,可以通過(guò)設(shè)置相關(guān)運(yùn)行參數(shù),預(yù)防死循環(huán)、高危系統(tǒng)api調(diào)用等情況。
5、代碼精簡(jiǎn),依賴最小,250k的jar包適合所有java的運(yùn)行環(huán)境,在android系統(tǒng)的低端pos機(jī)也得到廣泛運(yùn)用。
基本概念
架構(gòu)圖
三個(gè)核心組件
表達(dá)式express
直接寫在java類中 注意使用ql語(yǔ)法
寫在ql文件中 ql文件中讀取表達(dá)式
上下文context
運(yùn)行時(shí) runner
- 單例模式
快速上手
引入pom依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.2.0</version>
</dependency>
寫個(gè)簡(jiǎn)單的例子計(jì)算a+b+c
public void quick_start() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
//下面五個(gè)參數(shù)意義分別是 表達(dá)式,上下文,errorList,是否緩存,是否輸出日志
Object result = runner.execute("a+b+c", context, null, true, false);
System.out.println("a+b+c=" + result);
}
步驟總結(jié)
- 生成Runner
實(shí)際使用建議是單例的,看下上面的架構(gòu)圖
- 上下文傳參
生成DefaultContext,并往里面添加數(shù)據(jù)
- 定義表達(dá)式
- 執(zhí)行得到結(jié)果
語(yǔ)法
操作符和java對(duì)象操作
普通java語(yǔ)法
支持 +,-,*,/,<,>,<=,>=,==,!=,<>【等同于!=】,%,mod【取模等同于%】,++,--,
in【類似sql】,like【sql語(yǔ)法】,&&,||,!,等操作符
支持for,break、continue、if then else 等標(biāo)準(zhǔn)的程序控制邏輯
三目運(yùn)算符
下面展示一些基本的例子
- 計(jì)算1-100的和
public void test_basic_use_for() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "n=100;sum=0;" +
"for(i=1;i<=n;i++){" +
"sum = sum+i;" +
"}" +
"return sum;";
Object result = runner.execute(express, context, null, true, false);
System.out.println("1...100的和是: " + result);
}
- 三目運(yùn)算符
public void test_basic_use_three_var() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 5);
context.put("b", 10);
String express = "a>b?a:b";
Object max = runner.execute(express, context, null, true, false);
System.out.println("a和b中較大的指是:" + max);
}
注意點(diǎn)
和java語(yǔ)法相比,要避免ql的一些寫法錯(cuò)誤
不支持try{}catch{}
不支持java8的lambda表達(dá)式
-
不支持for循環(huán)集合操作for (GRCRouteLineResultDTO item : list)
- 通過(guò)數(shù)組方式訪問(wèn)
弱類型語(yǔ)言,請(qǐng)不要定義類型聲明,更不要用Templete(Map<String,List>之類的)
-
array的聲明不一樣
- 使用[]
min,max,round,print,println,like,in 都是系統(tǒng)默認(rèn)函數(shù)的關(guān)鍵字,請(qǐng)不要作為變量名
數(shù)組
- 聲明的方式類似arr=new int[3];
- 聲明并賦值的時(shí)候和java有點(diǎn)不同
- mins=[5,30];
- 迭代的時(shí)候?qū)傩越凶鰈ength
public void test_array_use() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "arr=new int[3];" +
"arr[0]=1;arr[1]=2;arr[2]=3;" +
"sum=arr[0]+arr[1]+arr[2];" +
"return sum;";
Object arrSum = runner.execute(express, context, null, true, false);
System.out.println("arrSum: " + arrSum);
}
list
new ArrayList(); 不用范型
迭代的時(shí)候?qū)傩詌ist.size()
取的時(shí)候用list.get(i)
public void test_list_use() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "list = new ArrayList();" +
"list.add(3);list.add(4);list.add(5);" +
"sum=0;" +
"for(i=0;i<list.size();i++){" +
"sum = sum+list.get(i);" +
"}" +
"return sum;";
Object listSum = runner.execute(express, context, null, true, false);
System.out.println("listSum: " + listSum);
}
map
不使用范型 new HashMap()
迭代的時(shí)候先生成keySet
keySet.toArray()獲取keyArray
迭代keyArray獲取key
通過(guò)key獲取value
public void test_map_use() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "map=new HashMap();" +
"map.put('a',2);map.put('b',2);map.put('c',2);" +
"sum=0;" +
"keySet=map.keySet();" +
"keyArray=keySet.toArray();" +
"for(i=0;i<keyArray.length;i++){" +
"sum=sum+map.get(keyArray[i]);" +
"}" +
"return sum;";
Object mapValueSum = runner.execute(express, context, null, true, false);
System.out.println("mapValueSum: " + mapValueSum);
}
語(yǔ)法糖
可以使用NewList和NewMap快速創(chuàng)建list和map
- NewList
public void test_NewList() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "abc=NewList(1,2,3);return abc.get(0)+abc.get(1)+abc.get(2);";
Object listSum = runner.execute(express, context, null, true, false);
System.out.println("listSum: " + listSum);
}
- NewMap
public void test_NewMap() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "abc=NewMap('a':1,'b':2,'c':3);return abc.get('a')+abc.get('b')+abc.get('c');";
Object mapSum = runner.execute(express, context, null, true, false);
System.out.println("mapSum: " + mapSum);
}
java bean
- 需要import引入依賴
- 系統(tǒng)自動(dòng)會(huì)import java.lang.,import java.util.;
- 創(chuàng)建對(duì)象后可以調(diào)用靜態(tài)和非靜態(tài)的方法
package com.ql.util.express.zihao;
/**
* @author tangzihao
* @Date 2021/1/20 9:17 下午
*/
public class Person {
public void sayHello() {
System.out.println("hello,world! this is non static method");
}
public void sayHelloStatic() {
System.out.println("hello,world! this is static method");
}
}
public void test_java_bean() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
//qlexpress只會(huì)引入java.util.*和java.lang.*
String express = "import com.ql.util.express.zihao.Person;" +
"person=new Person();" +
"person.sayHello();" +
"person.sayHelloStatic();";
runner.execute(express, context, null, true, false);
}
腳本中定義function
寫法和js非常相似。
注意點(diǎn)
- 在入?yún)⒌牡胤叫枰暶黝愋?/li>
- function的右}需要添加;
public void test_add_func() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "function add(int a,int b){" +
"return a+b;" +
"};" +
"a=2;b=2;" +
"return add(a,b);";
Object funcResult = runner.execute(express, context, null, true, false);
System.out.println("add(a,b)=" + funcResult);
}
擴(kuò)展操作符Operator
替換if,then,else關(guān)鍵字
注意then和else后需要使用;
public void test_replace_if_then_else() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addOperatorWithAlias("如果", "if", null);
runner.addOperatorWithAlias("則", "then", null);
runner.addOperatorWithAlias("否則", "else", null);
context.put("語(yǔ)文", 100);
context.put("數(shù)學(xué)", 100);
context.put("英語(yǔ)", 100);
String express = "如果 ((語(yǔ)文+數(shù)學(xué)+英語(yǔ))>270) 則 return 1;否則 return 0;";
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
}
定義自己的operator
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
addOperator
輸出結(jié)果[1,2,3]
public void test_add_operator() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "1 join 2 join 3";
runner.addOperator("join", new JoinOperator());
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
}
replaceOperator
替換原有的操作符,比如將+替換為join
public void test_replace_operator() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "1 + 2 + 3";
runner.replaceOperator("+", new JoinOperator());
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
}
addFunction
public void test_add_function() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
String express = "join(1,2,3)";
runner.addFunction("join", new JoinOperator());
Object result = runner.execute(express, context, null, true, false);
System.out.println(result);
}
綁定java類型或者對(duì)象的method
- addFunctionOfClassMethod 綁定靜態(tài)方法
- addFunctionOfServiceMethod 綁定實(shí)例方法
參數(shù)說(shuō)明:
- 函數(shù)名稱
- 類名稱或者對(duì)象實(shí)例
- 方法名稱
- 方法的參數(shù)類型
- 錯(cuò)誤信息 如果函數(shù)執(zhí)行的結(jié)果是false,需要輸出的錯(cuò)誤信息
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
runner.addFunctionOfClassMethod("取絕對(duì)值", Math.class.getName(), "abs",
new String[]{"double"}, null);
runner.addFunctionOfClassMethod("轉(zhuǎn)換為大寫", 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 = "取絕對(duì)值(-100);轉(zhuǎn)換為大寫(\"hello world\");打印(\"你好嗎?\");contains(\"helloworld\",\"aeiou\");";
runner.execute(exp, context, null, false, false);
- addFunctionAndClassMethod 給現(xiàn)有的類增加方法
public void test_function_and_class_method() throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
//給類增加方法
runner.addFunctionAndClassMethod("isBlank", String.class, new Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
Object obj = list[0];
if (obj == null) {
return true;
}
String str = String.valueOf(obj);
return str.length() == 0;
}
});
String express = "a=\"\".isBlank()";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
}
宏定義
public void test_macro_use() throws Exception {
ExpressRunner runner = new ExpressRunner();
IExpressContext<String, Object> context = new DefaultContext<String, Object>();
runner.addMacro("計(jì)算平均成績(jī)", "(語(yǔ)文+數(shù)學(xué)+英語(yǔ))/3.0");
runner.addMacro("是否優(yōu)秀", "計(jì)算平均成績(jī)>90");
context.put("語(yǔ)文", 88);
context.put("數(shù)學(xué)", 99);
context.put("英語(yǔ)", 95);
Object result = runner.execute("是否優(yōu)秀", context, null, false, false);
System.out.println(result);
}
編譯腳本,查詢外部需要定義的變量和函數(shù)
public void test_compile_script() throws Exception {
String express = "int 平均分 = (語(yǔ)文+數(shù)學(xué)+英語(yǔ)+綜合考試.科目2)/4.0;return 平均分";
ExpressRunner runner = new ExpressRunner(true, true);
String[] names = runner.getOutVarNames(express);
for (String s : names) {
System.out.println("var : " + s);
}
String[] functions = runner.getOutFunctionNames(express);
for (String s : functions) {
System.out.println("function : " + s);
}
}
關(guān)于不定參數(shù)的使用
比如BeanExample類的getTemplate方法的入?yún)⑹遣欢▍?shù)。
public class BeanExample {
public Object getTemplate(Object... params) throws Exception{
String result = "";
for(Object obj:params){
result = result+obj+",";
}
return result;
}
}
使用數(shù)組
@Test
public void test_multi_params_use_array() throws Exception {
ExpressRunner runner = new ExpressRunner();
IExpressContext<String, Object> expressContext = new DefaultContext<String, Object>();
runner.addFunctionOfServiceMethod("getTemplate", new BeanExample(), "getTemplate", new Class[]{Object[].class}, null);
Object r = runner.execute("getTemplate([11,'22',33L,true])", expressContext, null, false, false);
System.out.println(r);
}
使用動(dòng)態(tài)參數(shù)
注意需要打開(kāi)動(dòng)態(tài)開(kāi)關(guān) DynamicParamsUtil.supportDynamicParams = true;否則會(huì)拋出異常
public void test_multi_params_dynamic() throws Exception {
DynamicParamsUtil.supportDynamicParams = true;
ExpressRunner runner = new ExpressRunner();
IExpressContext<String, Object> expressContext = new DefaultContext<String, Object>();
runner.addFunctionOfServiceMethod("getTemplate", new BeanExample(), "getTemplate", new Class[]{Object[].class}, null);
Object r = runner.execute("getTemplate(11,'22',33L,true)", expressContext, null, false, false);
System.out.println(r);
}
集合的遍歷
類似java的語(yǔ)法,只是ql不支持for(obj:list){}的語(yǔ)法,只能通過(guò)下標(biāo)訪問(wèn)。
詳細(xì)見(jiàn) 操作符和java對(duì)象操作 這一小節(jié)的示例
運(yùn)行參數(shù)和api介紹
屬性開(kāi)關(guān)
isPrecise
高精度計(jì)算在會(huì)計(jì)財(cái)務(wù)中非常重要,java的float、double、int、long存在很多隱式轉(zhuǎn)換,做四則運(yùn)算和比較的時(shí)候其實(shí)存在非常多的安全隱患。 所以類似匯金的系統(tǒng)中,會(huì)有很多BigDecimal轉(zhuǎn)換代碼。而使用QLExpress,你只要關(guān)注數(shù)學(xué)公式本身 訂單總價(jià) = 單價(jià) * 數(shù)量 + 首重價(jià)格 + ( 總重量 - 首重) * 續(xù)重單價(jià) ,然后設(shè)置這個(gè)屬性即可,所有的中間運(yùn)算過(guò)程都會(huì)保證不丟失精度。
/**
* 是否需要高精度計(jì)算
*/
private boolean isPrecise = false;
public void is_precise() throws Exception {
ExpressRunner runner = new ExpressRunner(true, false);
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
//訂單總價(jià) = 單價(jià) * 數(shù)量 + 首重價(jià)格 + (總重量 - 首重) * 續(xù)重單價(jià)
context.put("單價(jià)", 1.25);
context.put("數(shù)量", 100);
context.put("首重價(jià)格", 125.25);
context.put("總重量", 20.55);
context.put("首重", 10.34);
context.put("續(xù)重單價(jià)", 3.33);
String express = "單價(jià)*數(shù)量+首重價(jià)格+(總重量-首重)*續(xù)重單價(jià)";
Object totalPrice = runner.execute(express, context, null, true, false);
System.out.println("totalPrice:" + totalPrice);
}
上面在創(chuàng)建ExpressRunner的時(shí)候第一個(gè)參數(shù)是是否開(kāi)啟高精度默認(rèn)false,第二個(gè)參數(shù)是是否開(kāi)啟trace。
開(kāi)啟高精度的情況下輸出是284.2493
沒(méi)有開(kāi)啟高精度的情況下輸出是284.2493
isShortCicuit
/**
* 是否使用邏輯短路特性
*/
private boolean isShortCircuit = true;
在很多業(yè)務(wù)決策系統(tǒng)中,往往需要對(duì)布爾條件表達(dá)式進(jìn)行分析輸出,普通的java運(yùn)算一般會(huì)通過(guò)邏輯短路來(lái)減少性能的消耗。例如規(guī)則公式: star>10000 and shoptype in('tmall','juhuasuan') and price between (100,900) 假設(shè)第一個(gè)條件 star>10000 不滿足就停止運(yùn)算。但業(yè)務(wù)系統(tǒng)卻還是希望把后面的邏輯都能夠運(yùn)算一遍,并且輸出中間過(guò)程,保證更快更好的做出決策。
具體可以參見(jiàn)ShortCircuitLogicTest測(cè)試類
runner.setShortCircuit(true);
runner.setShortCircuit(false);//顯式指定,默認(rèn)是true
isTrace
/**
* 是否輸出所有的跟蹤信息,同時(shí)還需要log級(jí)別是DEBUG級(jí)別
*/
private boolean isTrace = false;
ExpressRunner runner = new ExpressRunner(true, false);//第二個(gè)參數(shù)是否開(kāi)啟trace
這個(gè)主要是是否輸出腳本的編譯解析過(guò)程,一般對(duì)于業(yè)務(wù)系統(tǒng)來(lái)說(shuō)關(guān)閉之后會(huì)提高性能。
調(diào)用入?yún)?/h2>
對(duì)于runner.execute執(zhí)行方法的參數(shù)入?yún)⒄f(shuō)明
/**
* 執(zhí)行一段文本
* @param expressString 程序文本
* @param context 執(zhí)行上下文,可以擴(kuò)展為包含ApplicationContext
* @param errorList 輸出的錯(cuò)誤信息List
* @param isCache 是否使用Cache中的指令集,建議為true
* @param isTrace 是否輸出詳細(xì)的執(zhí)行指令信息,建議為false
* @param aLog 輸出的log
* @return
* @throws Exception
*/
Object execute(String expressString, IExpressContext<String,Object> context,List<String> errorList, boolean isCache, boolean isTrace, Log aLog);
功能擴(kuò)展API列表
QLExpress主要通過(guò)子類實(shí)現(xiàn)Operator.java提供的以下方法來(lái)最簡(jiǎn)單的操作符定義,然后可以被通過(guò)addFunction或者addOperator的方式注入到ExpressRunner中。
具體參見(jiàn)上面的JoinOperator的例子。
如果你使用Operator的基類OperatorBase.java將獲得更強(qiáng)大的能力,基本能夠滿足所有的要求。
function相關(guān)API
//通過(guò)name獲取function的定義
OperatorBase getFunciton(String name);
//通過(guò)自定義的Operator來(lái)實(shí)現(xiàn)類似:fun(a,b,c)
void addFunction(String name, OperatorBase op);
//fun(a,b,c) 綁定 object.function(a,b,c)對(duì)象方法
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,同時(shí) 支持a.fun(b) ,fun(a,b) 兩種方法調(diào)用
//比如擴(kuò)展String.class的isBlank方法:“abc”.isBlank()和isBlank("abc")都可以調(diào)用
void addFunctionAndClassMethod(String name,Class<?>bindingClass, OperatorBase op);
Operator相關(guān)API
提到腳本語(yǔ)言的操作符,優(yōu)先級(jí)、運(yùn)算的目數(shù)、覆蓋原始的操作符(+,-,*,/等等)都是需要考慮的問(wèn)題,QLExpress統(tǒng)統(tǒng)幫你搞定了。
//添加操作符號(hào),可以設(shè)置優(yōu)先級(jí)
void addOperator(String name,Operator op);
void addOperator(String name,String aRefOpername,Operator op);
//替換操作符處理
//比如將+替換成自定義的operator 見(jiàn)上面的例子
OperatorBase replaceOperator(String name,OperatorBase op);
//添加操作符和關(guān)鍵字的別名,比如 if..then..else -> 如果。。那么。。否則。。
//具體見(jiàn)上面的例子
void addOperatorWithAlias(String keyWordName, String realKeyWordName,
String errorInfo);
宏相關(guān)API
QLExpress的宏定義比較簡(jiǎn)單,就是簡(jiǎn)單的用一個(gè)變量替換一段文本,和傳統(tǒng)的函數(shù)替換有所區(qū)別。
見(jiàn)上面使用宏計(jì)算是否優(yōu)秀的例子
//比如addMacro("天貓賣家","userDO.userTag &1024 ==1024")
void addMacro(String macroName,String express)
java class相關(guān)API
QLExpress可以通過(guò)給java類增加或者改寫一些method和field,比如鏈?zhǔn)秸{(diào)用:"list.join("1").join("2")",比如中文屬性:"list.長(zhǎng)度"。
//添加類的屬性字段
void addClassField(String field,Class<?>bindingClass,Class<?>returnType,Operator op);
//添加類的方法
void addClassMethod(String name,Class<?>bindingClass,OperatorBase op);
public void testArrayOrMapJoinMethod() throws Exception {
ExpressRunner runner = new ExpressRunner();
IExpressContext<String,Object> context = new DefaultContext<String, Object>();
runner.addClassMethod("join", java.util.List.class, new Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
ArrayList arrayList = (ArrayList) list[0];
return StringUtils.join(arrayList,(String) list[1]);
}
});
runner.addClassMethod("join", java.util.Map.class, new Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
HashMap map = (HashMap) list[0];
StringBuilder sb = new StringBuilder();
for(Object key: map.keySet()){
sb.append(key).append("=").append(map.get(key)).append((String) list[1]);
}
return sb.substring(0,sb.length()-1);
}
});
Object result = runner.execute("list=new ArrayList();list.add(1);list.add(2);list.add(3);return list.join(' , ');",context,null,false,false);
System.out.println(result);
result = runner.execute("list=new HashMap();list.put('a',1);list.put('b',2);list.put('c',3);return list.join(' , ');",context,null,false,false);
System.out.println(result);
}
public void testAop() throws Exception {
ExpressRunner runner = new ExpressRunner();
IExpressContext<String,Object> context = new DefaultContext<String, Object>();
runner.addClassMethod("size", java.util.List.class, new Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
ArrayList arrayList = (ArrayList) list[0];
System.out.println("攔截到List.size()方法");
return arrayList.size();
}
});
runner.addClassField("長(zhǎng)度", java.util.List.class, new Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
ArrayList arrayList = (ArrayList) list[0];
System.out.println("攔截到List.長(zhǎng)度 字段的計(jì)算");
return arrayList.size();
}
});
Object result = runner.execute("list=new ArrayList();list.add(1);list.add(2);list.add(3);return list.size();",context,null,false,false);
System.out.println(result);
result = runner.execute("list=new ArrayList();list.add(1);list.add(2);list.add(3);return list.長(zhǎng)度;",context,null,false,false);
System.out.println(result);
//bugfix 沒(méi)有return 的時(shí)候可能會(huì)多次調(diào)用getType,并且返回錯(cuò)誤
Object result2 = runner.execute("list=new ArrayList();list.add(1);list.add(2);list.add(3);list.長(zhǎng)度;",context,null,false,false);
System.out.println(result2);
}
注意,這些類的字段和方法是執(zhí)行器通過(guò)解析語(yǔ)法執(zhí)行的,而不是通過(guò)字節(jié)碼增強(qiáng)等技術(shù),所以只在腳本運(yùn)行期間生效,不會(huì)對(duì)jvm整體的運(yùn)行產(chǎn)生任何影響,所以是絕對(duì)安全的。
語(yǔ)法樹(shù)解析變量、函數(shù)的API
這些接口主要是對(duì)一個(gè)腳本內(nèi)容的靜態(tài)分析,可以作為上下文創(chuàng)建的依據(jù),也可以用于系統(tǒng)的業(yè)務(wù)處理。 比如:計(jì)算 “a+fun1(a)+fun2(a+b)+c.getName()” 包含的變量:a,b,c 包含的函數(shù):fun1,fun2
//獲取一個(gè)表達(dá)式需要的外部變量名稱列表
String[] getOutVarNames(String express);
String[] getOutFunctionNames(String express);
語(yǔ)法解析校驗(yàn)API
腳本語(yǔ)法是否正確,可以通過(guò)ExpressRunner編譯指令集的接口來(lái)完成。
String expressString = "for(i=0;i<10;i++){sum=i+1}return sum;";
InstructionSet instructionSet = expressRunner.parseInstructionSet(expressString);
//如果調(diào)用過(guò)程不出現(xiàn)異常,指令集instructionSet就是可以被加載運(yùn)行(execute)了!
指令集緩存相關(guān)API
因?yàn)镼LExpress對(duì)文本到指令集做了一個(gè)本地HashMap緩存,通常情況下一個(gè)設(shè)計(jì)合理的應(yīng)用腳本數(shù)量應(yīng)該是有限的,緩存是安全穩(wěn)定的,但是也提供了一些接口進(jìn)行管理。
//優(yōu)先從本地指令集緩存獲取指令集,沒(méi)有的話生成并且緩存在本地
InstructionSet getInstructionSetFromLocalCache(String expressString);
//清除緩存
void clearExpressCache();
安全風(fēng)險(xiǎn)控制
防止死循環(huán)
try {
express = "sum=0;for(i=0;i<1000000000;i++){sum=sum+i;}return sum;";
//可通過(guò)timeoutMillis參數(shù)設(shè)置腳本的運(yùn)行超時(shí)時(shí)間:1000ms
Object r = runner.execute(express, context, null, true, false, 1000);
System.out.println(r);
throw new Exception("沒(méi)有捕獲到超時(shí)異常");
} catch (QLTimeOutException e) {
System.out.println(e);
}
防止調(diào)用不安全系統(tǒng)API
public void test_invoke_risk_api() {
ExpressRunner runner = new ExpressRunner();
QLExpressRunStrategy.setForbiddenInvokeSecurityRiskMethods(true);
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
try {
String express = "System.exit(1);";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
throw new Exception("沒(méi)有捕獲到不安全的方法");
} catch (QLException e) {
System.out.println(e);
} catch (Exception e) {
e.printStackTrace();
}
}
增強(qiáng)上下文參數(shù)Context相關(guān)的API
與spirng集成
上下文參數(shù) IExpressContext context 非常有用,它允許put任何變量,然后在腳本中識(shí)別出來(lái)。
在實(shí)際中我們很希望能夠無(wú)縫的集成到spring框架中,可以仿照下面的例子使用一個(gè)子類。
public class QLExpressContext extends HashMap<String, Object> implements
IExpressContext<String, Object> {
private ApplicationContext context;
//構(gòu)造函數(shù),傳入context和 ApplicationContext
public QLExpressContext(Map<String, Object> map,
ApplicationContext aContext) {
super(map);
this.context = aContext;
}
/**
* 抽象方法:根據(jù)名稱從屬性列表中提取屬性值
*/
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
自定義函數(shù)操作符獲取原始context控制上下文
自定義的Operator需要直接繼承OperatorBase,獲取到parent即可,可以用于在運(yùn)行一組腳本的時(shí)候,直接編輯上下文信息,業(yè)務(wù)邏輯處理上也非常有用。
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','錯(cuò)誤信息');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);
}
}
輸出內(nèi)容是
也就是說(shuō)原始的上下文沒(méi)有被污染
{success=false, warning=提醒信息, error=錯(cuò)誤信息}
其他
腳本預(yù)熱
可以通過(guò)提前加載腳本的方式來(lái)進(jìn)行預(yù)熱,防止初次編譯腳本帶來(lái)的耗時(shí)。
動(dòng)態(tài)規(guī)則推送
可以通過(guò)接入配置中心來(lái)實(shí)現(xiàn)規(guī)則的動(dòng)態(tài)推送。
流程設(shè)計(jì)器
因?yàn)檫@個(gè)流程引擎沒(méi)有流程設(shè)計(jì)器,所以可以看下http://m.itdecent.cn/p/9bb2f01ad816 我寫的另外一篇文章。