QlExpress使用

背景和特性

背景

由阿里的電商業(yè)務(wù)規(guī)則、表達式(布爾組合)歼跟、特殊數(shù)學(xué)公式計算(高精度)和媳、語法分析、腳本二次定制等強需求而設(shè)計的一門動態(tài)腳本引擎解析工具哈街。 在阿里集團有很強的影響力留瞳,同時為了自身不斷優(yōu)化针炉、發(fā)揚開源貢獻精神胶征,于2012年開源棚贾。

特性

  • 1夺英、線程安全茉唉,引擎運算過程中的產(chǎn)生的臨時變量都是threadlocal類型浩销。

  • 2妈经、高效執(zhí)行临梗,比較耗時的腳本編譯過程可以緩存在本地機器胞得,運行時的臨時變量創(chuàng)建采用了緩沖池的技術(shù)荧止,和groovy性能相當。

  • 3、弱類型腳本語言罩息,和groovy嗤详,javascript語法類似,雖然比強類型腳本語言要慢一些瓷炮,但是使業(yè)務(wù)的靈活度大大增強葱色。

  • 4、安全控制,可以通過設(shè)置相關(guān)運行參數(shù)娘香,預(yù)防死循環(huán)苍狰、高危系統(tǒng)api調(diào)用等情況。

  • 5烘绽、代碼精簡淋昭,依賴最小,250k的jar包適合所有java的運行環(huán)境安接,在android系統(tǒng)的低端pos機也得到廣泛運用翔忽。

基本概念

架構(gòu)圖

image.png

三個核心組件

表達式express

  • 直接寫在java類中 注意使用ql語法

  • 寫在ql文件中 ql文件中讀取表達式

上下文context

運行時 runner

  • 單例模式

快速上手

引入pom依賴

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>QLExpress</artifactId>
  <version>3.2.0</version>
</dependency>

寫個簡單的例子計算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);
    //下面五個參數(shù)意義分別是 表達式,上下文盏檐,errorList歇式,是否緩存,是否輸出日志
    Object result = runner.execute("a+b+c", context, null, true, false);
    System.out.println("a+b+c=" + result);
}

步驟總結(jié)

  1. 生成Runner

實際使用建議是單例的胡野,看下上面的架構(gòu)圖

  1. 上下文傳參

生成DefaultContext材失,并往里面添加數(shù)據(jù)

  1. 定義表達式
  2. 執(zhí)行得到結(jié)果

語法

操作符和java對象操作

普通java語法

  • 支持 +,-,*,/,<,>,<=,>=,==,!=,<>【等同于!=】,%,mod【取模等同于%】,++,--,

  • in【類似sql】,like【sql語法】,&&,||,!,等操作符

  • 支持for,break硫豆、continue龙巨、if then else 等標準的程序控制邏輯

  • 三目運算符

下面展示一些基本的例子

  • 計算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);
}
  • 三目運算符
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);
}

注意點

和java語法相比,要避免ql的一些寫法錯誤

  • 不支持try{}catch{}

  • 不支持java8的lambda表達式

  • 不支持for循環(huán)集合操作for (GRCRouteLineResultDTO item : list)

    • 通過數(shù)組方式訪問
  • 弱類型語言熊响,請不要定義類型聲明,更不要用Templete(Map<String,List>之類的)

  • array的聲明不一樣

    • 使用[]
  • min,max,round,print,println,like,in 都是系統(tǒng)默認函數(shù)的關(guān)鍵字旨别,請不要作為變量名

數(shù)組

  • 聲明的方式類似arr=new int[3];
  • 聲明并賦值的時候和java有點不同
    • mins=[5,30];
  • 迭代的時候?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(); 不用范型

  • 迭代的時候?qū)傩詌ist.size()

  • 取的時候用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()

  • 迭代的時候先生成keySet

  • keySet.toArray()獲取keyArray

  • 迭代keyArray獲取key

  • 通過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);
}

語法糖

可以使用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)自動會import java.lang.,import java.util.;
  • 創(chuàng)建對象后可以調(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只會引入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非常相似。
注意點

  • 在入?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);
}

擴展操作符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("語文", 100);
    context.put("數(shù)學(xué)", 100);
    context.put("英語", 100);
    String express = "如果 ((語文+數(shù)學(xué)+英語)>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類型或者對象的method

  • addFunctionOfClassMethod 綁定靜態(tài)方法
  • addFunctionOfServiceMethod 綁定實例方法

參數(shù)說明:

  • 函數(shù)名稱
  • 類名稱或者對象實例
  • 方法名稱
  • 方法的參數(shù)類型
  • 錯誤信息 如果函數(shù)執(zhí)行的結(jié)果是false昼榛,需要輸出的錯誤信息
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();

runner.addFunctionOfClassMethod("取絕對值", 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 = "取絕對值(-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("計算平均成績", "(語文+數(shù)學(xué)+英語)/3.0");
    runner.addMacro("是否優(yōu)秀", "計算平均成績>90");
    context.put("語文", 88);
    context.put("數(shù)學(xué)", 99);
    context.put("英語", 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 平均分 = (語文+數(shù)學(xué)+英語+綜合考試.科目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);
}

使用動態(tài)參數(shù)

注意需要打開動態(tài)開關(guān) DynamicParamsUtil.supportDynamicParams = true;否則會拋出異常

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的語法奥喻,只是ql不支持for(obj:list){}的語法偶宫,只能通過下標訪問。
詳細見 操作符和java對象操作 這一小節(jié)的示例

運行參數(shù)和api介紹

屬性開關(guān)

isPrecise

高精度計算在會計財務(wù)中非常重要环鲤,java的float纯趋、double、int、long存在很多隱式轉(zhuǎn)換吵冒,做四則運算和比較的時候其實存在非常多的安全隱患纯命。 所以類似匯金的系統(tǒng)中,會有很多BigDecimal轉(zhuǎn)換代碼痹栖。而使用QLExpress亿汞,你只要關(guān)注數(shù)學(xué)公式本身 訂單總價 = 單價 * 數(shù)量 + 首重價格 + ( 總重量 - 首重) * 續(xù)重單價 ,然后設(shè)置這個屬性即可揪阿,所有的中間運算過程都會保證不丟失精度疗我。

/**
* 是否需要高精度計算
*/
private boolean isPrecise = false;
public void is_precise() throws Exception {
    ExpressRunner runner = new ExpressRunner(true, false);
    DefaultContext<String, Object> context = new DefaultContext<String, Object>();
    //訂單總價 = 單價 * 數(shù)量 + 首重價格 + (總重量 - 首重) * 續(xù)重單價
    context.put("單價", 1.25);
    context.put("數(shù)量", 100);
    context.put("首重價格", 125.25);
    context.put("總重量", 20.55);
    context.put("首重", 10.34);
    context.put("續(xù)重單價", 3.33);

    String express = "單價*數(shù)量+首重價格+(總重量-首重)*續(xù)重單價";
    Object totalPrice = runner.execute(express, context, null, true, false);
    System.out.println("totalPrice:" + totalPrice);
}

上面在創(chuàng)建ExpressRunner的時候第一個參數(shù)是是否開啟高精度默認false,第二個參數(shù)是是否開啟trace南捂。
開啟高精度的情況下輸出是284.2493
沒有開啟高精度的情況下輸出是284.2493

isShortCicuit

/**
* 是否使用邏輯短路特性
*/
private boolean isShortCircuit = true;

在很多業(yè)務(wù)決策系統(tǒng)中吴裤,往往需要對布爾條件表達式進行分析輸出,普通的java運算一般會通過邏輯短路來減少性能的消耗溺健。例如規(guī)則公式: star>10000 and shoptype in('tmall','juhuasuan') and price between (100,900) 假設(shè)第一個條件 star>10000 不滿足就停止運算麦牺。但業(yè)務(wù)系統(tǒng)卻還是希望把后面的邏輯都能夠運算一遍,并且輸出中間過程鞭缭,保證更快更好的做出決策剖膳。
具體可以參見ShortCircuitLogicTest測試類

runner.setShortCircuit(true);
runner.setShortCircuit(false);//顯式指定,默認是true

isTrace

/**
* 是否輸出所有的跟蹤信息缚去,同時還需要log級別是DEBUG級別
*/
private boolean isTrace = false;
ExpressRunner runner = new ExpressRunner(true, false);//第二個參數(shù)是否開啟trace

這個主要是是否輸出腳本的編譯解析過程潮秘,一般對于業(yè)務(wù)系統(tǒng)來說關(guān)閉之后會提高性能。

調(diào)用入?yún)?/h2>

對于runner.execute執(zhí)行方法的參數(shù)入?yún)⒄f明

/**
 * 執(zhí)行一段文本
 * @param expressString 程序文本
 * @param context 執(zhí)行上下文易结,可以擴展為包含ApplicationContext
 * @param errorList 輸出的錯誤信息List
 * @param isCache 是否使用Cache中的指令集,建議為true
 * @param isTrace 是否輸出詳細的執(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);

功能擴展API列表

QLExpress主要通過子類實現(xiàn)Operator.java提供的以下方法來最簡單的操作符定義,然后可以被通過addFunction或者addOperator的方式注入到ExpressRunner中搞动。
具體參見上面的JoinOperator的例子躏精。
如果你使用Operator的基類OperatorBase.java將獲得更強大的能力,基本能夠滿足所有的要求鹦肿。

function相關(guān)API

//通過name獲取function的定義
OperatorBase getFunciton(String name);

//通過自定義的Operator來實現(xiàn)類似: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) 兩種方法調(diào)用
//比如擴展String.class的isBlank方法:“abc”.isBlank()和isBlank("abc")都可以調(diào)用
void addFunctionAndClassMethod(String name,Class<?>bindingClass, OperatorBase op);

Operator相關(guān)API

提到腳本語言的操作符箩溃,優(yōu)先級瞭吃、運算的目數(shù)、覆蓋原始的操作符(+,-,*,/等等)都是需要考慮的問題涣旨,QLExpress統(tǒng)統(tǒng)幫你搞定了歪架。

//添加操作符號,可以設(shè)置優(yōu)先級
void addOperator(String name,Operator op);
void addOperator(String name,String aRefOpername,Operator op);
    
//替換操作符處理
//比如將+替換成自定義的operator 見上面的例子
OperatorBase replaceOperator(String name,OperatorBase op);
    
//添加操作符和關(guān)鍵字的別名,比如 if..then..else -> 如果霹陡。和蚪。那么止状。。否則攒霹。怯疤。
//具體見上面的例子
void addOperatorWithAlias(String keyWordName, String realKeyWordName,
            String errorInfo);

宏相關(guān)API

QLExpress的宏定義比較簡單,就是簡單的用一個變量替換一段文本催束,和傳統(tǒng)的函數(shù)替換有所區(qū)別集峦。
見上面使用宏計算是否優(yōu)秀的例子

//比如addMacro("天貓賣家","userDO.userTag &1024 ==1024")
void addMacro(String macroName,String express) 

java class相關(guān)API

QLExpress可以通過給java類增加或者改寫一些method和field,比如鏈式調(diào)用:"list.join("1").join("2")"泣崩,比如中文屬性:"list.長度"少梁。

//添加類的屬性字段
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("長度", java.util.List.class, new Operator() {
        @Override
        public Object executeInner(Object[] list) throws Exception {
            ArrayList arrayList = (ArrayList) list[0];
            System.out.println("攔截到List.長度 字段的計算");
            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.長度;",context,null,false,false);
    System.out.println(result);

    //bugfix 沒有return 的時候可能會多次調(diào)用getType,并且返回錯誤
    Object result2  = runner.execute("list=new ArrayList();list.add(1);list.add(2);list.add(3);list.長度;",context,null,false,false);
    System.out.println(result2);
}

注意矫付,這些類的字段和方法是執(zhí)行器通過解析語法執(zhí)行的凯沪,而不是通過字節(jié)碼增強等技術(shù),所以只在腳本運行期間生效买优,不會對jvm整體的運行產(chǎn)生任何影響妨马,所以是絕對安全的。

語法樹解析變量杀赢、函數(shù)的API

這些接口主要是對一個腳本內(nèi)容的靜態(tài)分析烘跺,可以作為上下文創(chuàng)建的依據(jù),也可以用于系統(tǒng)的業(yè)務(wù)處理脂崔。 比如:計算 “a+fun1(a)+fun2(a+b)+c.getName()” 包含的變量:a,b,c 包含的函數(shù):fun1,fun2

//獲取一個表達式需要的外部變量名稱列表
String[] getOutVarNames(String express);

String[] getOutFunctionNames(String express);

語法解析校驗API

腳本語法是否正確滤淳,可以通過ExpressRunner編譯指令集的接口來完成。

String expressString = "for(i=0;i<10;i++){sum=i+1}return sum;";
InstructionSet instructionSet = expressRunner.parseInstructionSet(expressString);
//如果調(diào)用過程不出現(xiàn)異常砌左,指令集instructionSet就是可以被加載運行(execute)了脖咐!

指令集緩存相關(guān)API

因為QLExpress對文本到指令集做了一個本地HashMap緩存,通常情況下一個設(shè)計合理的應(yīng)用腳本數(shù)量應(yīng)該是有限的汇歹,緩存是安全穩(wěn)定的屁擅,但是也提供了一些接口進行管理。

//優(yōu)先從本地指令集緩存獲取指令集产弹,沒有的話生成并且緩存在本地
InstructionSet getInstructionSetFromLocalCache(String expressString);
//清除緩存
void clearExpressCache();

安全風(fēng)險控制

防止死循環(huán)

try {
    express = "sum=0;for(i=0;i<1000000000;i++){sum=sum+i;}return sum;";
    //可通過timeoutMillis參數(shù)設(shè)置腳本的運行超時時間: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);
}

防止調(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("沒有捕獲到不安全的方法");
    } catch (QLException e) {
        System.out.println(e);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

增強上下文參數(shù)Context相關(guān)的API

與spirng集成

上下文參數(shù) IExpressContext context 非常有用派歌,它允許put任何變量,然后在腳本中識別出來痰哨。
在實際中我們很希望能夠無縫的集成到spring框架中胶果,可以仿照下面的例子使用一個子類。

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è)務(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','錯誤信息');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)容是
也就是說原始的上下文沒有被污染

{success=false, warning=提醒信息, error=錯誤信息}

其他

腳本預(yù)熱

可以通過提前加載腳本的方式來進行預(yù)熱咪奖,防止初次編譯腳本帶來的耗時。

動態(tài)規(guī)則推送

可以通過接入配置中心來實現(xiàn)規(guī)則的動態(tài)推送酱床。

流程設(shè)計器

因為這個流程引擎沒有流程設(shè)計器羊赵,所以可以看下http://www.reibang.com/p/9bb2f01ad816 我寫的另外一篇文章。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扇谣,一起剝皮案震驚了整個濱河市昧捷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌罐寨,老刑警劉巖靡挥,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鸯绿,居然都是意外死亡跋破,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門瓶蝴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毒返,“玉大人,你說我怎么就攤上這事舷手∨◆ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵男窟,是天一觀的道長盆赤。 經(jīng)常有香客問我,道長蝎宇,這世上最難降的妖魔是什么弟劲? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮姥芥,結(jié)果婚禮上兔乞,老公的妹妹穿的比我還像新娘。我一直安慰自己凉唐,他們只是感情好庸追,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著台囱,像睡著了一般淡溯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上簿训,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天咱娶,我揣著相機與錄音米间,去河邊找鬼。 笑死膘侮,一個胖子當著我的面吹牛屈糊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琼了,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逻锐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了雕薪?” 一聲冷哼從身側(cè)響起昧诱,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎所袁,沒想到半個月后盏档,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡纲熏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年妆丘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片局劲。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡勺拣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鱼填,到底是詐尸還是另有隱情药有,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布苹丸,位于F島的核電站愤惰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赘理。R本人自食惡果不足惜宦言,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望商模。 院中可真熱鬧奠旺,春花似錦、人聲如沸施流。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞪醋。三九已至忿晕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間银受,已是汗流浹背践盼。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工鸦采, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咕幻。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓赖淤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谅河。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容