go的依赖注入实现(通过xml配置)

前言

相信很多小伙伴在进阶做一些大型项目的时候,会遇见,类特别多,接口化后调用非常繁琐的问题,这个时候真的就需要java的spring的那套方法,把接口传进实现,通过接口的方法调用来写逻辑而不用关心具体的实现。

那go的主要领域是网络中间件,并不是说go不适合做工程化的东西,因为spring的IOC那一套是设计模式,和语言并无关系。

笔者自己在主导做一些go的大项目的时候就遇见过这个需求,后来找了很多没办法,只能自己来实现一个依赖注入的框架了,有所取巧,但是还算成功,已经用在大项目上运行了半年多,生产级别是没问题了。

让实现类和接口关联起来

spring的依赖注入写法有种方式,是在xml配置文件里面写一个集合,集合里面的每个item有两个参数,一个是接口名,一个是实例名。
这样在启动的时候会先把这个文件解析,然后把实例和接口的映射关系缓存起来,如果实例不实现接口,则会报错。
我的想法也是,既然go的反射没这么强大,我就必须自己做这个工作了,自己写个框架然后跑起来吧。

那第一步是什么呢,第一步就是我需要一个保存映射关系的载体,我的选择是xml。很简单,xml比json更适合写描述性文档,同时有attribute和element,比yaml承载的信息和层次更加适合。

那接下来第二步就是,我们根据go的xml反序列化来看,是否支持。很遗憾,不支持,因为go的xml是不支持interface的反序列化的,这里真的很无奈。以前java的xmlSerialize可以自己添加接口的是实体类和xml的elementName的关系从而实现接口反序列化,没办法,只能从xml反序列化开始了。

配置文件定义和实现

这份实现我已经上传到github了,需要用到golang的依赖注入的朋友可以用
xmlDeserializer

平常的xml tag的结构体我们看一下

type MyInstancestruct 
	AName            string   `xml:"Name"`
	IsMatch 		 bool     `xml:"Match"`
	MatchString      string   `xml:"Value"`
}
type RootModel struct {

	InstancePtr *MyInstancestruct  `xml:"tagName"`
}

成员变量里面有个struct指针类型的,这个很好理解,如果xml里面匹配到了tagName,有就有,没有就nil,那现在问题在于我们要解决的情况是,我们定义了一堆同一个接口的不同实例,这个时候xml怎么写。

我的方法是我会定义一个tagHeader,目前是factory,代表实例工程的意思。

type RootModel struct {

	SingleInterface          IEqualRuler   `xml:"Factory.EqualRuler"`
	InterfaceArray           []IParser     `xml:"Factory.Parser"`
	InterfacePtrDeepChildren []IEqualRuler `xml:"EqualRulers>Factory.EqualRuler"`
}

可以看到,我这三个成员变量都是接口化的实例,我的想法很简单,当反序列化的时候,遇见了Factory开头的,则我去实例工厂里面拿xmlElementName和实例的xmlName匹配,匹配到了就拿出来。

于是引出了下面的问题,我们还需要一个实例工厂啊。

为什么需要实例工厂,我的上篇文章 已经讲了,go不支持根据路径字符串直接反序列化出一个实例,必须至少传一个type,而type的来源本身也是从对象,所以我们可以从一个对象反射出一个新的空对象。

所以我们需要一个实例工厂,这个工厂定义了name和实例的映射关系。

好的,说好就开干。

//define instance factory
var instanceMap map[string]map[string]interface{}


func initXmlInstanceFactory() {
	instanceMap = make(map[string]map[string]interface{})

	equalRuleMap := make(map[string]interface{})
	instanceMap["EqualRuler"] = equalRuleMap
	equalRuleMap["EqualRuleA"] = EqualRulerA{}
	equalRuleMap["EqualRuleB"] = EqualRulerB{}

	parserMap := make(map[string]interface{})
	instanceMap["Parser"] = parserMap
	parserMap["NotifyParser"] = NotifyParser{}
	parserMap["CallParser"] = CallParser{}
}

