一、前言
最近时间,某公司给我们开发了一套系统,通过对源代码的分析,整套系统最重要的就是存储过程、公式计算、流程审批这三个模块,对于公式计算模块,我也一直在思考公式计算是如何实现的问题?可以通过配置类似excel的公式,然后由java来进行解析公式,返回结果?一次偶然的机会在Github上发现了阿里巴巴开源的QLExpress,它是一个动态脚本引擎解析工具,他有以下几个特性:
- **1、线程安全,**引擎运算过程中的产生的临时变量都是threadlocal类型。
- **2、高效执行,**比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当。
- **3、弱类型脚本语言,**和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强。
- **4、安全控制,**可以通过设置相关运行参数,预防死循环、高危系统api调用等情况。
- **5、代码精简,依赖最小,**250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用。
二、语法相关介绍
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.2.0</version>
</dependency>
复制代码
2.1 基础实现
//1.语法分析和计算
ExpressRunner runner = new ExpressRunner();
//2.存储上下文信息
DefaultContext<String,Object> context = new DefaultContext<>();
context.put("chinese",98);
context.put("math",100);
context.put("english",78);
//3.执行一段语句chinese+math+english,并且传入上下文
Object executeResult = runner.execute("chinese+math+english", context, null, true, false);
System.out.println(executeResult);
复制代码
2.2 与Java对象的区别
- 不支持try{}catch{}
- 不支持java8的lambda表达式
- 不支持for循环集合操作for (GRCRouteLineResultDTO item : list)
- 弱类型语言,请不要定义类型声明,更不要用Templete(Map<String,List>之类的)
- array的声明不一样
- min,max,round,print,println,like,in 都是系统默认函数的关键字,请不要作为变量名
2.3 扩展操作符:Operator
给相关操作符取别名
//语法分析和计算器
ExpressRunner runner = new ExpressRunner();
/**
* 取别名,对其中的符号取别名
*/
runner.addOperatorWithAlias("如果","if",null);
runner.addOperatorWithAlias("大于",">",null);
runner.addOperatorWithAlias("则","then",null);
runner.addOperatorWithAlias("否则","else",null);
//计算公式
String exp = "如果(chinese 大于 english) 则 {return 1;} 否则 {return 0;}";
//添加属性
DefaultContext<String,Object> context = new DefaultContext<>();
context.put("chinese",200);
context.put("english",100);
//执行操作
Object execute = runner.execute(exp, context, null, true, false);
System.out.println(execute);
复制代码
以上的内容,我们来分解一下内容:
- 定义语法分析和计算器
//语法分析和计算器
ExpressRunner runner = new ExpressRunner();
复制代码
- 给操作符取别名
/**
* 取别名,对其中的符号取别名
*/
runner.addOperatorWithAlias("如果","if",null);
runner.addOperatorWithAlias("大于",">",null);
runner.addOperatorWithAlias("则","then",null);
runner.addOperatorWithAlias("否则","else",null);
复制代码
- 定义计算公式和属性
//计算公式
String exp = "如果(chinese 大于 english) 则 {return 1;} 否则 {return 0;}";
//添加属性
DefaultContext<String,Object> context = new DefaultContext<>();
context.put("chinese",200);
context.put("english",100);
复制代码
- 执行操作
//执行操作
Object execute = runner.execute(exp, context, null, true, false);
System.out.println(execute);
复制代码
2.4 自定义Operator
创建自定义JoinOperator,继承Operator
/**
* 自定义Opperator
*/
public class JoinOperator extends Operator {
@Override
public Object executeInner(Object[] list) throws Exception {
Object opdata1 = list[0];
Object opdata2 = list[1];
//判断是否是集合,如果是集合就直接添加到list中
if(opdata1 instanceof java.util.List){
((java.util.List)opdata1).add(opdata2);
return opdata1;
}else{
//如果不是list,就创建list用来存储数据
List result = new ArrayList();
result.add(opdata1);
result.add(opdata2);
return result;
}
}
}
复制代码
第一种方法:addOperator,注入自定义的Operator
//语法分析和计算的入口类
ExpressRunner runner = new ExpressRunner();
//注入我们自定义的Operator,并且取别名为join,以后在调用的时候,在表达式中使用join即可
runner.addOperator("join",new JoinOperator());
DefaultContext<String,Object> context = new DefaultContext<>();
//表达式
String exp = "1 join 2 join 3";
//执行
Object result = runner.execute(exp, context, null, true, true);
System.out.println("result:"+result);
复制代码
22:23:56.337 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 1
22:23:56.338 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 2
22:23:56.338 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - join(1,2)
22:23:56.339 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 3
22:23:56.339 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - join([1, 2],3)
result:[1, 2, 3]
复制代码
第二种方法:replaceOperator
//语法分析和计算的入口类
ExpressRunner runner = new ExpressRunner();
//替换操作符
runner.replaceOperator("+",new JoinOperator());
DefaultContext<String,Object> context = new DefaultContext<>();
//表达式
String exp = "1 + 2 + 3";
//执行
Object result = runner.execute(exp, context, null, true, true);
System.out.println("result:"+result);
复制代码
22:27:29.475 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 1
22:27:29.477 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 2
22:27:29.477 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - +(1,2)
22:27:29.478 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 3
22:27:29.478 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - +([1, 2],3)
result:[1, 2, 3]
复制代码
第三种方法:addFunction
//语法分析和计算的入口类
ExpressRunner runner = new ExpressRunner();
//添加函数定义,使用的时候直接使用函数式-----> join(1,2)
runner.addFunction("join", new JoinOperator());
DefaultContext<String,Object> context = new DefaultContext<>();
//执行
Object result = runner.execute("join(1,2)", context, null, true, true);
System.out.println("result:"+result);
复制代码
22:29:59.091 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 1
22:29:59.093 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - LoadData 2
22:29:59.093 [main] DEBUG com.ql.util.express.instruction.detail.Instruction - join(1,2)
result:[1, 2]
复制代码
2.5 绑定java类或者对象的method
首先,需要定一个类,存在两个方法
- upper 转换为大写
- equels 两个字符串进行比对
public class ClassExample {
/**
* 转换为大写
* @param str 需要转换的字符串
* @return
*/
public static String upper(String str){
return str.toUpperCase();
}
/**
* 判断两个字符串是否相等
* @param str1
* @param str2
* @return
*/
public boolean equals(String str1,String str2){
return str1.equals(str2);
}
}
复制代码
然后,调用以上方法
- addFunctionOfClassMethod
public void addFunctionOfClassMethod(String name, String aClassName,
String aFunctionName, String[] aParameterTypes, String errorInfo)
/**
* 添加一个类的函数定义,例如:Math.abs(double) 映射为表达式中的 "取绝对值(-5.0)"
* Params:
* name – 函数名称(别名)
* aClassName – 类名称
* aFunctionName – 类中的方法名称
* aParameterTypes – 方法的参数类型名称
* errorInfo – 如果函数执行的结果是false,需要输出的错误信息
*/
复制代码
- 1.使用jdk内置方法
ExpressRunner runner = new ExpressRunner();
DefaultContext<String,Object> context = new DefaultContext<>();
/**
* 添加一个类的函数定义,例如:Math.abs(double) 映射为表达式中的 "取绝对值(-5.0)"
* Params:
* name – 函数名称(别名)
* aClassName – 类名称
* aFunctionName – 类中的方法名称
* aParameterTypes – 方法的参数类型名称
* errorInfo – 如果函数执行的结果是false,需要输出的错误信息
*/
runner.addFunctionOfClassMethod("取绝对值",Math.class.getName(),"abs",new String[]{"double"},null);
//这里根据addFunctionOfClassMethod定义的别名,直接调用
Object execute = runner.execute("取绝对值(-100)", context, null, true, true);
System.out.println(execute);
复制代码
- 2.使用自定义的类中的方法,如:调用ClassExample中的equels方法
ExpressRunner runner = new ExpressRunner();
DefaultContext<String,Object> context = new DefaultContext<>();
/**
* 添加一个类的函数定义,例如:Math.abs(double) 映射为表达式中的 "取绝对值(-5.0)"
* Params:
* name – 函数名称(别名)
* aClassName – 类名称
* aFunctionName – 类中的方法名称
* aParameterTypes – 方法的参数类型名称
* errorInfo – 如果函数执行的结果是false,需要输出的错误信息
*/
//因为example(str1,str2)有两个参数,所以在addFunctionOfClassMethod中第四个参数,需要指定两个参数的类型 new String[]{'String','String'}
runner.addFunctionOfClassMethod("example",ClassExample.class.getName(),"equals",new String[]{"String","String"},null);
//以上取了一个别名为example,使用的时候就直接使用example(yangzinan,mic)
Object execute = runner.execute("example(\"yangzinan\",\"mic\")", context, null, true, true);
System.out.println(execute);
复制代码
- addFunctionOfServiceMethod
public void addFunctionOfServiceMethod(String name, Object aServiceObject,
String aFunctionName, String[] aParameterTypes, String errorInfo)
/**
* 用于将一个用户自己定义的对象(例如Spring对象)方法转换为一个表达式计算的函数
* Params:
* name – 别名
* aServiceObject – 创建对象
* aFunctionName – 方法名
* aParameterTypes – 参数类型
* errorInfo –
*/
复制代码
ExpressRunner runner = new ExpressRunner();
DefaultContext<String,Object> context = new DefaultContext<>();
/**
* 用于将一个用户自己定义的对象(例如Spring对象)方法转换为一个表达式计算的函数
* Params:
* name – 别名
* aServiceObject – 创建对象
* aFunctionName – 方法名
* aParameterTypes – 参数类型
* errorInfo –
*/
runner.addFunctionOfServiceMethod("upper",new ClassExample(),"upper",new String[]{"String"},null);
Object execute = runner.execute("upper(\"yangzinan\")", context, null, true, true);
System.out.println(execute);
复制代码
2.6 macro宏定义
ExpressRunner runner = new ExpressRunner();
//定义宏
runner.addMacro("计算成绩平均值","(语文+数学+英语)/3.0");
//定义宏
runner.addMacro("是否优秀","计算成绩平均值>90");
//设置参数
DefaultContext<String,Object> context = new DefaultContext<>();
context.put("语文",90);
context.put("数学",98);
context.put("英语",101);
//执行
Object result = runner.execute("是否优秀", context, null, true, true);
System.out.println(result);
复制代码
2.7 查询外部需要定义的变量和函数
这个的意思就是:我们在表达式中定义了某一个定量,但是在类的内部并没有定义该属性,通过QL中的方法查询出来,没有定义的属性
ExpressRunner runner = new ExpressRunner(true,true);
//表达式
String exp = "a + b * c";
//获取一个表达式需要的外部变量名称列表
String[] outVarNames = runner.getOutVarNames(exp);
//遍历
for(String str:outVarNames){
System.out.println("需要定义的属性:"+str);
}
复制代码
今天就写这么多,等后面实际使用中再具体的弥补一些知识点,更多的api可以到官网中查看
https://github.com/alibaba/QLExpress
复制代码
近期评论