可视化逻辑表达式编辑器

优质博文:IT-BLOG-CN

一、QueryBuilder介绍

QueryBuilder 是一个用于创建查询和过滤器的 UI 组件。

QueryBuilder的特点

1、支持的输入属性丰富,常见的 字符串,整数,浮点数,布尔类型,日期类型,数组列表等都支持

2、高度可定制。QueryBuilder是由规则以及规则组组合而成的查询以及过滤组件,规则以及规则组可以层层嵌套,所以复杂的规则也可以配置出来。

3、支持的生成脚本语句多。QueryBuilder的最终目的是,通过配置规则树,最终把规则树转化为我们想要的脚本语句。

目前支持的脚本语句有 groovy脚本,jsonLogic脚本,mpsql脚本等目前市面上的QueryBuilder很多,功能大同小异,我们选用的是react-awesome-query-builder,功能性以及可用性相比较是最好的

QueryBuilder的详细介绍

QueryBuilder主要分为以下几个部分:

规则以及规则组

1、规则可以理解为一条判断语句,比如今天天气很好,或者今天天气不好,它的结果是一个布尔判断,即是或者否

2、规则组,则是多个规则的组合,比如 当前时间大于8点并且当前时间小于21点并且今天不是周末,他是老师或者他是公务员,它是有多个布尔判断拼接而成,结果也是一个布尔值

连接词(conjunctions) 连接词是用来连接规则与规则或者规则组与规则组,或者规则与规则组之间关系的逻辑词,连接词有三种,不是,或者,并且,对应的逻辑符号是 !,||, &&

输入属性(widgets),或者叫做左值(leftValue) 输入属性支持的类型有 文本/数值/单选值/多选值/日期类型/布尔类型/函数

操作符(operators) 操作符是用来连接输入属性以及期望值的逻辑符号

1、不同的输入属性会有不同的操作符

2、对于文本类型,对应的操作符有["等于","不等于","包含","包含数组项","字符开头是","字符结尾是","正则匹配","为空","不为空"]

3、对于数值类型,对应的操作符有["等于","不等于","小于","小于等于","大于","大于等于","范围在","范围不在","为空","不为空"]

4、对于数组类型,对应的操作符有["等于","不等于","等于其中一个","不等于其中一个","包含其中一个"]

5、对于布尔类型,对应的操作符有["等于","不等于"]

期望值,或者叫做右值(rightValue) 期望值就是我们希望在这个规则里命中的值,如果输入值跟期望值相匹配,那么这条规则就返回true,否则这条规则就返回false

总结一下:输入属性 + 操作符 + 期望值 就组合形成一个规则

规则+ 连接词+规则 可以组成一个规则组

规则 和 规则组 相互嵌套,可以形成一个规则树

比如截图里面的规则树 就是由两个规则+两个规则组嵌套组合而成

接下来我来介绍一下怎么由规则树解析成我们想要的groovy脚本语言

举例说明规则的解析过程

先看单条规则是怎样解析成groovy脚本的

groovy 复制代码
equal: {
        label: '等于',
        groovy: (leftValue, rightValue) => `${leftValue} == ${rightValue}`,
        labelForFormat: '==',
        sqlOp: '=',
        reversedOp: 'not_equal',
        formatOp: (field, op, value, valueSrcs, valueTypes, opDef, operatorOptions, isForDisplay, fieldDef) => {
            if (valueTypes == 'boolean' && isForDisplay)
                return value == 'No' ? `NOT ${field}` : `${field}`;
            else
                return `${field} ${opDef.label} ${value}`;
        },
        mongoFormatOp: mongoFormatOp1.bind(null, '$eq', v => v, false),
        jsonLogic: '==',
    },
like: {
        label: '包含',
        groovy: (leftValue, rightValue) => `${leftValue}.contains(${rightValue})`,
        labelForFormat: 'Like',
        reversedOp: 'not_like',
        sqlOp: 'LIKE',
        sqlFormatOp: (field, op, values, valueSrc, valueType, opDef, operatorOptions) => {
            if (valueSrc == 'value') {
                return `${field} LIKE ${values}`;
            } else return undefined; // not supported
        },
        mongoFormatOp: mongoFormatOp1.bind(null, '$regex', v => (typeof v == 'string' ? escapeRegExp(v) : undefined), false),
        //jsonLogic: (field, op, val) => ({ "in": [val, field] }),
        jsonLogic: "in",
        _jsonLogicIsRevArgs: true,
        valueSources: ['value'],
    },
between: {
        label: '范围在',
        groovy: (leftValue, rightValue) => `(${leftValue} >= ${rightValue[0]} && ${leftValue} <= ${rightValue[1]}) `,
        labelForFormat: 'BETWEEN',
        sqlOp: 'BETWEEN',
        cardinality: 2,
        formatOp: (field, op, values, valueSrcs, valueTypes, opDef, operatorOptions, isForDisplay) => {
            let valFrom = values.first();
            let valTo = values.get(1);
            if (isForDisplay)
                return `${field} >= ${valFrom} AND ${field} <= ${valTo}`;
            else
                return `${field} >= ${valFrom} && ${field} <= ${valTo}`;
        },
        mongoFormatOp: mongoFormatOp2.bind(null, ['$gte', '$lte'], false),
        valueLabels: [
            '开始值',
            '结束值'
        ],
        textSeparators: [
            null,
            '到'
        ],
        reversedOp: 'not_between',
        jsonLogic: "<=",
    }, 