这里定义了两个接口集合,一个是IParser,一个是IEqualRuler,看这名字就知道我以前是经常写java的人了,因为go的推荐做法是在行为后面加er,比如speak->speaker,没办法,会的语言太多了😂

如果经常写依赖注入配置的朋友,我写到这里应该就理解我这样做的原理了。
步骤如下:

  1. 我的项目的某个大模块抽象成一个接口对象,该接口对象继续抽象,里面会用到好几个对象的接口。
  2. 接口的实例继续递归实现接口,把该对象需要的实例化接口准备好
  3. xml配置来写好这些映射关系的定义
  4. xml反序列化这些抽象接口到项目中的对象

xml反序列化抽象接口

这个功能搞定,其实别的都没什么难度了

那golang官方怎么反序列化接口呢,很无奈的是,不支持。

我追踪源码的时候跟到encoding.xml.read.go文件里面的func (d *Decoder)unmarshal函数的第381行蓦然发现这么一段说明
在这里插入图片描述
官方解释的很温和,todo我们在近期的未来会支持的,目前请你忽略它吧😝

对于我这样经常写接口的人来说,抽象并不难,难的是没框架实现IOC,go的轮子还是太少,那还是自己造吧。

我们来看下这个xml我们如何反序列化

//define IParser interface
type IParser interface {
	Parse(string) error
}
//define NotifyParser struct
type NotifyParser struct {
	XMLName xml.Name `xml:"NotifyParser"`
	Name    string   `xml:"Name"`
}

func (this *NotifyParser) Parse(string) error {
	return nil
}

//define NotifyParser struct
type CallParser struct {
	XMLName xml.Name `xml:"CallParser"`
	Index   int      `xml:"Index"`
}

