详解如何使用golang实现自定义规则引擎-kb88凯时官网登录

来自:网络
时间:2024-08-28
阅读:
免费资源网,https://freexyz.cn/

规则引擎的功能可以简化为当满足一些条件时触发一些操作,通常使用 dsl 自定义语法来表述。规则引擎需要先解析 dsl 语法形成语法树,然后遍历语法树得到完整的语法表达式,最后执行这些语法表达式完成规则的执行。

本文以gengine来探讨如何设计和实现一个自定义规则引擎。

支持的语句

为了满足基本的业务规则需求,规则引擎应该要支持的语句有:

逻辑与算术运算

  • 数学运算( 、-、*、/)
  • 逻辑运算(&&、||、!)
  • 比较运算(==、!=、>、<、>=、<=)

流程控制

  • 条件(if else)
  • 循环 (for)

高级语句

  • 对象属性访问(对象。属性)
  • 方法调用(func ())

规则语法的解析

规则的 dsl 语法定义应该简单明了,gengine 使用了开源的语法解析器 antlr4 来定义和解析规则语法。

定义规则语法

规则的 dsl 基本语法格式如下:

rule "rulename" "rule-describtion" salience  10
begin
//规则体
end

其中规则体为具体规则语句,由上述的 逻辑与算术运算、流程控制、高级语句 组合而成。

例如,判断为一个大额异常订单的规则体:

if order.price>= 1000000 {
    return
}

编写解析器语法

antlr4 解析器语法定义文件后缀名为.g4,以下内容为解析器的语法定义,解析器根据语法定义去逐行解析生成语法树。

这里省略了一些非核心的语法定义并做了简化,完整内容查看 gengine.g4。

grammar gengine;
primary: ruleentity ;
// 规则定义
ruleentity:  rule rulename ruledescription? salience? begin rulecontent end;
rulename : stringliteral;
ruledescription : stringliteral;
salience : salience integer;
// 规则体
rulecontent : statements;
statements: statement* returnstmt?;
// 基本语句
statement : ifstmt | breakstmt;
expression : mathexpression
            | expression comparisonoperator expression
            | expression logicaloperator expression
            ;
mathexpression : mathexpression  mathmdoperator mathexpression
               | mathexpression  mathpmoperator mathexpression
               | expressionatom
               | lr_bracket mathexpression rr_bracket
               ;
expressionatom
    : functioncall
    | constant
    | variable
    ;
returnstmt : return expression?;
ifstmt : if expression lr_brace statements rr_brace elseifstmt*  elsestmt?;
elsestmt : else lr_brace statements rr_brace;
constant
    : booleanliteral
    | integer
    | stringliteral
    ;
functionargs
    : (constant | variable  | functioncall | expression)  (','(constant | variable | functioncall | expression))*
    ;
integer : minus? int;
stringliteral: dquota_string;
booleanliteral : true | false;
functioncall : simplename lr_bracket functionargs? rr_bracket;
variable :  simplename | dottedname;
mathpmoperator : plus | minus;
mathmdoperator : mul | div;
comparisonoperator : gt | lt | gte | lte | equals | notequals;

解析器生成语法树

如,判断为一个大额异常订单的规则:

rule "order-large-price" "订单大额金额" salience 10
begin
    if order.price >= 1000000 {
        return
    }
end

语法解析器解析之后,生成语法树:

详解如何使用golang实现自定义规则引擎

遍历语法树生成语句表达式

解析器生成语法树之后,只需要遍历语法树即可得到完整的语句表达式。antlr4 解析器会生成 listener 接口,这些接口在遍历语法树时会被调用。