由于操作符有很多,他们的解析过程比较类似,我就不一一介绍了。

规则树由规则以及规则组嵌套而成,最后生成的数据结构就是一个树状结构

下图是上面的示例对应的规则树结构

groovy 复制代码
{
    "id":"99aa8a99-4567-489a-bcde-f18de0917366",
    "type":"group",
    "children1":{
        "b8b8abba-89ab-4cde-b012-318de0917366":{
            "type":"rule",
            "properties":{
                "field":"weather",
                "operator":"equal",
                "value":[
                    "晴"
                ],
                "valueSrc":[
                    "value"
                ],
                "valueType":[
                    "text"
                ]
            }
        },
        "9b898a88-4567-489a-bcde-f18de0917366":{
            "type":"rule",
            "properties":{
                "field":"weekday",
                "operator":"select_any_in",
                "value":[
                    [
                        "6",
                        "7"
                    ]
                ],
                "valueSrc":[
                    "value"
                ],
                "valueType":[
                    "multiselect"
                ]
            }
        },
        "aa989a8a-0123-4456-b89a-b18de0923911":{
            "type":"group",
            "properties":{
                "conjunction":"AND"
            },
            "children1":{
                "89b8abab-cdef-4012-b456-718de0923912":{
                    "type":"rule",
                    "properties":{
                        "field":"phone",
                        "operator":"match",
                        "value":[
                            "^1[3456789]\\d{9}$"
                        ],
                        "valueSrc":[
                            "value"
                        ],
                        "valueType":[
                            "text"
                        ]
                    }
                },
                "898baa88-89ab-4cde-b012-318de092c6b8":{
                    "type":"rule",
                    "properties":{
                        "field":"isOpen",
                        "operator":"equal",
                        "value":[
                            true
                        ],
                        "valueSrc":[
                            "value"
                        ],
                        "valueType":[
                            "boolean"
                        ]
                    }
                }
            }
        },
        "a9a8baa9-4567-489a-bcde-f18de092e65a":{
            "type":"group",
            "properties":{
                "conjunction":"AND"
            },
            "children1":{
                "b8a8b99b-0123-4456-b89a-b18de092e65a":{
                    "type":"rule",
                    "properties":{
                        "field":"time",
                        "operator":"greater",
                        "value":[
                            8
                        ],
                        "valueSrc":[
                            "value"
                        ],
                        "valueType":[
                            "number"
                        ]
                    }
                },
                "89bbb8bb-cdef-4012-b456-718de093f996":{
                    "type":"rule",
                    "properties":{
                        "field":"time",
                        "operator":"less",
                        "value":[
                            17
                        ],
                        "valueSrc":[
                            "value"
                        ],
                        "valueType":[
                            "number"
                        ]
                    }
                }
            }
        }
    },
    "properties":{
        "conjunction":"AND"
    }
} 

知道了单条规则翻译以及规则树结构,就可以把规则树对应的groovy脚本翻译出来

groovy 复制代码
const ruleGroupToGroovy = (ruleGroup, contract) => {
    let conjunction = ruleGroup.properties.conjunction === "OR" ? " || " : " && ";
    let not = ruleGroup.properties.not ? "!" : "";
 
    let segments = [];
    for (let id in ruleGroup.children1) {
        let child = ruleGroup.children1[id];
        let segment = null;
        if (child.type === "group") {
            segment = ruleGroupToGroovy(child, contract);
        }
        else {
            segment = ruleToGroovy(child, contract);
        }
        if (segment) {
            segments.push(segment);
        }
    }
 
    if (segments.length === 0) {
        return "";
    }
    else if (segments.length === 1) {
        return not + segments[0];
    }
    else {
        let script = not + "(" + segments[0];
        for (let i = 1; i < segments.length; i++) {
            script += conjunction + segments[i];
        }
        script = script + ")";
        return script;
    }
}; 

由于我们是Java项目,所以我们需要groovy脚本即可,上述流程树翻译好的groovy脚本

groovy 复制代码
(weather == "晴" && weekday in ["6","7"] && (!!(phone =~ /^1[3456789]\d{9}$/) && isOpen == true) && (time > 8 && time < 17)) 

除了groovy脚本,还有一种常用的脚本是jsonLogic脚本

jsonLogic,这是一种用 json 构造的语法树,最主要优势是语言无关、前后端通用。非常简单明了,jsonLogic 官方有 js/php/python/ruby 对应的解析库。

