1

go依赖注入

 1 year ago
source link: https://qiankunli.github.io/2023/02/16/go_ioc.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

go依赖注入

2023年02月16日

设计模式分为创建、结构和行为三大类,如果自己构造依赖关系, 则创建 与 行为 两个目的的代码容易耦合在一起, 代码较长,给理解造成困难。

  1. 组件(比如go语言中的structs)在初始化期间构建其自己的依赖关系
  2. 依赖注入:组件(比如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 语言开发习惯,能真正为开发人员提供编程、思考、运维、以及代码阅读上的便利。

go_ioc_layer.png

Spring的核心抽象是使用了BeanDefinition、BeanFactory、BeanDefinitionRegistry、BeanDefinitionReader等核心抽象实现了Bean的定义、获取和创建。

spring ioc-golang
  BeanDefinition StructDescriptor
  BeanFactory、BeanDefinitionRegistry singleton/normal
注册方式 Xml/注解 自动生成代码用于注册
使用 beanFacory.GetBean 每一个struct 自动生成代码。singleton.GetImpl(sdid, nil)

IOC-golang 的 AOP 原理与应用

通过标签注入依赖对象

// +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 将会生成

  1. 专属interface,iocli 会为任何期望注册在框架的结构生成专属接口,专属接口的命名规则为 $(结构名)IOCInterface
     type AppIOCInterface interface {
         Run()
     }
    
  2. 代理struct,实现专属interface,包含一个代理方法。任何被注入到接口的字段(字段类型是 interface),都会被框架自动封装代理 AOP 层,即注入到接口的结构体指针,并非真实结构体指针,而是封装了结构体的代理指针。
     type app_ struct {
         Run_ func()
     }
     func (a *app_) Run() {
         a.Run_()
     }
    
  3. 注册自定义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)
    

使用 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 语言官方依赖注入工具 Wire 使用指北Wire 是一个强大的依赖注入工具。与 Inject 、Dig 等不同的是,Wire只生成代码而不是使用反射在运行时注入,不用担心会有性能损耗。

Go中的依赖注入 推荐使用 uber-go/dig A reflection based dependency injection toolkit for Go.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK