3 分钟阅读

前言

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

  1. 组件(比如go语言中的structs)在初始化期间构建其自己的依赖关系。PS:对象主动创建和管理自己的依赖。
  2. 依赖注入:组件(比如go语言中的structs)在创建时应该接收它的依赖关系。PS:由容器自动管理对象的依赖关系

按照常规的应用开发模式,在一个“开发单元”内,开发者需要关注哪些事情?我们习惯于编写一个构造函数返回需要的对象,这个构造函数的入参,包含了一些参数以及下游依赖,我们在构造函数中会把这些对象和参数拼接成一个结构,再执行初始化逻辑,最后返回。比如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-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)
    

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.

标签:

分类:

更新时间:

留下评论