groovy 复制代码
//Rule
{
 "and": [
   {
     "==": [
       {
         "var": "weather"
       },
       "晴"
     ]
   },
   {
     "in": [
       {
         "var": "weekday"
       },
       ["6","7"]
     ]
   },
   {
     "and": [
       {
         "match": [
           {
             "var": "phone"
           },
           "^1[3456789]\d{9}$"
         ]
       },
       {
         "==": [
           {
             "var": "isOpen"
           },
           true
         ]
       }
     ]
   },
   {
     "and": [
       {
         ">": [
           {
             "var": "time"
           },
           8
         ]
       },
       {
         "<" [
           {
             "var": "time"
           },
           17
         ]
       }
     ]
   }
 ]
}
// Data
{
 "weather": ""
 "weekday": "",
 "phone": "",
 "isOpen": null,
 "time": null
} 

我们知道

java 复制代码
{     
	// 天气  
	"weather": ""     
	
	// 星期几  
	"weekday": "",    
	
	// 游乐场电话  
	"phone": "",     
	
	// 游乐场是否开门  
	"isOpen": null,     
	
	// 时间  
	"time": null
}

这几个是输入属性,对这个流程树设置不同的输入属性,就可以得到不同的布尔值

比如设置输入属性 {"weather": "晴","weekday": "6","phone":"13212341234","isOpen":true,"time":12} 得到的值就是 true

比如设置输入属性 {"weather": "晴","weekday": "5","phone":"13212341234","isOpen":true,"time":12} 得到的值就是 false

逻辑表达式编辑器的实现

逻辑表达式编辑器流程介绍

我们的逻辑表达式编辑器就是基于上述的QueryBuilder的规则树实现的,对于QueryBuilder过滤器后面加了输出语句

这样就是一个判断分支。

对于复杂逻辑肯定会有多个判断分支,我们可以添加多个判断分支组合起来

由于分支不一定都命中,所以我们要设置一个兜底逻辑,相当于判断语句中的default,这样整个逻辑表达式的流程就完整了

这样,对应的逻辑就可以这样简单的表示

二、服务端实现介绍

首先看一下流程图

对于每一个规则流程都有一个唯一的accesskey,比如

trip.ibu.TTS.outbound

trip.flight.Offline.outbound.call

我们在项目初始化的时候,项目里面有几个规则流程,把这些规则流程对应的accessKey放到List里面,然后逐个的遍历初始化

一个规则流程有多个规则分支,因为groovy脚本,Java程序是无法直接执行的,因此我们需要需要进行转化。

这里我们用到了GroovyClassLoader,GroovyClassLoader主要负责在运行时编译groovy脚本为Class的工作,从而使Groovy实现了将groovy源代码动态加载为Class的功能。

注意,我们要有预热的过程,不能在程序运行中将groovy脚本转化为class对象,然后让程序执行。因为一个规则流程有多个规则分支,一个规则分支就对应一个groovy脚本,运行的时候就要转化成一个class对象,一个复杂的规则流程可能有几百个规则分支。

由于groovy脚本转化为class对象是比较耗时的,所以在程序初始化阶段进行预热是有必要的。

groovy 复制代码
// 脚本转化为class对象
GroovyClassLoader classLoader = new GroovyClassLoader();
Class<Script> scriptClazz = (Class<Script>) classLoader.parseClass(JARS + scriptStr);
// 执行判断
Binding binding = new Binding(parameters);
Script script = InvokerHelper.createScript( scriptClazz, binding);
return (boolean) script.run();

需要注意的一个点

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  • 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  • 加载该类的ClassLoader已经被GC。
  • 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法.

因此,class不是很容易被gc的。

所以当我们监听到规则流程有变更,需要去重新加载规则流程里面的流程分支时,这种情况下很多规则分支是没有被改动的,被改动的往往是一两个规则分支。

所以这种情况下,我们不能再次将所有的groovy初始化生成class对象,这样会造成重复生成class对象,容易造成out of metaspace的错误

这里的解决方案是 对每一个groovy脚本生成一个md5值,存放在map<md5, Class>里面,value值就是groovy脚本转化生成的Class对象

这样的话,就不会重复生成class对象,我们只重新生成了规则分支更改的class对象

相关推荐
rock——you2 分钟前
django通过关联表字段进行排序并去重
数据库·后端·postgresql·django
daiyang123...5 分钟前
JavaEE 【知识改变命运】05 多线程(4)
java·单例模式·java-ee
zzxxlty13 分钟前
Intellij IDEA 2023 获取全限定类名
java·ide·intellij-idea
ThisIsClark16 分钟前
【后端面试总结】MySQL哪些操作是不能回滚的
mysql·面试·职场和发展
悟空非空也16 分钟前
178K⭐排名第一计算机面试笔记
笔记·面试·职场和发展
coding侠客19 分钟前
避免版本冲突:Spring Boot项目中正确使用Maven的DependencyManagement
java·spring boot·maven
sky丶Mamba34 分钟前
Java虚拟机启动时默认携带参数(jdk8)
java·jvm
五味香1 小时前
Java学习,字符串搜索
java·c语言·开发语言·python·学习·golang·kotlin
weixin_1122331 小时前
基于Java图书借阅系统的设计与实现
java·开发语言
小马爱打代码1 小时前
Spring Boot集成ShedLock实现分布式定时任务
spring boot·分布式·后端