QlExpress使用

背景和特性

背景

由阿里的電商業(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)圖

image.png

三個(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é)

  1. 生成Runner

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

  1. 上下文傳參

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

  1. 定義表達(dá)式
  2. 執(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 我寫的另外一篇文章。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容