天天看點

QLExpress功能清單功能清單

QLExpress從一開始就是從複雜的阿裡電商業務系統出發,并且不斷完善的腳本語言解析引擎架構,在不追求java文法的完整性的前提下(比如異常處理,foreach循環,lambda表達式,這些都是groovy是強項),定制了很多普遍存在的業務需求解決方案(比如變量解析,spring打通,函數封裝,操作符定制,宏替換),同時在高性能、高并發、線程安全等方面也下足了功夫,久經考驗。

功能清單

QLExpressRunner如下圖所示,從文法樹分析、上下文、執行過程三個方面提供二次定制的功能擴充。

QLExpress功能清單功能清單

1、屬性開關

/**
     * ExpressRunner.java的構造函數
     * @param aIsPrecise 是否需要高精度計算支援
     * @param aIstrace 是否跟蹤執行指令的過程
     */
    public ExpressRunner(boolean aIsPrecise,boolean aIstrace)           

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 不滿足就停止運算。但業務系統卻還是希望把後面的邏輯都能夠運算一遍,并且輸出中間過程,保證更快更好的做出決策。

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);
           
如果要壓測性能,或者在正式環境使用,請把isCache設定為true
           

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整體的運作産生任何影響,是以是絕對安全的。

(5)文法樹解析變量、函數的API

這些接口主要是對一個腳本内容的靜态分析,可以作為上下文建立的依據,也可以用于系統的業務處理。

比如:計算 “a+fun1(a)+fun2(a+b)+c.getName()”

包含的變量:a,b,c

包含的函數:fun1,fun2

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

String[] getOutFunctionNames(String express);           

(6)文法解析校驗api

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

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

(7)指令集緩存相關的api

因為QLExpress對文本到指令集做了一個本地HashMap緩存,通常情況下一個設計合理的應用腳本數量應該是有限的,緩存是安全穩定的,但是也提供了一些接口進行管理。

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

(8)增強上下文參數Context相關的api

8.1 與spring架構的無縫內建

上下文參數 IExpressContext context 非常有用,它允許put任何變量,然後在腳本中識别出來。

在實際中我們很希望能夠無縫的內建到spring架構中,可以仿照下面的例子使用一個子類。

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);
    }
}