前言
相信很多小伙伴在进阶做一些大型项目的时候,会遇见,类特别多,接口化后调用非常繁琐的问题,这个时候真的就需要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,没办法,会的语言太多了😂
如果经常写依赖注入配置的朋友,我写到这里应该就理解我这样做的原理了。
步骤如下:
- 我的项目的某个大模块抽象成一个接口对象,该接口对象继续抽象,里面会用到好几个对象的接口。
- 接口的实例继续递归实现接口,把该对象需要的实例化接口准备好
- xml配置来写好这些映射关系的定义
- 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
有空继续分享我的几个开源项目。