gengine源码解析

概要

关于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
}

编译环节的核心逻辑

  1. 首先它是antlr4生成的一棵树,节点都是rule(非token);
  2. 树上所有节点都会促发回调函数,enter or exit;
  3. 遍历树,当遇到非叶节点时,先将该节点push进stack中;当离开该节点时,将其从stack中push出来;
  4. 所有叶节点可以通过stack.pop()获取到所属节点(父节点),并将其值赋予父节点;
  5. 所有的非叶节点,在遍历进入该节点时创建该节点对应的对象放入stack中,供子节点使用;
  6. 如果某一个类型的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进行编写。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值