type genginelistener interface {
    antlr.parsetreelistener
    // 省略了一些只列举了部分方法
    // enterruleentity is called when entering the ruleentity production.
    enterruleentity(c *ruleentitycontext)
    // exitruleentity is called when exiting the ruleentity production.
    exitruleentity(c *ruleentitycontext)
    // enterrulecontent is called when entering the rulecontent production.
    enterrulecontent(c *rulecontentcontext)
    // exitrulecontent is called when exiting the rulecontent production.
    exitrulecontent(c *rulecontentcontext)
    // enterstatement is called when entering the statement production.
    enterstatement(c *statementcontext)
    // exitstatement is called when exiting the statement production.
    exitstatement(c *statementcontext)
    // enterifstmt is called when entering the ifstmt production.
    enterifstmt(c *ifstmtcontext)
    // exitifstmt is called when exiting the ifstmt production.
    exitifstmt(c *ifstmtcontext)
    // enterexpression is called when entering the expression production.
    enterexpression(c *expressioncontext)
    // exitexpression is called when exiting the expression production.
    exitexpression(c *expressioncontext)
    // enterinteger is called when entering the integer production.
    enterinteger(c *integercontext)
    // exitinteger is called when exiting the integer production.
    exitinteger(c *integercontext)
}

可以发现在遍历语法树时,每个节点都有 enterxxx() 和 exitxxx() 方法存在,是成对出现的。

因此要遍历语法树只需要实现 genginelistener 接口即可,gengine 巧妙的引入结构,遍历完语法树后(树的递归遍历就是进栈出栈过程),就得到了完整的规则语句表达式。这里只列举部分方法,完整实现见 gengine_parser_listener。

type gengineparserlistener struct {
    parser.basegenginelistener
    knowledgecontext *base.knowledgecontext
    stack            *stack.stack
}
func (g *gengineparserlistener) enterruleentity(ctx *parser.ruleentitycontext) {
    if len(g.parseerrors) > 0 {
        return
    }
    entity := &base.ruleentity{
        salience: 0,
    }
    g.rulename = ""
    g.ruledescription = ""
    g.salience = 0
    g.stack.push(entity)
}
func (g *gengineparserlistener) exitruleentity(ctx *parser.ruleentitycontext) {
    if len(g.parseerrors) > 0 {
        return
    }
    entity := g.stack.pop().(*base.ruleentity)
    g.knowledgecontext.ruleentities[entity.rulename] = entity
}

gengine 通过解析器解析规则内容之后,规则的数据结构如下:

详解如何使用golang实现自定义规则引擎

全局的 hashmap 以规则名为 key,规则体为 value,规则体中的 rulecontent 为该规则所有的语句表达式列表,列表中的值指向具体的语句表达式实体,语句表达式实体由 逻辑与算术运算、流程控制(if、for)等基本语句组成。

规则语法的执行

其实遍历语法树的过程中,将规则的执行逻辑也放入 exitxxx() 方法,这样就能一并完成规则的解析和执行。但是 gengine 没有这么做,而是将规则的解析和执行解耦,因为规则的解析往往只需要初始化一次,或者在规则有变更时热更新解析,而规则的执行则是在需要校验规则的时候。

从 gengine 的规则数据结构可知,只需要遍历全局的 hashmap,即可按顺序执行所有的规则(顺序模式),执行每一个规则后会通过addresult()方法记录执行结果:

// 顺序模式
func (g *gengine) execute(rb *builder.rulebuilder, b bool) error {
    for _, r := range rb.kc.ruleentities {
        v, err, bx := r.execute(rb.dc)
        if bx {
            // 记录每个规则执行结果
            g.addresult(r.rulename, v)
        }
    }
    // 省略部分
    ...
}

对于某一个规则的执行,则会去遍历规则体 rulecontent 的所有语句表达式列表,然后按顺序去执行该规则下的所有语句表达式:

