由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。
为了解决当时电商规则动态编译、表达式高精度计算、复杂布尔运算、自定义函数和操作符号、语法树生成等需求而设计的。
<dependency>
<groupId>com.alibabagroupId>
<artifactId>QLExpressartifactId>
<version>3.2.2version>
dependency>
Github地址:QLExpressionStudy
从语法树分析、上下文、执行过程三个方面提供二次定制的功能扩展
若要展示其中的编译过程,将log4j.properties中的log4j.logger.com.imooc.mapper改成DEBUG。
参见 TestQLExpress
类的 testQLExpression
函数:
ExpressRunner runner = new ExpressRunner();
/**
* 表达式计算的数据注入接口
*/
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a",1);
context.put("b",2);
context.put("c",3);
String express = "a+b*c";//===> 定义规则,计算表达式等等
Object r = runner.execute(express, context, null, true, true);// 解析规则+执行规则
System.out.println(r);
创建ExpressRunner的执行器示例时,可以设置下面两个参数用以达到精度计算。
aIsPrecise
是否需要高精度计算支持aIstrace
是否跟踪执行指令的过程针对执行器的execute方法,可以设置如下参数。
expressString
程序文本,即要执行的表达式或规则context
执行上下文。【IExpressContext对象(如果是Spring的Bean,则创建SpringBeanContext对象) 表示执行上下文】errorList
输出的错误信息ListisCache
是否使用Cache中的指令集,多次执行同一语句的情况下用以提高执行效率isTrace
是否输出详细的执行指令信息log
输出的log从简单的例子来看,QLExpress的运行过程为:
单词分解–>单词分析–>构建语法树进行语法分析–>生成运行期指令集合–>执行生成的指令集合。
其中前4个过程涉及语法的匹配运算等非常耗时,可以设置execute方法的 isCache 是否使用Cache中的指令集参数,它可以缓存前四个过程。
即把 express语句在本地缓存中换成一段指令,第二次重复执行的时候直接执行指令,极大的提高了性能。
或者ExpressRunner设置成singleton(结合spring是非常容易做到的)。
运算符 | 备注 | 示例 |
---|---|---|
+,-,*,/,<,>,<=,>=,==,!=,%,++,– |
mod 等同于% |
a * b |
in,like,&&,! |
in ,like 类似于sql语法 |
a in [1,2,3] |
for,break、continue,if then else |
不支持try{}catch{} 不支持java8的lambda表达式 不支持for循环集合操作 弱类型语言,请不要定义类型声明,更不要用Templete array的声明不一样 min,max,round,print,println,like,in 都是系统默认函数的关键字,请不要作为变量名 |
int n=10; int sum=0; for(int i=0;i |
分类 | 运算符 | 示例 |
---|---|---|
位运算 | ^ ,~ ,& ,| ,<< ,>> |
1<<2 |
四则运算 | + ,- ,* ,/ ,% ,++ ,-- |
3%2 |
Boolean运算 | ! ,< ,> ,<= ,>= ,== ,!= ,&& ,|| |
2==3 |
其他运算 | = ,?: |
2>3?1:0 |
n=10;
sum=0;
for(i=0;i
参见 TestQLExpress
类的 testExpressForOperater
函数。
a=1;
b=2;
maxnum = a>b?a:b;
参见 TestQLExpress
类的 testTernaryOperator
函数。
运算符 | 描述 | 运算符 | 描述 |
---|---|---|---|
mod | 与 % 等同 |
for | 循环语句控制符 |
return | 进行值返回 | if | 条件语句控制符 |
in | 类似sql语句的in | then | 与if同用 |
exportAlias | 创建别名,并转换为全局别名 | else | 条件语句控制符 |
alias | 创建别名 | break | 退出循环操作符 |
macro | 定义宏 | continue | 继续循环操作符 |
exportDef | 将局部变量转换为全局变量 | function | 进行函数定义 |
like | 类似sql语句的like | new | 创建一个对象 |
import | 引入包或类,需在脚本最前头 | class | 定义类 |
NewMap | 创建Map | NewList | 创建集合 |
部分说明:
include:在一个表达式中引入其它表达式。例如: include com.taobao.upp.b; 资源的转载可以自定义接口IExpressResourceLoader来实现,缺省是从文件中装载
[]:匿名创建数组.int[][] abc = [[11,12,13],[21,22,23]];
NewMap:创建HashMap. Map abc = NewMap(1:1,2:2);Map abc = NewMap("a":1,"b":2)
NewList:串接ArrayList.List abc = NewList(1,2,3);
exportDef: 将局部变量转换为全局变量,例如:exportDef long userId
alias:创建别名,例如: alias 用户ID user.userId
macro: 定义宏,例如: macro 降级 {level = level - 1}
in: 操作符号,例如: 3 in (3,4,5)
like:操作符号,例如: "abc" like ‘ab%‘
自定义系统函数
max:取最大值max(3,4,5)
min:最最小值min(2,9,1)
round:四舍五入round(19.08,1)
print:输出信息不换行print("abc")
println:输出信息并换行 println("abc")
n=10;
for(sum=0,i=0;i
参见 TestQLExpress
类的 testExpressForOperater
函数。
a=1;
b=2;
maxnum = a>b?a:b;
参见 TestQLExpress
类的 testTernaryOperator
函数。
keys = new ArrayList();
deviceName2Value = new HashMap();
deviceNames = ["ng","si","umid","ut","mac","imsi","imei"];
mins = [5,30];
参见 GrammarTest
类的 TestArrayCreate
函数。
object.amount*2+object.volume
参见 GrammarTest
类的 testJavaObjectOperate
函数。
import org.envision.tqw.study.BeanDefine.ObjectBean;
aObject = new ObjectBean();
aObject.setAmount(100);
aObject.volume = 20;
System.out.println("Volume:\n"+aObject.getVolume());
参见 GrammarTest
类的 testJavaObject2Operate
函数。
使用注意
aObject.volume = 20;
相当于调用 aObject.setVolume(20);
import
,而且是在脚本定义之前部分【参考java类的使用,import语句一般位于定义类的文件前面部分】。import
自定义类,默认会导入 import java.lang.*,import java.util.*;
list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for(i=0;i<list.size();i++){
item = list.get(i);
System.out.println(item);
}
参见 GrammarTest
类的 testArrayErgodicTest
函数。
map = new HashMap();
map.put("T","tan");
map.put("Q","qi");
map.put("W","wei");
keySet = map.keySet();
objArr = keySet.toArray();
for (i=0;i
参见 GrammarTest
类的 testMapErgodicTest
函数。
function add(int a,int b){
return a+b;
};
function sub(int a,int b){
return a - b;
};
a=10;
return add(a,4) + sub(a,9);
参见 FunctionTest
类的 testFunction1
函数。
装载表达式,但不执行,例如一些宏定义,或者自定义函数。
runner.loadMutilExpress("",funExp);
如果 (语文+数学+英语>270) 则 {return 1;} 否则 {return 0;}
参见 OperatorTest
类的 testOperateReplace
函数。
1 addT 22 addT 2
参见 OperatorTest
类的 testAddBinaryOperate
函数。
二元操作符定义示例
/**
* @ClassName: BinaryOperator
* @Description:
* 定义二元操作符,该操作符实现
* a + ( b + b)
* @Author: qiwei.tan
* @Date: 2019/8/26 16:05
* @Version: 1.0
*/
public class BinaryOperator extends Operator {
public Object executeInner(Object[] list) throws Exception {
int a = (Integer)list[0];
int b = (Integer)list[1];
return a + b + b;
}
}
4 addN (1,2,3)
参见 OperatorTest
类的 testNElementOperate
函数。
多元操作符定义示例
/**
* @ClassName: AddNOperator
* @Description:
* 多元操作符
* 4 addN (1,2,3)
* 4+1+2+3
* @Author: qiwei.tan
* @Date: 2019/8/29 15:35
* @Version: 1.0
*/
public class AddNOperator extends Operator {
public Object executeInner(Object[] list) throws Exception {
int r = 0;
for(int i=0;i<list.length;i++){
r = r + (Integer)list[i];
}
return r;
}
}
1 join 2 join 3
1 + 2 + 3
join(1,2,3)
/**
* @ClassName: JoinOperator
* @Description:
* 自定义连接操作符(二元或多元运算符【支持成函数列表调用】)
* 1 join 2 join 3 ===> [1,2,3]
* 返回ArrayList数组
* @Author: qiwei.tan
* @Date: 2019/8/29 11:39
* @Version: 1.0
*/
public class JoinOperator extends Operator {
public Object executeInner(Object[] list) throws Exception {
Object opdata1 = list[0];
Object opdata2 = list[1];
if(opdata1 instanceof java.util.List){
((java.util.List)opdata1).add(opdata2);
return opdata1;
}else{
java.util.List result = new java.util.ArrayList();
result.add(opdata1);
result.add(opdata2);
if(list.length > 2){
int index = -1;
for(Object obj:list)
if(++index < 2)
continue;
else
result.add(obj);
}
return result;
}
}
}
官方Readme文件的定义有误,无法支持当变成function时的多元操作符,故而更改。
runner.addOperator("join",new JoinOperator());
实现表达式中的词“name”与自定义操作符的绑定
表达式:
1 join 2 join 3
参见 OperatorTest
类的 testOperateUserDefine
函数。
runner.replaceOperator("+",new JoinOperator());
实现将内置的操作符定义变换为自定义操作符定义
表达式:
1 + 2 + 3
参见 OperatorTest
类的 testOperateUserDefine2
函数。
runner.addOperator("join",new JoinOperator());
实现表达式转换为函数的使用,实际上是多元运算符的一种操作
表达式:
join(1,2,3)
参见 OperatorTest
类的 testOperateUserDefine3
函数。
本部分不提供相应案例代码
1、提供表达式上下文,属性的值不需要在初始的时候全部加入,而是在表达式计算的时候,需要什么信息才通过上下文接口获取。
避免因为不知道计算的需求,而在上下文中把可能需要的数据都加入。
runner.execute(“三星卖家 and 消保用户”,errorList,true,expressContext) "三星卖家"和"消保用户"的属性是在需要的时候通过接口去获取。
2、可以将计算结果直接存储到上下文中供后续业务使用。例如:
runner.execute("c = 1000 + 2000",errorList,true,expressContext);
//则在expressContext中会增加一个属性c=3000,也可以在expressContext实现直接的数据库操作等。
3、支持高精度浮点运算,只需要在创建执行引擎的时候指定参数即可:new ExpressRunner(true,false);
4、可以将Class和Spring对象的方法映射为表达式计算中的别名,方便其他业务人员的立即和配置。例如
//将 Math.abs() 映射为 "取绝对值"。
runner.addFunctionOfClassMethod("取绝对值", Math.class.getName(), "abs",new String[] { "double" }, null);
runner.execute("取绝对值(-5.0)",null,true,null);
5、可以为已经存在的boolean运算操作符号设置别名,增加错误信息同步输出,在计算结果为false的时候,同时返回错误信息,减少业务系统相关的处理代码
runner.addOperatorWithAlias("属于", "in", "用户$1不在允许的范围")。
//用户自定义的函数同样也可以设置错误信息:例如:
runner.addFunctionOfClassMethod("isOk", BeanExample?.class.getName(),"isOk", new String[] { "String" }, "$1 不是VIP用户");
则在调用:
List errorList = new ArrayList?();
Object result =runner.execute("( (1+1) 属于 (4,3,5)) and isOk("玄难")",errorList,true,null);
// 执行结果 result = false.同时在errorList中还会返回2个错误原因:
// 1、"用户 2 不在允许的范围"
// 2、玄难 不是VIP用户
6、可以自定义函数,自定一个操作函数 group
class GroupOperator extends Operator {
public GroupOperator(String aName) {
this.name= aName;
}
public Object executeInner(Object[] list)throws Exception {
Object result = new Integer(0);
for (int i = 0; i < list.length; i++) {
result = OperatorOfNumber.Add.execute(result, list[i]);
}
return result;
}
}
则执行:
runner.addFunction("累加", new GroupOperator("累加"));
runner.addFunction("group", new GroupOperator("group"));
//则执行:group(2,3,4) = 9 ,累加(1,2,3)+累加(4,5,6)=21
7、可以自定操作符号。自定义的操作符号优先级设置为最高。例如自定一个操作函数 love:
class LoveOperator extends Operator {
public LoveOperator(String aName) {
this.name= aName;
}
public Object executeInner(Object[] list)
throws Exception {
String op1 = list[0].toString();
String op2 = list[1].toString();
String result = op2 +"{" + op1 + "}" + op2;
return result;
}
}
然后增加到运算引擎:
runner.addOperator("love", new LoveOperator("love"));
//则执行:'a' love 'b' love 'c' love 'd' = "d{c{b{a}b}c}d"
8、可以重载已有的操作符号。例如替换“+”的执行逻辑。
9、可以延迟运算需要的数据
10、一个脚本可以调用其它脚本定义的宏和函数
11、可以类似VB的语法来使用操作符号和函数。print abc; 等价于 print(abc).
12、支持类定义
13、对 in 操作支持后面的是一个数组或者List变量
addFunctionOfClassMethod
/**
* 添加一个类的函数定义,例如:Math.abs(double) 映射为表达式中的 "取绝对值(-5.0)"
* @param name 函数名称
* @param aClassName 类名称
* @param aFunctionName 类中的方法名称
* @param aParameterTypes 方法的参数类型名称
* @param errorInfo 如果函数执行的结果是false,需要输出的错误信息
* @throws Exception
*/
public void addFunctionOfClassMethod(String name, String aClassName,
String aFunctionName, String[] aParameterTypes, String errorInfo)
throws Exception{...}
addFunctionOfServiceMethod
/**
* 用于将一个用户自己定义的对象(例如Spring对象)方法转换为一个表达式计算的函数
* @param name
* @param aServiceObject
* @param aFunctionName
* @param aParameterTypes
* @param errorInfo
* @throws Exception
*/
public void addFunctionOfServiceMethod(String name, Object aServiceObject,
String aFunctionName, String[] aParameterTypes, String errorInfo)
throws Exception
两种方法,无论是何种形式,启用errorInfo一般都应该是boolean类型函数合适。
参见 GrammarTest
类的 testBindingClassOrObjectMethod
函数。
测试脚本
(语文+数学+英语)/3.0
计算平均成绩>90
是否优秀
参见 GrammarTest
类的 testMacro
函数。
脚本:
int 平均分 = (语文+数学+英语+综合考试.科目2)/4.0;
return 平均分;
参见 GrammarTest
类的 testVarNeedDef
函数。
脚本:
getTemplate([11,'22',33L,true])
getTemplate(11,'22',33L,true)
参见 GrammarTest
类的 testMethodReplace
函数。
脚本
abc = NewMap(1:1,2:2); return abc.get(1) + abc.get(2);
abc = NewList(1,2,3); return abc.get(1)+abc.get(2)
abc = [1,2,3]; return abc[1]+abc[2];
参见 GrammarTest
类的 testSetCreateFast
函数。
参见 PerfomanceTest
类的 testLocalCacheMutualImpact
函数。
running.checkSyntax(String text, boolean mockRemoteJavaClass, List remoteJavaClassNames)
参见 GrammarTest
类的 testCheckySyntax
函数。
runner的setIgnoreConstChar方法。
'a' -> "a"
参见 GrammarTest
类的 testIgnoreConstChar
函数。
参见 GrammarTest
类的 TestClassDefine
函数和TestClassDefine2
函数。
参见 GrammarTest
类的 testBitTest
函数。
参见 GrammarTest
类的 testImportClassPath
函数。
参见 GrammarTest
类的 testStack
函数。
针对runner的 isShortCircuit
属性,来判断,是否使用逻辑短路特性增强质量的效率
runner.setShortCircuit(false);
参见 GrammarTest
类的 testShortCircuit
函数和 testNoShortCircuit
函数。
参见 SceneOne 类。
在业务系统中存在一些逻辑判断,例如 "商品A"只能是三星卖家,而且已经开通淘宝店铺的用户才能订购 。
那么我们期望业务人员看到的配置信息就是:“三星卖家 而且 已经开店”
则通过下列步骤可能实现这个功能:
class UserInfo {
long id;
long tag;
String name;
public UserInfo(long aId,String aName, long aUserTag) {
this.id = aId;
this.tag = aUserTag;
this.name = aName;
}
public String getName(){
return name;
}
public long getUserId() {
return id;
}
public long getUserTag() {
return tag;
}
}
/**
* 判断一个用户TAG的第X位是否为1。这个的demo,其实现合理性不考虑
* @param user
* @param tagBitIndex
* @return
*/
public boolean userTagJudge(UserInfo user,int tagBitIndex){
boolean r = (user.getUserTag() & ((long)Math.pow(2, tagBitIndex))) > 0;
return r;
}
/**
* 判断一个用户是否订购过某个商品
* @param user
* @param goodsId
* @return
*/
public boolean hasOrderGoods(UserInfo user,long goodsId){
//随机模拟一个
if(user.getUserId() % 2 == 1){
return true;
}else{
return false;
}
}
public void initial() throws Exception{
runner.addOperatorWithAlias("而且","and",null);
runner.addFunctionOfClassMethod("userTagJudge", SceneOne .class.getName(), "userTagJudge",new String[] {UserInfo.class.getName(),"int"}, "你不是三星卖家");
runner.addFunctionOfClassMethod("hasOrderGoods", SceneOne .class.getName(), "hasOrderGoods",new String[] {UserInfo.class.getName(),"long"}, "你没有开通淘宝店铺");
runner.addMacro("三星卖家", "userTagJudge(userInfo,3)");//3表示三星卖家的标志位
runner.addMacro("已经开店", "hasOrderGoods(userInfo,100)");//100表示旺铺商品的ID
}
/**
* 判断逻辑执行函数
* @param userInfo
* @param expression
* @return
* @throws Exception
*/
public String hasPermission(UserInfo userInfo,String expression) throws Exception {
IExpressContext<String,Object> expressContext = new DefaultContext<String,Object>();
expressContext.put("userInfo",userInfo);
List<String> errorInfo = new ArrayList<String>();
Boolean result = (Boolean)runner.execute(expression, expressContext, errorInfo, true, false);
String resultStr ="";
if(result.booleanValue() == true){
resultStr = "可以订购此商品";
}else{
for(int i=0;i<errorInfo.size();i++){
if(i > 0){
resultStr = resultStr + "," ;
}
resultStr = resultStr + errorInfo.get(i);
}
resultStr = resultStr + ",所以不能订购此商品";
}
return "亲爱的" + userInfo.getName() + " : " + resultStr;
}
public static void main(String args[]) throws Exception{
SceneOne demo = new SceneOne();
demo.initial();
System.out.println(demo.hasPermission(new UserInfo(100,"xuannan",7), "三星卖家 而且 已经开店"));
System.out.println(demo.hasPermission(new UserInfo(101,"qianghui",8), "三星卖家 而且 已经开店"));
System.out.println(demo.hasPermission(new UserInfo(100,"张三",8), "三星卖家 and 已经开店"));
System.out.println(demo.hasPermission(new UserInfo(100,"李四",7), "三星卖家 and 已经开店"));
简单的流程管理,本场景涉及用于展示如何定义表达式,方法,并使用上下文变量。
脚本如下:
如果 (审批通过(经理,金额)){
如果 (金额 大于 5000){
如果 (审批通过(总监,金额)){
如果 (审批通过(财务,金额)){
报销入账(金额)
}否则 {
打回修改(申请人)
}
}否则 {
打回修改(申请人)
}
}否则 {
如果 (审批通过(财务,金额)){
报销入账(金额)
}否则 {
打回修改(申请人)
}
}
}否则 {
打回修改(申请人)
}
打印("完成");
查看SceneTwo
类
非加权:
费用科目(物流订单.仓储TP,"仓储费")= 物流订单.重量 * 0.5 ;
if(物流订单.重量 > 5) then{
费用科目(物流订单.物流TP,"运输费")= 3.0 + (物流订单.重量 - 5 ) * 1.5 ;
} else {
费用科目(物流订单.物流TP,"运输费")= 3.0;
};
费用科目(物流订单.包装TP,"包装费")= 物流订单.重量 * 2.5 ;
加权:
费用科目(物流订单.仓储TP,"仓储费")= 物流订单.重量 * 0.5 ;
if(物流订单.重量 > 5) then{
费用科目(物流订单.物流TP,"运输费")= 3.0 + (物流订单.重量 - 5 ) * 1.5 ;
} else {
费用科目(物流订单.物流TP,"运输费")= 3.0;
};
费用科目(物流订单.包装TP,"包装费")= 物流订单.重量 * 2.5 ;
查看SceneThree
类
示例来源于官方文案