QLExpress從一開始就是從複雜的阿裡電商業務系統出發,并且不斷完善的腳本語言解析引擎架構,在不追求java文法的完整性的前提下(比如異常處理,foreach循環,lambda表達式,這些都是groovy是強項),定制了很多普遍存在的業務需求解決方案(比如變量解析,spring打通,函數封裝,操作符定制,宏替換),同時在高性能、高并發、線程安全等方面也下足了功夫,久經考驗。
功能清單
QLExpressRunner如下圖所示,從文法樹分析、上下文、執行過程三個方面提供二次定制的功能擴充。
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.java8.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);
}
}