func (this *CallParser) Parse(string) error {
	return nil
}
type RootModel struct {
	InterfaceArray  []IParser  `xml:"Factory.Parser"
}
<Parsers>
	<NotifyParser>
		<Name>pppp</Name>
	</NotifyParser>
	<NotifyParser>
		<Name>mmmm</Name>
	</NotifyParser>
	<CallParser>
		<Index>111</Index>
	</CallParser>
	<CallParser>
		<Index>222</Index>
	</CallParser>
</Parsers>

大家记住,我们上面的实例工厂已经把这两个parser定义进去了

parserMap := make(map[string]interface{})
instanceMap["Parser"] = parserMap
parserMap["NotifyParser"] = NotifyParser{}
parserMap["CallParser"] = CallParser{}

那么正常流程就是xml先遍历node节点

根据xmlElementName-"NotifyParser"找到了NotifyParser{},

根据xmlElementName-"CallParser"找到了CallParser{},

然后根据实体再实例化一个空对象

func resolveInstance(factory map[string]map[string]interface{}, typ string, name string) interface{} {
	typDict, ok := factory[typ]
	if !ok {
		return nil
	}

	resIns, ok := typDict[name]
	if !ok {
		return nil
	}

	tp := reflect.ValueOf(resIns).Type()
	h := reflect.New(tp).Interface()
	return h
}

然后空对象再进行官方的xml解析,解析过程中遇见xml的Tag里面是factory关键字开头的再进行接口化解析,一路递归下去即可。

说起来很容易,做起来其实遇见很多坑,我简单说下吧,遇见坑填坑才是最有挑战的地方。

首先就是官方的xml库并没有获取某个node节点的xml内容的功能,这个我不得不找了github.com/beevik/etree库,然后自己实现xmlElement的clone方法

func GetElementXml(elem *etree.Element, addInst bool) string {
	doc := etree.NewDocument()
	if addInst {
		doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
	}
	doc.Indent(4)

	root := doc.Element.CreateElement("")
	CloneElement(root, elem)

	str, _ := doc.WriteToString()
	return str
}


func CloneElement(newRoot *etree.Element, oldRoot *etree.Element) {
	newRoot.Tag = oldRoot.Tag
	newRoot.SetText(oldRoot.Text())

	for _, item := range oldRoot.Attr {
		newRoot.CreateAttr(item.Key, item.Value)
	}
	for _, item := range oldRoot.ChildElements() {
		sub := newRoot.CreateElement(item.Tag)
		sub.SetText(item.Text())
		CloneElement(sub, item)
	}
}

然后,上面这个只是反序列化一个接口,如果是slicet的接口数组怎么办,
我在这段期间研究了大量golang的反射功能,并且以此写了很多工程化的库应用到项目上,也是有很大的收获。数组的处理如下

func (this *Deserializer) parsePrefixXmlInterfaceSliceField(root *etree.Element, field reflect.Value, xmlTagName string) error {
	nodes := this.getMatchTagNodes(root, xmlTagName)
	if nodes == nil || len(nodes) == 0 {
		return nil
	}

	mapTypeName := getMapTypeNameFromXmlTag(xmlTagName, this.prefix)
	sliceCarrier := make([]reflect.Value, 0)
	for _, node := range nodes {
		newInstance := resolveInstance(this.factory, mapTypeName, node.Tag)
		if newInstance == nil {
			return fmt.Errorf("resolveInstance by %s return nil", xmlTagName)
		}
		err := unmarshalByElement(node, newInstance)
		if err != nil {
			return err
		}

		err = this.parseByElement(node, newInstance)
		if err != nil {
			return err
		}

		sliceCarrier = append(sliceCarrier, reflect.ValueOf(newInstance))
	}
	arrBind := reflect.Append(field, sliceCarrier...)
	field.Set(arrBind)
	return nil
}

这样就可以支持接口切片的反序列化了,除此之外还有非接口下面的接口也要识别等,反正遇见的诸多坑已经过去式了。
后面这个还支持了反序列化方法执行结束后可以执行自定义的操作

type IXmlUnmarshaler interface {
	AfterUnmarshal() error
}

只要对象实现这个方法即可,自动判断执行。
这个库比官方的好很多,虽然需要自己写一点实例工厂,但是管理接口和对象的映射依然已经做到了。

最后节选一点点晒一下我的应用,实例工厂和xml的应用文件

我这个项目支持上千路的会话,同时和redis数据库黑名单等多个组件打交道,是一个核心组件的项目,需要高度抽象,并且网络层面的调用非常多。

首先是我的handler的类,已经完全接口化了
不需要关心里面的复杂的逻辑和属性

type CCHandler struct {
	HandlerName   string `xml:"name,attr"`
	ExcuteTimeout int64  `xml:"timeout,attr"`

	//方法准入规则过滤
	AllowRulers   []iService.IAllowRuler   `xml:"AllowRule>Factory.AllowRuler"`
	
	//最小执行单元
	MinorExcutors []iService.IMinorExcutor `xml:"Factory.MinorExcutor"`
	
	//收到通知后的处理逻辑
	WaitExcutors  []iService.IWaitExcutor  `xml:"Factory.WaitExcutor"`
	
	//运行时会话实例
	sessionCache iService.ISessionCache
}

实例工厂如下,实例多不可怕,把复杂性规范在工厂里面,初始化后运行的代码里面压根没有具体命名实例,全部都是接口
在这里插入图片描述
xml文件节选如下,对应通知的处理,在代码里同样可以几行代码就操作接口方法搞定整个逻辑。因为我所有的通知对应的解析和后续处理单元在xml里就已经定了。
在这里插入图片描述
看下我的xml解析后接口化实例的展现,每个define下面都有checker/parser/handler,handler下面又有ruler/excutor,所有的对象都是接口化的,而这个映射关系在程序启动开始就通过依赖注入的配置文件决定了。

那我的业务逻辑其实只需要写个for循环,然后parser后准入规则判断rule,后面执行hanlder就可以了,避免了很多写对象的同学一直根据实例来if else然后把业务代码写的极其复杂,这个就是依赖注入的目的。
简化代码逻辑,更具备可读性,更容易维护。

同时映射关系在程序启动的时候初始化,后面的逻辑和接口抽象并没有性能成本。

在这里插入图片描述

好了,接口配置化的方案就介绍到这里,github地址是https://github.com/xukgo/xmlDeserializer

有空继续分享我的几个开源项目。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值