前言
设计模式分为创建、结构和行为三大类,如果自己构造依赖关系, 则创建 与 行为 两个目的的代码容易耦合在一起, 代码较长,给理解造成困难。
- 组件(比如go语言中的structs)在初始化期间构建其自己的依赖关系
- 依赖注入:组件(比如go语言中的structs)在创建时应该接收它的依赖关系。PS:这个理念在java、spring 已经普及多年。
按照常规的应用开发模式,在一个“开发单元”内,开发者需要关注哪些事情?我们习惯于编写一个构造函数返回需要的对象,这个构造函数的入参,包含了一些参数以及下游依赖,我们在构造函数中会把这些对象和参数拼接成一个结构,再执行初始化逻辑,最后返回。比如A 调用B且由不同人负责,B有多个构造函数,对于A来说,调用B 还要明白构造函数细节。所以ioc 一般要求 组件不提供构造函数(即使用默认构造函数),依赖的组件、配置 由ioc 负责注入,通过框架将 业务无关代码(用来表示对象的 代码中的耦合关系) 形式化为 代码外的xml/yaml(代码中的annotation),减少了代码量,明确了关系。
深入浅出依赖注入及其在抖音直播中的应用在软件工程中,依赖注入(dependency injection)的意思为:给予调用方它所需要的事物。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。该设计的目的是为了分离调用方和依赖方,从而实现代码的高内聚低耦合,提高可读性以及重用性。
Go和依赖注入
为什么依赖注入只在 Java 技术栈中流行,在 go 和 cpp 没有大量使用?依赖注入Dependency-Injection (DI)只是Inversion-of-Control (IoC) 的一种实现方式,IOC还有许多更常见的实现,比如callback,胖指针。为什么java选择了di而不是callback或者其他,java中定义callback的开销无异于一个实体bean。所以java走得更进一步,把bean抽取出来,使用delegation,proxy等设计模式,实现了DI。对于go或者cpp这种native语言来说,既有闭包又有函数指针,实现ioc的手段有很多,但di却是开销很大的一种,所以比较少见,大白话就是可以但没有必要。
http.HandleFunc("/", func(w http.ResponseWriter,r *http.Request) {
fmt.Fprintf(w, "Hello world!")
}
IOC-golang
Alibaba/IOC-golang 正式开源 ——打造服务于go开发者的IOC框架在面向对象编程的思路下,开发者需要直接关心对象之间的依赖关系、对象的加载模型、对象的生命周期等等问题。对于较为复杂的业务应用系统,随着对象数目增长,对象之间的拓扑关系呈指数级增加,如果这些逻辑全部由开发人员手动设计和维护,将会在应用内保存较多业务无关的冗余代码,影响开发效率,提高代码学习成本,增加了模块之间的耦合度,容易产生循环依赖等等问题。随着开发者的增多,设计模型的复杂化,将会产生对象管理框架的诉求,例如 Java 生态的 Spring 框架,其设计的核心就是控制反转思路,从而为开发者提供依赖注入、配置注入、生命周期管理等能力。Go 语言生态在开源侧也有较多基于该思路的实现,但普遍能力较为单一,相比于我们的设计思路 ,在可扩展性、易用性等方面有所不足。IOC-golang 不是 Go 语言实现的 Spring 框架!我们致力于打造一款针对 Go 开发人员的框架,它适配与 Go 的语法和各种基本概念,符合 Go 语言开发习惯,能真正为开发人员提供编程、思考、运维、以及代码阅读上的便利。
Spring的核心抽象是使用了BeanDefinition、BeanFactory、BeanDefinitionRegistry、BeanDefinitionReader等核心抽象实现了Bean的定义、获取和创建。
spring | ioc-golang | |
---|---|---|
BeanDefinition | StructDescriptor | |
BeanFactory、BeanDefinitionRegistry | singleton/normal | |
注册方式 | Xml/注解 | 自动生成代码用于注册 |
使用 | beanFacory.GetBean | 每一个struct 自动生成代码。singleton.GetImpl(sdid, nil) |
通过标签注入依赖对象
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 将实现注入至结构体指针,字段本身已经可以定位期望被注入的结构,因此不需要在标签中给定期望被注入的结构名
ServiceStruct *ServiceStruct `singleton:""`
// 将实现注入至接口,框架自动为 main.ServiceImpl 结构创建代理,并将代理结构注入在 ServiceImpl 字段
ServiceImpl Service `singleton:"main.ServiceImpl"`
}
通过 API 的方式获取对象
app, err := GetAppSingleton() // 获取真实结构体指针
func GetAppSingleton() (*App, error) {
if _appSDID == "" {
_appSDID = util.GetSDIDByStructPtr(new(App))
}
i, err := singleton.GetImpl(_appSDID, nil)
if err != nil {
return nil, err
}
impl := i.(*App)
return impl, nil
}
app, err := GetAppIOCInterfaceSingleton() // 获得封装了代理层的接口
代码生成
对于自定义一个struct ,iocli gen
将会生成
- 专属interface,iocli 会为任何期望注册在框架的结构生成专属接口,专属接口的命名规则为
$(结构名)IOCInterface
type AppIOCInterface interface { Run() }
- 代理struct,实现专属interface,包含一个代理方法。任何被注入到接口的字段(字段类型是 interface),都会被框架自动封装代理 AOP 层,即注入到接口的结构体指针,并非真实结构体指针,而是封装了结构体的代理指针。
type app_ struct { Run_ func() } func (a *app_) Run() { a.Run_() }
- 注册自定义struct、代理struct到 StructDescriptor
// 注册代理结构 normal.RegisterStructDescriptor(&autowire.StructDescriptor{ Factory: func() interface{} { return &app_{} }, }) appStructDescriptor := &autowire.StructDescriptor{ Factory: func() interface{} { return &App{} }, Metadata: map[string]interface{}{ "aop": map[string]interface{}{}, "autowire": map[string]interface{}{}, }, } singleton.RegisterStructDescriptor(appStructDescriptor)
AOP
使用 Go 语言实现方法代理的思路有二,分别为通过反射实现接口代理,和基于 Monkey 补丁的函数指针交换。后者不依赖接口,可以针对任何结构的方法封装函数代理,需要侵入底层汇编代码,关闭编译优化,对于 CPU 架构有要求,并且在处理并发请求时会显著削弱性能。
源码分析
获取真实结构体指针
// 注册
singleton.RegisterStructDescriptor(appStructDescriptor)
// <id,sd>
var singletonStructDescriptorsMap = make(map[string]*autowire.StructDescriptor)
autowire.RegisterStructDescriptor(sd)
// <id,sd>
structDescriptorsMap[sd.ID()] = sd
registerImplements(sd) // 建立 sd 实现的interface 与 sd的关联关系
// 获取
singleton.GetImpl(_appSDID, nil)
autowire.Impl(Name, sdId, param)
sd := GetStructDescriptor(targetSDID)
WrapperAutowireImpl.ImplWithParam(targetSDID, param, expectWithProxy=false, force)
rawPtr, err = w.Autowire.Factory(sdID) // 实例化目标对象 得到 rawPtr, sd.Factory
rawPtr, err = w.Autowire.Construct(sdID, rawPtr, param) // construct field, sd.Contstruct
w.inject(rawPtr, sdID) // 根据rawPtr 获取目标对象的类型,进而获取其包含的字段,再根据字段类型 实例化字段对象
获得封装了代理层的接口。
// 注册
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{} {
return &app_{}
},
})
// 获取
singleton.GetImplWithProxy(_appSDID, nil)
autowire.ImplWithProxy(Name, sdId, param)
wrapperAutowire.ImplWithParam(targetSDID, param, expectWithProxy=true, force)
rawPtr, err = w.Autowire.Factory(sdID)
rawPtr, err = w.Autowire.Construct(sdID, rawPtr, param)
w.inject(rawPtr, sdID)
finalPtr = GetProxyFunction()(rawPtr)
proxyFunction(rawPtr)
proxyStructPtr, err := normal.GetImpl(proxySDID, nil) // 即代理struct
implProxy(rawPtr, proxyStructPtr, sdid) // 为代理struct field(本质是一个方法) 赋值
proxyFunc = makeProxyFunction // 构造代理后的方法
return proxyStructPtr
proxyPtr = finalPtr
impl := i.(AppIOCInterface)
框架目前提供的aop 是针对全部对象的。每一个raw struct 会有一个对应的 proxy struct,都实现了 专属接口,proxy struct 与raw struct 拥有相同的个方法,每个方法对应一个 function field,初始化时会被赋值为 interceptor 与 raw stuct 合成后的方法。比如Run 方法,proxy.Run() ==> proxy.Run field ==> interceptor.BeforeInvoke + raw.Run + interceptor.AfterInvoke。
将 Interceptor 与 rawFunction “合成”一个新的方法
func makeProxyFunction(proxyPtr interface{}, rf reflect.Value, sdid, methodName string, isVariadic bool) func(in []reflect.Value) []reflect.Value {
rawFunction := rf
interceptorImpls := getInterceptors()
proxyFunc := func(in []reflect.Value) []reflect.Value {
invocationCtx := NewInvocationContext(proxyPtr, sdid, methodName, common.CurrentCallingMethodName(3), in)
for _, i := range interceptorImpls {
i.BeforeInvoke(invocationCtx) // 前置方法
}
defer func() {
for _, i := range interceptorImpls {
i.AfterInvoke(invocationCtx) // 前置方法
}
}()
if isVariadic {
varParam := in[len(in)-1]
in = in[:len(in)-1]
for j, l := 0, varParam.Len(); j < l; j++ {
in = append(in, varParam.Index(j))
}
}
out := rawFunction.Call(in) // 原方法调用
invocationCtx.SetReturnValues(out)
return out
}
return proxyFunc
}
Interceptor 的来源
// github.com/alibaba/IOC-golang/aop/aop.go
type Interceptor interface {
BeforeInvoke(ctx *InvocationContext)
AfterInvoke(ctx *InvocationContext)
}
var interceptorFactories = make([]interceptorFactory, 0)
func RegisterAOP(aopImpl AOP) {
aops = append(aops, aopImpl)
if aopImpl.InterceptorFactory != nil {
interceptorFactories = append(interceptorFactories, aopImpl.InterceptorFactory)
}
if aopImpl.RPCInterceptorFactory != nil {
rpcInterceptorFactories = append(rpcInterceptorFactories, aopImpl.RPCInterceptorFactory)
}
if aopImpl.GRPCServiceRegister != nil {
grpcServiceRegisters = append(grpcServiceRegisters, aopImpl.GRPCServiceRegister)
}
if aopImpl.ConfigLoader != nil {
configLoaderFuncs = append(configLoaderFuncs, aopImpl.ConfigLoader)
}
}
以log 为例
// github.com/alibaba/IOC-golang/extension/aop/log/aop.go
func init() {
aop.RegisterAOP(aop.AOP{
Name: Name,
InterceptorFactory: func() aop.Interceptor {
// get loaded logInterceptor singleton
logInterceptorSingleton, _ := GetlogInterceptorIOCInterfaceSingleton(nil)
return logInterceptorSingleton
},
GRPCServiceRegister: func(server *grpc.Server) {...},
ConfigLoader: func(aopConfig *common.Config) {...},
})
}
// github.com/alibaba/IOC-golang/extension/aop/log/interceptor.go
// +ioc:autowire=true
// +ioc:autowire:type=singleton
// +ioc:autowire:proxy:autoInjection=false
// +ioc:autowire:paramType=logInterceptorParams
// +ioc:autowire:constructFunc=initLogInterceptor
type logInterceptor struct {
...
}
func (w *logInterceptor) BeforeInvoke(ctx *aop.InvocationContext) {
...
}
func (w *logInterceptor) AfterInvoke(ctx *aop.InvocationContext) {
...
}
在go build 编译的时候 通过 -tags disableAOP
编译参数即可引入/排除所有 aop 扩展实现。
ioc-golang
/extension
/aop
/log
/monitor
/trace
/imports
/cli
/boot
/imports.go # 将extension/aop 下的所有目录都引入了一遍,包含 `//go:build !disableAOP`
/imports_default.go # 空文件,包含 `//go:build disableAOP`
其它方案
Go 语言官方依赖注入工具 Wire 使用指北Wire 是一个强大的依赖注入工具。与 Inject 、Dig 等不同的是,Wire只生成代码而不是使用反射在运行时注入,不用担心会有性能损耗。
Go中的依赖注入 推荐使用 uber-go/dig A reflection based dependency injection toolkit for Go.