概要
关于gengine是干什么的,请参考B站新一代golang规则引擎的设计与实现 - 云+社区 - 腾讯云
本文主要通过两部分来解析gengine源码,一是介绍框架代码,理清楚该系统的架构;二是以跟随实际例子,看看在编译环节和执行环节分别做了哪些工作
gengine是基于antlr4来实现编译和执行的,之前也对该工具进行过介绍,可以参考Antlr4简介_zhanglehes的专栏-CSDN博客
框架代码
RuleBuilder.go文件
RuleBuilder.go 用于从字符串中解析出具体的语法树
type RuleBuilder struct {
Kc *base.KnowledgeContext
Dc *context.DataContext
buildLock sync.Mutex
}
它的核心方法
func (builder *RuleBuilder) BuildRuleFromString(ruleString string) error {
kc := base.NewKnowledgeContext()
in := antlr.NewInputStream(ruleString)
lexer := parser.NewgengineLexer(in) // 文本解析
lexerErrListener := iparser.NewGengineErrorListener()
lexer.AddErrorListener(lexerErrListener)
stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
listener := iparser.NewGengineParserListener(kc) // listener是继承原生的listener,实现各回调函数的作用
psr := parser.NewgengineParser(stream)
psr.BuildParseTrees = true
//grammar listener
grammarErrListener := iparser.NewGengineErrorListener()
psr.AddErrorListener(grammarErrListener)
antlr.ParseTreeWalkerDefault.Walk(listener, psr.Primary()) // 遍历树,促发Accept函数的回调
return nil
}
BuildRuleFromString函数从字符串中解析规则,生成Kc,如果在语法解析中失败需要打印失败信息
将用户输入的字符串,解析为具体的语法时,需要这些定义好的节点来承接具体的数据。在执行具体的规则时,就是在语法树上的遍历递归,基于语法树遍历的顺序,来执行这些定义好的节点代码。
KnowledgeContext.go文件
KnowledgeContext.go 用于存储解析出来的规则,如果一次输入可以包含多条规则(rule),将它们分别进行存储。
type KnowledgeContext struct {
// ruleName - RuleEntity
RuleEntities map[string]*RuleEntity // 通过名称进行关联
SortRules []*RuleEntity // 按照优先级排序
SortRulesIndexMap map[string]int
}
DataContext.go 文件
DataContext.go 是用户向规则引擎中添加可用API的接口或结构体
type DataContext struct {
lockVars sync.Mutex // 保护规则文件中定义的变量
lockBase sync.Mutex // 保护base变量
base map[string]reflect.Value // 外部注入的变量,key为变量名,value为指针(函数地址或者变量地址)
}
注入变量的方法
func (dc *DataContext) Add(key string, obj interface{}) {
dc.lockBase.Lock()
defer dc.lockBase.Unlock()
dc.base[key] = reflect.ValueOf(obj)
}
PluginLoader函数动态加载golang文件和符号,同时将符号与文件中的(函数)指针关联起来;
ExecFunc、ExecMethod、ExecThreeLevel三个函数实现了规则文件的函数能力;
GetValue和SetValue函数实现的是获取和设置变量值的能力;
Gengine.go文件
Gengine.go 提供各种规则执行模式的接口,以最常用的顺序执行为例。
func (g *Gengine) Execute(rb *builder.RuleBuilder, b bool) error {
g.returnResult = make(map[string]interface{}) // 记录返回的结果
for _, r := range rb.Kc.SortRules { // 按照规则的优先级进行排序
v, err, bx := r.Execute(rb.Dc) // 执行规则
if bx {
g.addResult(r.RuleName, v) // 添加返回值
}
return nil
}
编译环节的核心逻辑
- 首先它是antlr4生成的一棵树,节点都是rule(非token);
- 树上所有节点都会促发回调函数,enter or exit;
- 遍历树,当遇到非叶节点时,先将该节点push进stack中;当离开该节点时,将其从stack中push出来;
- 所有叶节点可以通过stack.pop()获取到所属节点(父节点),并将其值赋予父节点;
- 所有的非叶节点,在遍历进入该节点时创建该节点对应的对象放入stack中,供子节点使用;
- 如果某一个类型的rule,其上层对应多个不同的rule,则需要实现一个rule_holder用来接受数据;相反,如果上层只对应一条rule,则直接使用该类型的指针;
实际例子
我们首先需要定义一个规则文件(符合antlr4语法规范),然后在rule builder环节,将规则进行了解析编译,execute环节则是顺序执行定义好的语法(statements)。我们使用函数调用来展示这一系列流程。
规则
与函数调用相关的规则如下所示:
Rule
1、ruleEntity: RULE ruleName ruleDescription? salience? BEGIN ruleContent END;
2、ruleContent : statements;
3、statements: statement* returnStmt? ;
4、statement : ifStmt | functionCall | methodCall | threeLevelCall | assignment | concStatement | forStmt | breakStmt |forRangeStmt | continueStmt;
5、functionCall : SIMPLENAME LR_BRACKET functionArgs? RR_BRACKET;
6、functionArgs
: (constant | variable | functionCall | methodCall | threeLevelCall | mapVar | expression) (','(constant | variable | functionCall | methodCall | threeLevelCall | mapVar | expression))*
;
Token
1、 SIMPLENAME : ('a'..'z' |'A'..'Z'| '_')+ ( ('0'..'9') | ('a'..'z' |'A'..'Z') | '_' )* ;
2、 LR_BRACKET : '(';
3、 RR_BRACKET : ')';
编译环节
代码实现在iparser/gengine_parser_listener.go文件中。
之前在antlr4简介中提到,使用listener模式会根据语法的结合性,生成回调函数,通知应用进行相应的操作。为了简单起见,我们只关注上面语法的5和6的variable部分,其它的实现规则类似。代码中存在一个全局的stack,存储当前编译解析的位置。
1、进入函数
func (g *GengineParserListener) EnterFunctionCall(ctx *parser.FunctionCallContext) {
if len(g.ParseErrors) > 0 {
return
}
funcCall := &base.FunctionCall{
FunctionName: ctx.SIMPLENAME().GetText(),
}
g.Stack.Push(funcCall)
}
这个函数的作用是记录函数的名称,由SIMPLENAME这个TOKEN语法表示,同时将新生成的对象base.FunctionCall放入stack中;
2、进入形参列表
func (g *GengineParserListener) EnterFunctionArgs(ctx *parser.FunctionArgsContext) {
if len(g.ParseErrors) > 0 {
return
}
funcArg := &base.Args{
ArgList: make([]*base.Arg, 0),
}
g.Stack.Push(funcArg)
}
这个函数创建了一个空的base.Args,并放入stack中
3、进入变量
func (g *GengineParserListener) EnterVariable(ctx *parser.VariableContext) {}
这个函数什么都没做
4、退出变量
func (g *GengineParserListener) ExitVariable(ctx *parser.VariableContext) {
if len(g.ParseErrors) > 0 {
return
}
varName := ctx.GetText()
holder := g.Stack.Peek().(base.VariableHolder)
err := holder.AcceptVariable(varName)
if err != nil {
g.AddError(err)
}
}
这个函数获取到变量的文本,同时获取到stack最上层的对象,并断言它是base.VariableHolder,最后使用该接口调用AcceptVariable的方法。我们看一下这个接口的定义
type VariableHolder interface {
AcceptVariable(name string) error
}
注意,此时stack最上层的对象是在第2步时push进去的,我们看下它是如何实现AcceptVariable方法的
func (as *Args) AcceptVariable(name string) error {
holder := &Arg{
Variable: name,
}
as.ArgList = append(as.ArgList, holder)
return nil
}
可见它的主要作用是去创建一个Arg对象,并存放进as.ArgList列表中去。
5、退出形参列表
func (g *GengineParserListener) ExitFunctionArgs(ctx *parser.FunctionArgsContext) {
if len(g.ParseErrors) > 0 {
return
}
expr := g.Stack.Pop().(*base.Args)
argHolder := g.Stack.Peek().(base.ArgsHolder)
err := argHolder.AcceptArgs(expr)
if err != nil {
g.AddError(err)
}
}
这一步是和第2步对应的(看函数名,一个是enter,一个是exit)。它的作用是取出stack中最上层的元素(说明此时函数的参数已经准备好了),并断言它是*base.Args类型。之后再去确认剩下stack中最上层的元素,断言它符合base.ArgsHolder接口,并调用该接口的AcceptArgs函数。通过步骤1,我们可以知道此时stack中最上层的对象是base.FunctionCall,我们看它是如何实现AcceptArgs函数的
func (fc *FunctionCall) AcceptArgs(funcArg *Args) error {
fc.FunctionArgs = funcArg
return nil
}
6、退出函数
func (g *GengineParserListener) ExitFunctionCall(ctx *parser.FunctionCallContext) {
if len(g.ParseErrors) > 0 {
return
}
expr := g.Stack.Pop().(*base.FunctionCall)
expr.Code = ctx.GetText()
expr.LineNum = ctx.GetStart().GetLine()
expr.Column = ctx.GetStart().GetColumn()
expr.LineStop = ctx.GetStop().GetColumn()
holder := g.Stack.Peek().(base.FunctionCallHolder)
err := holder.AcceptFunctionCall(expr)
if err != nil {
g.AddError(err)
}
}
该函数和步骤1对应,取出stack中最上层的元素,并断言它是*base.FunctionCall类型。
上例中处理的是变量,我们再看一下常量是如何处理的(以整数为例)
规则
constant
: booleanLiteral
| integer
| realLiteral
| stringLiteral
| atName
| atId
| atDesc
| atSal
;
integer : MINUS? INT;
INT : '0'..'9' + ;
编译环节
1、进入整数
func (g *GengineParserListener) EnterInteger(ctx *parser.IntegerContext) {}
这个函数什么也没做
2、退出整数
func (g *GengineParserListener) ExitInteger(ctx *parser.IntegerContext) {
if len(g.ParseErrors) > 0 {
return
}
val, err := strconv.ParseInt(ctx.GetText(), 10, 64)
if err != nil {
g.AddError(err)
return
}
holder := g.Stack.Peek().(base.IntegerHolder)
err = holder.AcceptInteger(val)
if err != nil {
g.AddError(err)
}
}
这个函数对于整数进行了解析,获取的值存放在了val中,同时使用AcceptInteger方法向上层元素进行传递
3、接受整数
func (cons *Constant) AcceptInteger(i64 int64) error {
cons.ConstantValue = reflect.ValueOf(i64)
return nil
}
可见刚解析出的整数被存储进了reflect.Value中
执行环节
1、函数评估
func (fc *FunctionCall) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (res reflect.Value, err error) {
var argumentValues []reflect.Value
if fc.FunctionArgs == nil {
argumentValues = nil
} else {
av, err := fc.FunctionArgs.Evaluate(dc, Vars) // 计算每个函数各参数的值
if err != nil {
return reflect.ValueOf(nil), err
}
argumentValues = av
}
res, e := dc.ExecFunc(Vars, fc.FunctionName, argumentValues) // 执行函数
if e != nil {
return reflect.ValueOf(nil), errors.New(fmt.Sprintf("line %d, column %d, code: %s, %+v", fc.LineNum, fc.Column, fc.Code, e))
}
return //res, nil
}
该函数主要的功能是计算每个函数各参数的值,以及执行函数
2、函数实参列表评估
func (as *Args) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) ([]reflect.Value, error) {
if as.ArgList == nil || len(as.ArgList) == 0 {
return make([]reflect.Value, 0), nil
}
retVal := make([]reflect.Value, len(as.ArgList))
for i, v := range as.ArgList {
rv, err := v.Evaluate(dc, Vars)
if err != nil {
return retVal, err
}
retVal[i] = rv
}
return retVal, nil
}
该函数的作用是逐个评估每个函数参数的值,以常量为例
3、常量评估
func (cons *Constant) Evaluate(dc *context.DataContext, Vars map[string]reflect.Value) (reflect.Value, error) {
return cons.ConstantValue, nil
}
可见返回的值正好是我们在编译环节存储的值
4、执行函数
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
}
dc.lockVars.Lock()
vv, vok := Vars[funcName]
dc.lockVars.Unlock()
if vok {
args := core.ParamsTypeChange(vv, parameters)
res := vv.Call(args)
raw, e := core.GetRawTypeValue(res)
if e != nil {
return reflect.ValueOf(nil), e
}
return raw, nil
}
return reflect.ValueOf(nil), errors.New(fmt.Sprintf("NOT FOUND function \"%s(..)\"", funcName))
}
该函数是函数执行的具体步骤,主要分为函数参数类型转换、调用函数、获取返回值。在执行到该步骤时,函数的形参类型和实参类型已确定,函数的形参类型可以通过根据函数名反射后,调用In(i).Kind()方法确定,实参类型已经通过reflect.Value进行了存储。
Q & A
1、代码在执行过程中,是如何存储数据的(变量和常量)?
答:简单来说,常量是在编译环节就进行了计算,以reflect.Value进行的存储。变量是在执行时才会进行计算和存储,以便之后的表达式使用。
2、为何要使用反射?
答:通过反射可以实现泛型编程,对于任何类型的变量或函数都可以进行统一的存储,同时对于函数还有参数类型、函数调用的方法可以使用。
3、规则和一门语言的区别
答:gengine的规则其实就是一门(新)语言。它和golang语言比较类似,但更加简洁。它不支持函数定义,struct类型定义,但是可以使用外部定义好的函数或类型进行计算。
4、什么场景适合使用规则引擎
答:对于可以高效进行组件编程的场景,通过编写规则(if-else)将组件以一种方式关联起来,组件的复用度高。通常组件使用golang进行编写。