func (s *statements) evaluate(dc *context.datacontext, vars map[string]reflect.value) (reflect.value, error, bool) {
    for _, statement := range s.statementlist {
        v, err, b := statement.evaluate(dc, vars)
        if err != nil {
            return reflect.valueof(nil), err, false
        }
        if b {
            // return的情况不需要继续执行
            return v, nil, b
        }
    if s.returnstatement != nil {
        return s.returnstatement.evaluate(dc, vars)
    }
    return reflect.valueof(nil), nil, false
}

gengine 为每个语句类型都实现了 evaluate() 方法,这里只讨论 if 语句的执行:

type ifstmt struct {
    expression     *expression
    statementlist  *statements
    elseifstmtlist []*elseifstmt
    elsestmt       *elsestmt
}
func (i *ifstmt) evaluate(dc *context.datacontext, vars map[string]reflect.value) (reflect.value, error, bool) {
    // 执行条件表达式
    it, err := i.expression.evaluate(dc, vars)
    if err != nil {
        return reflect.valueof(nil), err, false
    }
    // 执行条件为真时的语句
    if it.bool() {
        if i.statementlist == nil {
            return reflect.valueof(nil), nil, false
        } else {
            return i.statementlist.evaluate(dc, vars)
        }
    }
    return reflect.valueof(nil), nil, false
}

其中条件表达式expression.evaluate()为计算条件表达式的值:

func (e *expression) evaluate(dc *context.datacontext, vars map[string]reflect.value) (reflect.value, error) {
    // 原子表达式
    var atom reflect.value
    if e.expressionatom != nil {
        evl, err := e.expressionatom.evaluate(dc, vars)
        if err != nil {
            return reflect.valueof(nil), err
        }
        atom = evl
    }
    
    // 比较操作
    if e.comparisonoperator != "" {
        // 计算左值
        lv, err := e.expressionleft.evaluate(dc, vars)
        if err != nil {
            return reflect.valueof(nil), err
        }
        // 计算右值
        rv, err := e.expressionright.evaluate(dc, vars)
        if err != nil {
            return reflect.valueof(nil), err
        }
        // 省略了类型转化
        switch e.comparisonoperator {
        case "==":
            b = reflect.valueof(lv == rv)
            break
        case "!=":
            b = reflect.valueof(lv != rv)
            break
        case ">":
            b = reflect.valueof(lv > rv)
            break
        case "<":
            b = reflect.valueof(lv < rv)
            break
        case ">=":
            b = reflect.valueof(lv >= rv)
            break
        case "<=":
            b = reflect.valueof(lv <= rv)
            break
        }
    }
}

递归执行到expressionatom.evaluate()原子表达式时,就可以得到该原子表达式的值以结束递归:

func (e *expressionatom) evaluate(dc *context.datacontext, vars map[string]reflect.value) (reflect.value, error) {
    if len(e.variable) > 0 {
        // 是变量则取变量值,通过反射获取注入的自定义对象值
        return dc.getvalue(vars, e.variable)
    } else if e.constant != nil {
        // 是常量就返回值
        return e.constant.evaluate(dc, vars)
    }
    // 省略部分
}

支持自定义对象注入

在上下文中注入自定义对象后,就可以在规则中使用注入的对象。使用例子:

// 规则体
rule "test-object" "测试自定义对象" salience 10
begin
    // 访问自定义对象order
    if order.price >= 1000000 {
        return
    }
end
// 注入自定义对象order
datacontext := gctx.newdatacontext()
datacontext.add("order", order)

现在来看下 gengine 的具体实现,主要是使用反射特性:

func (dc *datacontext) add(key string, obj interface{}) {
    dc.lockbase.lock()
    defer dc.lockbase.unlock()
    dc.base[key] = reflect.valueof(obj)
}

gengine 解析规则时会将自定义对象标记为variable类型,通过 getvalue() 获取自定义对象属性值:

// 获取变量值
func (dc *datacontext) getvalue(vars map[string]reflect.value, variable string) (reflect.value, error) {
    if strings.contains(variable, ".") {
        // 对象a.b
        structandfield := strings.split(variable, ".")
        if len(structandfield) == 2 {
            a := structandfield[0]
            b := structandfield[1]
            // 获取注入的对象
            dc.lockbase.lock()
            v, ok := dc.base[a]
            dc.lockbase.unlock()
            if ok {
                return core.getstructattributevalue(v, b)
            }
        }
    }
}
// 反射获取对象属性值
func getstructattributevalue(obj reflect.value, fieldname string) (reflect.value, error) {
    stru := obj
    var attrval reflect.value
    if stru.kind() == reflect.ptr {
        attrval = stru.elem().fieldbyname(fieldname)
    } else {
        attrval = stru.fieldbyname(fieldname)
    }
    return attrval, nil
}

支持自定义方法注入

同样在上下文中注入自定义方法后,也可以在规则中使用注入的方法。使用例子:

// 规则体
rule "test-func" "测试自定义方法" salience 10
begin
    // 自定义方法getcount获取指标数据(患者当天的订单数量)
    num = getcount("order-patient-id", order.patientid)
    if num >= 5 {
        return
    }
end
// 注入自定义方法getcount
datasvc := s.indicatordao.newdataservice(ctx)
datacontext := gctx.newdatacontext()
datacontext.add("getcount", datasvc.getcount)

gengine 自定义方法的注入也是使用反射来实现,自定义方法的注入同自定义对象一样也是使用 add() 方法注入。

gengine 解析规则时会将自定义方法标记为functioncall类型:

func (dc *datacontext) execfunc(vars map[string]reflect.value, funcname string, parameters []reflect.value) (reflect.value, error) {
    // 获取注入的方法
    dc.lockbase.lock()
    v, ok := dc.base[funcname]
    dc.lockbase.unlock()
    if ok {
        args := core.paramstypechange(v, parameters)
        // 调用方法
        res := v.call(args)
        raw, e := core.getrawtypevalue(res)
        if e != nil {
            return reflect.valueof(nil), e
        }
        return raw, nil
    }
}

支持并发执行

通常情况下顺序模式执行即可满足要求,但是当规则数量比较大时,顺序执行的耗时就会比较长。

详解如何使用golang实现自定义规则引擎

规则引擎在执行所有规则的时候,其实是遍历全局的 hashmap 然后再顺序执行每一个规则,由于每个规则之间没有依赖关系,因此可以用每一个规则一个协程来并发执行。

func (g *gengine) executeconcurrent(rb *builder.rulebuilder) error {
    var wg sync.waitgroup
    wg.add(len(rb.kc.ruleentities))
    for _, r := range rb.kc.ruleentities {
        rr := r
        // 协程并发
        go func() {
            v, e, bx := rr.execute(rb.dc)
            if bx {
                g.addresult(rr.rulename, v)
            }
            wg.done()
        }()
    }
    wg.wait()
    // 省略部分
}

使用场景

有了规则引擎之后,很多在业务代码中的 if-else、switch 硬编码,都能抽象为规则并使用规则引擎,这样能通过配置规则代替硬编码,能极大地缩短变更上线时间。

业务风控

通过业务数据分析,可以抽象出用户异常行为的规则:

详解如何使用golang实现自定义规则引擎

然后,风控系统在判断是否为风险操作时,只需要规则引擎加载并执行风控规则,即可得到结果。想要提高风控系统的准确性,只需要不断地迭代完善风控规则。

详解如何使用golang实现自定义规则引擎

规则引擎在业务风控的实践,可以参考 基于准实时规则引擎的业务风控实践。

运营活动

拿最常见的抽奖和做任务 2 种运营活动来说,都可以将具体活动逻辑抽象为业务规则:① 抽奖,不同的人&不同的场景对应不同的奖池(中奖概率与奖品集合规则);② 做任务,任务领取规则、任务完成指标动态可配(任务规则);

详解如何使用golang实现自定义规则引擎

内容分发

针对某些特定的用户或者某种场景的用户,下发特定的展示内容或者推送短信等触达消息,都可以将这些特定用户的逻辑梳理为内容分发规则。

详解如何使用golang实现自定义规则引擎

以上就是详解如何使用golang实现自定义规则引擎的详细内容,更多关于golang自定义规则引擎的资料请关注其它相关文章!

免费资源网,https://freexyz.cn/
返回顶部
顶部
网站地图