QLExpress规则引擎

一、前言

最近时间,某公司给我们开发了一套系统,通过对源代码的分析,整套系统最重要的就是存储过程、公式计算、流程审批这三个模块,对于公式计算模块,我也一直在思考公式计算是如何实现的问题?可以通过配置类似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
复制代码