詳解如何使用Golang實現(xiàn)自定義規(guī)則引擎
規(guī)則引擎的功能可以簡化為當(dāng)滿足一些條件時觸發(fā)一些操作,通常使用 DSL 自定義語法來表述。規(guī)則引擎需要先解析 DSL 語法形成語法樹,然后遍歷語法樹得到完整的語法表達(dá)式,最后執(zhí)行這些語法表達(dá)式完成規(guī)則的執(zhí)行。
本文以gengine來探討如何設(shè)計和實現(xiàn)一個自定義規(guī)則引擎。
支持的語句
為了滿足基本的業(yè)務(wù)規(guī)則需求,規(guī)則引擎應(yīng)該要支持的語句有:
邏輯與算術(shù)運算
- 數(shù)學(xué)運算(+、-、*、/)
- 邏輯運算(&&、||、!)
- 比較運算(==、!=、>、<、>=、<=)
流程控制
- 條件(IF ELSE)
- 循環(huán) (FOR)
高級語句
- 對象屬性訪問(對象。屬性)
- 方法調(diào)用(func ())
規(guī)則語法的解析
規(guī)則的 DSL 語法定義應(yīng)該簡單明了,gengine 使用了開源的語法解析器 Antlr4 來定義和解析規(guī)則語法。
定義規(guī)則語法
規(guī)則的 DSL 基本語法格式如下:
rule "rulename" "rule-describtion" salience 10 begin //規(guī)則體 end
其中規(guī)則體為具體規(guī)則語句,由上述的 邏輯與算術(shù)運算、流程控制、高級語句 組合而成。
例如,判斷為一個大額異常訂單的規(guī)則體:
if Order.Price>= 1000000 { return }
編寫解析器語法
Antlr4 解析器語法定義文件后綴名為.g4,以下內(nèi)容為解析器的語法定義,解析器根據(jù)語法定義去逐行解析生成語法樹。
這里省略了一些非核心的語法定義并做了簡化,完整內(nèi)容查看 gengine.g4。
grammar gengine; primary: ruleEntity+; // 規(guī)則定義 ruleEntity: RULE ruleName ruleDescription? salience? BEGIN ruleContent END; ruleName : stringLiteral; ruleDescription : stringLiteral; salience : SALIENCE integer; // 規(guī)則體 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;
解析器生成語法樹
如,判斷為一個大額異常訂單的規(guī)則:
rule "order-large-price" "訂單大額金額" salience 10 begin if Order.Price >= 1000000 { return } end
語法解析器解析之后,生成語法樹:
遍歷語法樹生成語句表達(dá)式
解析器生成語法樹之后,只需要遍歷語法樹即可得到完整的語句表達(dá)式。Antlr4 解析器會生成 Listener 接口,這些接口在遍歷語法樹時會被調(diào)用。
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) }
可以發(fā)現(xiàn)在遍歷語法樹時,每個節(jié)點都有 EnterXXX() 和 ExitXXX() 方法存在,是成對出現(xiàn)的。
因此要遍歷語法樹只需要實現(xiàn) gengineListener 接口即可,gengine 巧妙的引入棧
結(jié)構(gòu),遍歷完語法樹后(樹的遞歸遍歷就是進(jìn)棧出棧過程),就得到了完整的規(guī)則語句表達(dá)式。這里只列舉部分方法,完整實現(xiàn)見 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 通過解析器解析規(guī)則內(nèi)容之后,規(guī)則的數(shù)據(jù)結(jié)構(gòu)如下:
全局的 hashmap 以規(guī)則名為 key,規(guī)則體為 value,規(guī)則體中的 ruleContent 為該規(guī)則所有的語句表達(dá)式列表,列表中的值指向具體的語句表達(dá)式實體,語句表達(dá)式實體由 邏輯與算術(shù)運算、流程控制(IF、FOR)等基本語句組成。
規(guī)則語法的執(zhí)行
其實遍歷語法樹的過程中,將規(guī)則的執(zhí)行邏輯也放入 ExitXXX() 方法,這樣就能一并完成規(guī)則的解析和執(zhí)行。但是 gengine 沒有這么做,而是將規(guī)則的解析和執(zhí)行解耦,因為規(guī)則的解析往往只需要初始化一次,或者在規(guī)則有變更時熱更新解析,而規(guī)則的執(zhí)行則是在需要校驗規(guī)則的時候。
從 gengine 的規(guī)則數(shù)據(jù)結(jié)構(gòu)可知,只需要遍歷全局的 hashmap,即可按順序執(zhí)行所有的規(guī)則(順序模式),執(zhí)行每一個規(guī)則后會通過addResult()
方法記錄執(zhí)行結(jié)果:
// 順序模式 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 { // 記錄每個規(guī)則執(zhí)行結(jié)果 g.addResult(r.RuleName, v) } } // 省略部分 ... }
對于某一個規(guī)則的執(zhí)行,則會去遍歷規(guī)則體 ruleContent 的所有語句表達(dá)式列表,然后按順序去執(zhí)行該規(guī)則下的所有語句表達(dá)式:
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的情況不需要繼續(xù)執(zhí)行 return v, nil, b } if s.ReturnStatement != nil { return s.ReturnStatement.Evaluate(dc, Vars) } return reflect.ValueOf(nil), nil, false }
gengine 為每個語句類型都實現(xiàn)了 Evaluate() 方法,這里只討論 IF 語句的執(zhí)行:
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) { // 執(zhí)行條件表達(dá)式 it, err := i.Expression.Evaluate(dc, Vars) if err != nil { return reflect.ValueOf(nil), err, false } // 執(zhí)行條件為真時的語句 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 }
其中條件表達(dá)式Expression.Evaluate()
為計算條件表達(dá)式的值:
func (e *Expression) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error) { // 原子表達(dá)式 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 } // 省略了類型轉(zhuǎn)化 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 } } }
遞歸執(zhí)行到ExpressionAtom.Evaluate()
原子表達(dá)式時,就可以得到該原子表達(dá)式的值以結(jié)束遞歸:
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) } // 省略部分 }
支持自定義對象注入
在上下文中注入自定義對象后,就可以在規(guī)則中使用注入的對象。使用例子:
// 規(guī)則體 rule "test-object" "測試自定義對象" salience 10 begin // 訪問自定義對象Order if Order.Price >= 1000000 { return } end // 注入自定義對象Order dataContext := gctx.NewDataContext() dataContext.Add("Order", Order)
現(xiàn)在來看下 gengine 的具體實現(xiàn),主要是使用反射特性:
func (dc *DataContext) Add(key string, obj interface{}) { dc.lockBase.Lock() defer dc.lockBase.Unlock() dc.base[key] = reflect.ValueOf(obj) }
gengine 解析規(guī)則時會將自定義對象標(biāo)記為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 }
支持自定義方法注入
同樣在上下文中注入自定義方法后,也可以在規(guī)則中使用注入的方法。使用例子:
// 規(guī)則體 rule "test-func" "測試自定義方法" salience 10 begin // 自定義方法GetCount獲取指標(biāo)數(shù)據(jù)(患者當(dāng)天的訂單數(shù)量) 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 自定義方法的注入也是使用反射來實現(xiàn),自定義方法的注入同自定義對象一樣也是使用 Add() 方法注入。
gengine 解析規(guī)則時會將自定義方法標(biāo)記為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) // 調(diào)用方法 res := v.Call(args) raw, e := core.GetRawTypeValue(res) if e != nil { return reflect.ValueOf(nil), e } return raw, nil } }
支持并發(fā)執(zhí)行
通常情況下順序模式執(zhí)行即可滿足要求,但是當(dāng)規(guī)則數(shù)量比較大時,順序執(zhí)行的耗時就會比較長。
規(guī)則引擎在執(zhí)行所有規(guī)則的時候,其實是遍歷全局的 hashmap 然后再順序執(zhí)行每一個規(guī)則,由于每個規(guī)則之間沒有依賴關(guān)系,因此可以用每一個規(guī)則一個協(xié)程來并發(fā)執(zhí)行。
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 // 協(xié)程并發(fā) go func() { v, e, bx := rr.Execute(rb.Dc) if bx { g.addResult(rr.RuleName, v) } wg.Done() }() } wg.Wait() // 省略部分 }
使用場景
有了規(guī)則引擎之后,很多在業(yè)務(wù)代碼中的 if-else、switch 硬編碼,都能抽象為規(guī)則并使用規(guī)則引擎,這樣能通過配置規(guī)則代替硬編碼,能極大地縮短變更上線時間。
業(yè)務(wù)風(fēng)控
通過業(yè)務(wù)數(shù)據(jù)分析,可以抽象出用戶異常行為的規(guī)則:
然后,風(fēng)控系統(tǒng)在判斷是否為風(fēng)險操作時,只需要規(guī)則引擎加載并執(zhí)行風(fēng)控規(guī)則,即可得到結(jié)果。想要提高風(fēng)控系統(tǒng)的準(zhǔn)確性,只需要不斷地迭代完善風(fēng)控規(guī)則。
規(guī)則引擎在業(yè)務(wù)風(fēng)控的實踐,可以參考 基于準(zhǔn)實時規(guī)則引擎的業(yè)務(wù)風(fēng)控實踐。
運營活動
拿最常見的抽獎和做任務(wù) 2 種運營活動來說,都可以將具體活動邏輯抽象為業(yè)務(wù)規(guī)則:① 抽獎,不同的人&不同的場景對應(yīng)不同的獎池(中獎概率與獎品集合規(guī)則);② 做任務(wù),任務(wù)領(lǐng)取規(guī)則、任務(wù)完成指標(biāo)動態(tài)可配(任務(wù)規(guī)則);
內(nèi)容分發(fā)
針對某些特定的用戶或者某種場景的用戶,下發(fā)特定的展示內(nèi)容或者推送短信等觸達(dá)消息,都可以將這些特定用戶的邏輯梳理為內(nèi)容分發(fā)規(guī)則。
以上就是詳解如何使用Golang實現(xiàn)自定義規(guī)則引擎的詳細(xì)內(nèi)容,更多關(guān)于Golang自定義規(guī)則引擎的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go并發(fā):使用sync.WaitGroup實現(xiàn)協(xié)程同步方式
這篇文章主要介紹了Go并發(fā):使用sync.WaitGroup實現(xiàn)協(xié)程同步方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05???????Golang實現(xiàn)RabbitMQ中死信隊列幾種情況
本文主要介紹了???????Golang實現(xiàn)RabbitMQ中死信隊列幾種情況,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03GoLang中的互斥鎖Mutex和讀寫鎖RWMutex使用教程
RWMutex是一個讀/寫互斥鎖,在某一時刻只能由任意數(shù)量的reader持有或者一個writer持有。也就是說,要么放行任意數(shù)量的reader,多個reader可以并行讀;要么放行一個writer,多個writer需要串行寫2023-01-01golang用melody搭建輕量的websocket服務(wù)的示例代碼
在Go中,可以使用gin和melody庫來搭建一個輕量級的WebSocket服務(wù),gin是一個流行的Web框架,而melody是一個用于處理WebSocket的庫,本文給大家演示如何使用gin和melody搭建WebSocket服務(wù),感興趣的朋友一起看看吧2023-10-10golang?slice中常見性能優(yōu)化手段總結(jié)
這篇文章主要為大家詳細(xì)一些Golang開發(fā)中常用的slice關(guān)聯(lián)的性能優(yōu)化手段,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10golang獲取當(dāng)前時間、時間戳和時間字符串及它們之間的相互轉(zhuǎn)換方法
這篇文章主要介紹了golang獲取當(dāng)前時間、時間戳和時間字符串及它們之間的相互轉(zhuǎn)換,本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-04-04