golang: Martini之inject源码分析
依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。在传统的程序设计过程中,调用者是自己来决定使用哪些被调用者实现的。但是在依赖注入模式中,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由注入器来完成,然后注入调用者,因此也称为依赖注入。 inject是依赖注入的golang实现,作者是codegangsta。它能在运行时注入参数,调用方法。是Martini框架的基础核心。 我对依赖注入提取了以下2点性质:
在inject中,被调用者为func,因此注入属性也即对func注入实参(当然inject也可以注入struct,这样的话注入的属性就是struct中的已添加tag为`inject`的导出字段)。我们来看下普通的函数调用: packagemain import( "fmt" ) funcSay(name,genderstring,ageint){ fmt.Printf("Mynameis%s,genderis%s,ageis%d!n",name,gender,age) } funcmain(){ Say("陈一回","男",20) } 上面的例子中,定义了函数Say并在main方法中手动调用。这样总是可行的,但是有时候我们不得不面对这样一种情况:比如在web开发中,我们注册路由,服务器接受请求,然后根据request path调用相应的handler。这个handler必然不是由我们手动来调用的,而是由服务器端根据路由匹配来查找对应的handler并自动调用。 是时候引入inject了,尝试用inject改写上面的代码: packagemain import( "fmt" "github.com/codegangsta/inject" ) typeSpecialStringinterface{} funcSay(namestring,genderSpecialString,age) } funcmain(){ inj:=inject.New() inj.Map("陈一回") inj.MapTo("男",(*SpecialString)(nil)) inj.Map(20) inj.Invoke(Say) } $cd$GOPATH/src/injector_test $gobuild $./injector_test Mynameis陈一回,genderis男,ageis20! 看不懂?没关系,因为我们对于inject还没有足够的知识储备,一切从分析inject的源码开始。 inject包只有2个文件,一个是inject.go文件,还有一个是inject_test.go,但我们只关注inject.go文件。 inject.go短小精悍,包括注释和空行才157行。定义了4个接口,包括一个父接口和三个子接口,接下来您就会知道这样定义的好处了。 为了方便,我把所有的注释都去掉了: typeInjectorinterface{ Applicator Invoker TypeMapper SetParent(Injector) } typeApplicatorinterface{ Apply(interface{})error } typeInvokerinterface{ Invoke(interface{})([]reflect.Value,error) } typeTypeMapperinterface{ Map(interface{})TypeMapper MapTo(interface{},interface{})TypeMapper Get(reflect.Type)reflect.Value } 接口Injector是接口Applicator、接口Invoker、接口TypeMapper的父接口,所以实现了Injector接口的类型,也必然实现了Applicator接口、Invoker接口和TypeMapper接口。 Applicator接口只规定了Apply成员,它用于注入struct。 Invoker接口只规定了Invoke成员,它用于执行被调用者。 TypeMapper接口规定了三个成员,Map和MapTo都用于注入参数,但它们有不同的用法。Get用于调用时获取被注入的参数。 另外Injector还规定了SetParent行为,它用于设置父Injector,其实它相当于查找继承。也即通过Get方法在获取被注入参数时会一直追溯到parent,这是个递归过程,直到查找到参数或为nil终止。 typeinjectorstruct{ valuesmap[reflect.Type]reflect.Value parentInjector } funcInterfaceOf(valueinterface{})reflect.Type{ t:=reflect.TypeOf(value) fort.Kind()==reflect.Ptr{ t=t.Elem() } ift.Kind()!=reflect.Interface{ panic("Calledinject.InterfaceOfwithavaluethatisnotapointertoaninterface.(*MyInterface)(nil)") } returnt } funcNew()Injector{ return&injector{ values:make(map[reflect.Type]reflect.Value),} } injector是inject包中唯一定义的struct,所有的操作都是基于injector struct来进行的。它有两个成员values和parent。values用于保存注入的参数,它是一个用reflect.Type当键、reflect.Value为值的map,这个很重要,理解这点将有助于理解Map和MapTo。New方法用于初始化injector struct,并返回一个指向injector struct的指针。但是请注意这个返回值被Injector接口包装了。 InterfaceOf方法虽然只有几句实现代码,但它是Injector的核心。InterfaceOf方法的参数必须是一个接口类型的指针,如果不是则引发panic。InterfaceOf方法的返回类型是reflect.Type,您应该还记得injector的成员values就是一个reflect.Type类型当键的map。这个方法的作用其实只是获取参数的类型,而不关心它的值。我之前有篇文章介绍过(*interface{})(nil),感兴趣的朋友可以去看看:golang: 详解interface和nil 。 为了加深理解,来举个例子: packagemain import( "fmt" "github.com/codegangsta/inject" ) typeSpecialStringinterface{} funcmain(){ fmt.Println(inject.InterfaceOf((*interface{})(nil))) fmt.Println(inject.InterfaceOf((*SpecialString)(nil))) } $cd$GOPATH/src/injector_test $gobuild $./injector_test interface{} main.SpecialString 上面的输出一点也不奇怪。InterfaceOf方法就是用来得到参数类型,而不关心它具体存储的是什么值。值得一提的是,我们定义了一个SpecialString接口。我们在之前的代码也有定义SpecialString接口,用在Say方法的参数声明中,之后您就会知道为什么要这么做。当然您不一定非得命名为SpecialString。 func(i*injector)Map(valinterface{})TypeMapper{ i.values[reflect.TypeOf(val)]=reflect.ValueOf(val) returni } func(i*injector)MapTo(valinterface{},ifacePtrinterface{})TypeMapper{ i.values[InterfaceOf(ifacePtr)]=reflect.ValueOf(val) returni } func(i*injector)Get(treflect.Type)reflect.Value{ val:=i.values[t] if!val.IsValid()&&i.parent!=nil{ val=i.parent.Get(t) } returnval } func(i*injector)SetParent(parentInjector){ i.parent=parent } Map和MapTo方法都用于注入参数,保存于injector的成员values中。这两个方法的功能完全相同,唯一的区别就是Map方法用参数值本身的类型当键,而MapTo方法有一个额外的参数可以指定特定的类型当键。但是MapTo方法的第二个参数ifacePtr必须是接口指针类型,因为最终ifacePtr会作为InterfaceOf方法的参数。 为什么需要有MapTo方法?因为注入的参数是存储在一个以类型为键的map中,可想而知,当一个函数中有一个以上的参数的类型是一样时,后执行Map进行注入的参数将会覆盖前一个通过Map注入的参数。 SetParent方法用于给某个Injector指定父Injector。Get方法通过reflect.Type从injector的values成员中取出对应的值,它可能会检查是否设置了parent,直到找到或返回无效的值,最后Get方法的返回值会经过IsValid方法的校验。举个例子来加深理解: packagemain import( "fmt" "github.com/codegangsta/inject" "reflect" ) typeSpecialStringinterface{} funcmain(){ inj:=inject.New() inj.Map("陈一回") inj.MapTo("男",(*SpecialString)(nil)) inj.Map(20) fmt.Println("stringisvalid?",inj.Get(reflect.TypeOf("姓陈名一回")).IsValid()) fmt.Println("SpecialStringisvalid?",inj.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid()) fmt.Println("intisvalid?",inj.Get(reflect.TypeOf(18)).IsValid()) fmt.Println("[]byteisvalid?",inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid()) inj2:=inject.New() inj2.Map([]byte("test")) inj.SetParent(inj2) fmt.Println("[]byteisvalid?",inj.Get(reflect.TypeOf([]byte("Golang"))).IsValid()) } $cd$GOPATH/src/injector_test $gobuild $./injector_test stringisvalid?true SpecialStringisvalid?true intisvalid?true []byteisvalid?false []byteisvalid?true 通过以上例子应该知道SetParent是什么样的行为。是不是很像面向对象中的查找链? func(inj*injector)Invoke(finterface{})([]reflect.Value,error){ t:=reflect.TypeOf(f) varin=make([]reflect.Value,t.NumIn())//PaniciftisnotkindofFunc fori:=0;i<t.NumIn();i++{ argType:=t.In(i) val:=inj.Get(argType) if!val.IsValid(){ returnnil,fmt.Errorf("Valuenotfoundfortype%v",argType) } in[i]=val } returnreflect.ValueOf(f).Call(in),nil } Invoke方法用于动态执行函数,当然执行前可以通过Map或MapTo来注入参数,因为通过Invoke执行的函数会取出已注入的参数,然后通过reflect包中的Call方法来调用。Invoke接收的参数f是一个接口类型,但是f的底层类型必须为func,否则会panic。 packagemain import( "fmt" "github.com/codegangsta/inject" ) typeSpecialStringinterface{} funcSay(namestring,(*SpecialString)(nil)) inj2:=inject.New() inj2.Map(20) inj.SetParent(inj2) inj.Invoke(Say) } 上面的例子如果没有定义SpecialString接口作为gender参数的类型,而把name和gender都定义为string类型,那么gender会覆盖name的值。如果您还没有明白,建议您把这篇文章从头到尾再看几遍。 func(inj*injector)Apply(valinterface{})error{ v:=reflect.ValueOf(val) forv.Kind()==reflect.Ptr{ v=v.Elem() } ifv.Kind()!=reflect.Struct{ returnnil } t:=v.Type() fori:=0;i<v.NumField();i++{ f:=v.Field(i) structField:=t.Field(i) iff.CanSet()&&structField.Tag=="inject"{ ft:=f.Type() v:=inj.Get(ft) if!v.IsValid(){ returnfmt.Errorf("Valuenotfoundfortype%v",ft) } f.Set(v) } } returnnil } Apply方法是用于对struct的字段进行注入,参数为指向底层类型为结构体的指针。可注入的前提是:字段必须是导出的(也即字段名以大写字母开头),并且此字段的tag设置为`inject`。以例子来说明: packagemain import( "fmt" "github.com/codegangsta/inject" ) typeSpecialStringinterface{} typeTestStructstruct{ Namestring`inject` Nick[]byte GenderSpecialString`inject` uidint`inject` Ageint`inject` } funcmain(){ s:=TestStruct{} inj:=inject.New() inj.Map("陈一回") inj.MapTo("男",(*SpecialString)(nil)) inj2:=inject.New() inj2.Map(20) inj.SetParent(inj2) inj.Apply(&s) fmt.Println("s.Name=",s.Name) fmt.Println("s.Gender=",s.Gender) fmt.Println("s.Age=",s.Age) } $cd$GOPATH/src/injector_test $gobuild $./injector_test s.Name=陈一回 s.Gender=男 s.Age=20 刑星写了一篇博文可供参考:在Golang中用名字调用函数,建议大家都去看下。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |