1 - IOC-控制反转
IOC 即控制反转,旨在降低代码的耦合度,减少冗余重复的对象组装代码,依赖注入是 IOC 思路的一种具体实现。
本框架命名为 IOC-golang, 是服务于 Go 开发者的 IOC 框架,使用语言原生概念,符合语言开发习惯。框架衍生出的多种能力和可扩展性,其能力建立在框架设计的 IOC 模型基础之上。
1.1 - 依赖注入
概念
依赖注入(Dependency Injection,DI) 是指开发人员无需显式地创建对象,而是通过特定标注(在本框架中为 标签 )的方式声明字段,由框架在对象加载阶段,将实例化对象注入该字段,以供后续使用。
优点
依赖注入可以降低代码的耦合度,减少冗余代码量,优化代码逻辑,使得开发人员可以只关心业务逻辑,帮助面向对象编程,面向接口编程。
IOC-golang 的依赖注入能力
本框架从两个角度实现依赖注入:
1.2 - 自动装载模型
概念
自动装载(Autowire)模型 封装了一类对象的装载方式,是本框架对装载过程抽象出的概念和接口。
用户需要定义的结构千变万化,如果每个结构描述符都要提供完整的装载过程信息,将是一件很麻烦的事情。我们将一类相似的结构抽象出一个 自动装载模型,选择注册在该模型的所有结构都需要遵循该模型的加载策略,这大大降低了用户需要提供的 结构描述符 内的定制化信息量,从而提高开发效率。
框架内置了两个基础自动装载模型:单例模型(singleton),多例模型(normal)
当前版本中,框架内置了三个扩展的自动装载模型:配置(config),gRPC 客户端(grpc),RPC(rpc)。其中配置模型是多例模型的扩展,gRPC 客户端是单例模型的扩展,RPC 模型提供了(rpc-client 和 rpc-server)两侧的自动装载模型。关于这三个自动装载模型的应用,可以参考example/autowire example/third_party/grpc 中给出的例子。
基于这些自动装载模型,框架内置了基于“扩展自动装载模型”的多个结构。例如,用户可以用几行代码将 “gRPC 客户端存根”注册在 “grpc 装载模型” 之上【示例】,再例如可以方便地从配置文件中的 指定位置读入 数据。
1.3 - 结构描述符
概念
结构描述符(Struct Descriptor, SD) 用于描述一个被开发者定义的结构,包含对象生命周期的全部信息,例如结构类型是什么,依赖哪些参数,如何被构造等等信息。
SD可以通过 注解 的方式使用工具 自动生成 。但还是推荐开发人员了解本框架定义的结构生命周期和结构描述信息,以便更清晰地了解加载过程。
对象生命周期
开发人员在 Go 语言开发过程中需要时时关注对象的生命周期,一个常见的对象生命周期如下:
- 对象定义:开发人员编码,编写结构,实现接口,确定模型(单例、多例..) 。
- 加载全部依赖:依赖的下游对象创建、配置读入等。
- 对象创建:产生一个基于该对象的指针
- 对象构造:获取全部依赖,并将其组装到空对象中,产生可用对象。
- 对象使用:调用对象的方法,读写对象字段。
- 对象销毁:销毁对象,销毁无需再使用的依赖对象。
参数
本框架的“参数”概念,是一个结构体,该结构体包含了创建一个对象所需全部依赖,并提供了构造方法。
例如:
type Config struct {
Host string
Port string
Username string
Password string
}
func (c *Config) New(mysqlImpl *Impl) (*Impl, error) {
var err error
mysqlImpl.db, err = gorm.Open(mysql.Open(getMysqlLinkStr(c)), &gorm.Config{})
mysqlImpl.tableName = c.TableName
return mysqlImpl, err
}
Config 结构即为 Impl 结构的“参数”。其包含了产生 Impl 结构的全部信息。
结构描述符 (Struct Descriptor)
定义的结构描述符如下:摘自 autowire/model.go
type StructDescriptor struct {
Factory func() interface{} // raw struct
ParamFactory func() interface{}
ParamLoader ParamLoader
ConstructFunc func(impl interface{}, param interface{}) (interface{}, error)
DestroyFunc func(impl interface{})
Alias string // alias of SDID
TransactionMethodsMap map[string]string // transaction
impledStructPtr interface{} // impledStructPtr is only used to get name
}
Factory【必要】
结构的工厂函数,返回值是未经初始化的空结构指针,例如
func () interface{}{
return &App{}
}
ParamFactory【非必要】
参数的工厂函数,返回值是未经初始化的空参数指针,例如
func () interface{}{
return &Config{}
}
ParamLoader【非必要】
参数加载器定义了参数的各个字段如何被加载,是从注入标签传入、还是从配置读入、或是以一些定制化的方式。
框架提供了默认的参数加载器,详情参阅 参数加载器概念
Constructor【非必要】
构造函数定义了对象被组装的过程。
入参为对象指针和参数指针,其中对象指针的所有依赖标签都已被注入下游对象,可以在构造函数中调用下游对象。参数指针的所有字段都已经按照参数加载器的要求加载好。构造函数只负责拼装。
返回值为经过拼装后的指针。例如:
func(i interface{}, p interface{}) (interface{}, error) {
param := p.(*Config)
impl := i.(*Impl)
return param.New(impl)
},
DestroyFunc 【非必要】
定义了对象的销毁过程,入参为对象指针。
Alias 【非必要】
由于 结构ID 一般较长,可以在这里指定结构的别名,可以通过这一别名替换 结构ID,调用 对象获取 API。
TransactionMethodsMap 【非必要】
基于 Saga 模型的事务函数声明, 这一 Map 的 Key 为需要使用事务能力的方法名,Value 为该方法的回滚函数,如果 Value 为空,则无回滚逻辑。参考事务例子 example/transaction
结构ID
结构(描述) ID定义为:"${包名}.${结构名}"
结构(描述) ID (Struct Description Identification) 在本文档和项目中多处被缩写为 SDID。
SDID 是唯一的,用于索引结构的键,类型为字符串。
例如,开发人员在 使用 API 获取对象 时,需要针对使用的自动装载模型,传入 SDID 来定位结构,从而获取对象。
1.4 - 参数加载器
概念
参数加载器描述了依赖参数如何在对象构造之前被加载,包括但不限于从配置加载、从 标签 参数加载等等。
参数加载器作为 SD(结构描述符)的一部分,可以被结构提供方定制化,也可以使用自动装载模型提供的默认参数加载器。
默认参数加载器
任何 SD 内定义参数加载器均被优先执行,如果加载失败,则尝试使用默认参数加载器加载。
默认参数加载器被两个基础自动装载模型(singleton、normal)引入。依次采用三种方式加载参数,如果三种方式均加载失败,则抛出错误。
方式1 从标签指向的对象名加载参数,参考 配置文件结构规范
Load support load struct described like:
```go
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{}{
return &Impl{}
},
ParamFactory: func() interface{}{
return &Config{}
},
ConstructFunc: func(i interface{}, p interface{}) (interface{}, error) {
return i, nil
},
})
}
type Config struct {
Address string
Password string
DB string
}
```
with
Autowire type 'normal'
StructName 'Impl'
Field:
MyRedis Redis `normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl, redis-1"`
from:
```yaml
extension:
normal:
github.com/alibaba/ioc-golang/extension/normal/redis.Impl:
redis-1:
param:
address: 127.0.0.1
password: xxx
db: 0
```
方式2 从标签加载参数
Load support load param like:
```go
type Config struct {
Address string
Password string
DB string
}
```
from field:
```go
NormalRedis normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,address=127.0.0.1&password=xxx&db=0"`
```
方式3 从配置加载参数,参考 配置文件结构规范
Load support load struct described like:
```go
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{}{
return &Impl{}
},
ParamFactory: func() interface{}{
return &Config{}
},
ConstructFunc: func(i interface{}, p interface{}) (interface{}, error) {
return i, nil
},
})
}
type Config struct {
Address string
Password string
DB string
}
```
with
Autowire type 'normal'
StructName 'Impl'
from:
```yaml
autowire:
normal:
github.com/alibaba/ioc-golang/extension/normal/redis.Impl:
param:
address: 127.0.0.1
password: xxx
db: 0
```
1.5 - 注解
概念
注解(annotation) 是 Go 代码中符合特定格式的注释,一般位于结构定义之前。可被命令行工具代码扫描识别,从而获取结构信息,自动生成需要的代码。
例如 快速开始 示例中的注解:
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
...
}
注解在 Go 应用开发的过程中,是一种直观、清晰的结构描述方式,通过使用注解进行标注,使用工具自动生成相关代码,可减少开发人员的工作量,降低代码重复率。
注解与代码生成
注解与代码生成能力,是为了让开发者无需关心 SD(结构描述符) 的组装和注册过程。开发者只需定义好结构,正确标注注解,即可使用 iocli 工 具自动生成当前目录和子目录下的 SD ,从而将编写的结构交给框架管理。
iocli 工具支持的注解
详情参阅 iocli #注解与代码生成
2 - AOP-面向切面编程
AOP (面向切面编程)是一种编程设计思想,旨在通过拦截业务过程的切面,实现特定模块化的能力,降低业务逻辑之间的耦合度。
IOC-golang 框架为所管理的 注入至接口字段 的结构封装 AOP 切面,从而实现方法粒度的运维可观测能力。
2.1 - 代理对象
基于接口的代理对象的使用与设计
概念
原始对象 是通过 结构描述符 直接创建的对象。
代理对象是对开发者提供的针对原始对象的封装,在 IOC-golang 的设计中,将“封装了 AOP 拦截器至原始结构,并赋值给接口”的对象,定义为“代理对象”。针对代理对象的函数调用在业务逻辑上完全等价于针对原始对象的函数调用,并为原始对象提供针对函数调用的 AOP 能力,这一能力可被应用在监控、可视化、事务等场景。
1. 代理对象由结构使用者关心
IOC-golang 在依赖注入的开发过程中存在两个视角,结构提供者和结构使用者。框架接受来自结构提供者定义的结构,并按照结构使用者的要求把结构提供出来。
结构提供者只需关注结构本体,无需关注结构实现了哪些接口。结构使用者需要关心结构的注入和使用方式,例如 通过 API 获取,或者通过 标签 注入。如通过标签注入,是注入至接口,或是注入至结构体指针。
2. 代理对象的获取
框架会默认为 注入/获取 至接口的场景注入代理对象。
2.1 通过标签注入代理对象
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 将结构对象注入至结构体指针
ServiceStruct *ServiceStruct `singleton:""`
// 将 main.ServiceImpl1 结构封装成代理对象,并注入至接口字段
ServiceImpl Service `singleton:"main.ServiceImpl1"`
}
App 的 ServiceStruct 字段是具体结构的指针,字段本身已经可以定位期望被注入的结构,因此不需要在标签中给定期望被注入的结构名。
App 的 ServiceImpl 字段是一个名为 Service 的接口,期望注入的结构指针是 main.ServiceImpl。本质上是一个从结构到接口的断言逻辑,虽然框架可以进行接口实现的校验,但仍需要结构使用者保证注入的接口实现了该方法。对于这种注入到接口的方式,IOC-golang 框架自动为 main.ServiceImpl 结构创建代理,并将代理结构注入在 ServiceImpl 字段,因此这一接口字段具备了 AOP 能力。
因此,ioc 更建议开发者面向接口编程,而不是直接依赖具体结构,除了 AOP 能力之外,面向接口编程也会提高 go 代码的可读性、单元测试能力、模块解耦合程度等。
2.2 通过 API 的方式获取代理对象
IOC-golang 框架的开发者可以通过 API 的方式 获取结构指针,通过调用自动装载模型(例如singleton)的 GetImpl
方法,可以获取结构指针。可以在生成的代码中找到类似如下的函数。
func GetServiceStructSingleton() (*ServiceStruct, error) {
i, err := singleton.GetImpl("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(*ServiceStruct)
// 返回原始结构体指针
return impl, nil
}
上述获取的是 结构体指针,我们更推荐开发者通过 API ,调用下面的方法获取接口对象,通过调用自动装载模型(例如singleton)的 GetImplWithProxy
方法,可以获取代理对象,该对象可被断言为一个接口供使用。
在使用 iocli 工具生成代码的时候,会默认为每个结构生成一个结构专属接口,可以在生成的代码中找到类似如下的函数,通过调用该函数,可以直接获取专属接口形态的代理对象。
func GetServiceStructIOCInterfaceSingleton() (ServiceStructIOCInterface, error) {
// 获取代理对象
i, err := singleton.GetImplWithProxy("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
// 将代理对象断言成对象专属接口 ServiceStructIOCInterface
impl := i.(ServiceStructIOCInterface)
// 返回结构专属接口形态的代理对象
return impl, nil
}
这两种通过 API 获取对象的方式可以由 iocli 工具自动生成。注意!这些代码的作用都是方便开发者调用 API ,减少代码编写量,而 ioc 自动装载的逻辑内核并不是由工具生成的,这是与 wire 提供的依赖注入实现思路的不同点之一,也是很多开发者误解的一点。
3. 结构专属接口
通过上面的介绍,我们知道 IOC-golang 框架提供了封装 AOP 层的代理对象,其注入方式是 强依赖接口 的。但要求开发者为自己的全部结构都手写一个与之匹配的接口出来,这会耗费大量的时间。因此 iocli 工具可以自动生成结构专属接口,减轻开发人员的代码编写量。
例如一个名为 ServiceImpl
的结构,其包含 GetHelloString
方法
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type ServiceImpl struct {
}
func (s *ServiceImpl) GetHelloString(name string) string {
return fmt.Sprintf("This is ServiceImpl1, hello %s", name)
}
当执行 iocli gen 命令后, 会在当前目录生成一份代码zz_generated.ioc.go 其中包含该结构的“专属接口”:
type ServiceImplIOCInterface interface {
GetHelloString(name string) string
}
专属接口的命名为 $(结构名)IOCInterface
,专属接口包含了结构的全部方法。专属接口的作用有二:
1、减轻开发者工作量,方便直接通过 API 的方式 Get 到代理结构,方便直接作为字段注入,见上述1.2节。
2、结构专属接口可以直接定位结构 ID,因此在注入专属接口的时候,标签无需显式指定结构类型:
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 注入 ServiceImpl 结构专属接口,无需在标签中指定结构ID
ServiceOwnInterface ServiceImplIOCInterface `singleton:""`
}
2.2 - AOP 实现
基于接口代理的 AOP 实现
概念
一个 AOP 实现 是基于框架定义的 AOP 结构所创建的实例化对象。其字段包含了一类 AOP 所关注问题的解决方案。
func init() {
// 注册一个名为 “list” 的 AOP 实现到框架。该实现提供了基于 debug 端口展示所有接口方法的 gRPC 服务。
aop.RegisterAOP(aop.AOP{
Name: "list",
GRPCServiceRegister: func(server *grpc.Server) {
list.RegisterListServiceServer(server, newListServiceImpl())
},
})
}
AOP 实现,具体来讲是 代理对象 在原始对象的基础上封装的拦截层及其周边能力。框架提供了一些基础的 AOP 代理层实现,例如上述 “list”。开发人员也可以将自定义的 AOP 代理层注册在框架上使用。一个 AOP 实现对象,关注的是一类问题的 AOP 解决方案,例如可视化、事务、链路追踪。
AOP 实现是一个对象,包含一些聚合在一起的概念。在当前版本中,它可以包含 AOP 领域的四个角度的解决方案:
函数调用拦截器
可以拦截针对所有代理对象的请求。
RPC调用拦截器
RPC 自动装载模型是框架默认提供的扩展自动装载模型,它提供了框架原生支持的 RPC 能力,可被开发者直接选用。在 RPC 自动装载模型中,会在所有 RPC 过程中调用已注册的 AOP 实现提供的 “RPC调用拦截器”,从而拦截全量框架原生 RPC 请求。
GRPC 服务注册器
本框架提供了调试端口 (默认 1999),被一个基于 gRPC 的 debug 服务监听 。AOP 实现可以将“待采集” 或 “待触发” 的被动逻辑以 gRPC Service 的形式注册在 debug 服务上,从而对外暴露。
与 gRPC Service 相对应的是客户端,在本框架默认提供的 AOP 实现中,客户端作为指令被注册在了 iocli 工具上,其实现与 AOP 实现处于同一pkg 下。本质上,gRPC 客户端、gRPC Service 、 两个拦截器的实现形成闭环,共同解决当前 AOP 实现所关注的问题。开发者可以选择其中的一个或多个进行实现。
ConfigLoader
提供当前 AOP 实现的读取框架配置能力。
AOP 结构的代码定义
type AOP struct {
Name string
InterceptorFactory interceptorFactory // type interceptorFactory func() Interceptor
RPCInterceptorFactory rpcInterceptorFactory // type rpcInterceptorFactory func() RPCInterceptor
GRPCServiceRegister gRPCServiceRegister // type gRPCServiceRegister func(server *grpc.Server)
ConfigLoader func(config *common.Config)
}
“AOP 实现” 的工作原理
AOP 实现的加载和工作流程:
在 init 函数中将 AOP 实现注册在框架上,引入这一 pkg。
func init(){
aop.RegisterAOP(aop.AOP{
...
})
}
ioc.Load()
阶段,加载框架配置,调用 ConfigLoader 将框架配置传递给 AOP 实现。
依赖注入阶段,如首次出现构建代理对象的情况,将调用所有注册在框架的 AOP 实现的 InterceptorFactory,获取到所有单例的函数拦截器。封装入代理对象。InterceptorFactory 只会被调用一次。
RPC 调用阶段,如首次出现 RPC 调用到情况,将调用所有注册在框架的 AOP 实现的 RPCInterceptorFactory,获取到所有单例的RPC 调用拦截器,应用在 RPC过程中。
2.3 - 拦截器
跟随 AOP 对象注册的拦截器模型
概念
拦截器 是 AOP 思路的基本切面单元。在 AOP 实现 中提到,本框架提供了针对代理对象函数调用的拦截器模型,和框架原生支持的 RPC 拦截器模型。
需要注意,在当前版本中,函数拦截器都是单例的,即 AOP 实现中的工厂函数只会被调用一次。函数拦截器尚不能保证调用顺序。
拦截器接口
// 函数拦截器
type Interceptor interface {
BeforeInvoke(ctx *InvocationContext)
AfterInvoke(ctx *InvocationContext)
}
// RPC 调用拦截器
type RPCInterceptor interface {
BeforeClientInvoke(req *http.Request) error
AfterClientInvoke(rsp *http.Response) error
BeforeServerInvoke(c *gin.Context) error
AfterServerInvoke(c *gin.Context) error
}
开发者可以按需定制单例模式的拦截器,并通过 AOP 实现注册在框架上,以供使用。
函数拦截器的参数为 InvocationContext
其包含了一次请求的上下文信息:
type InvocationContext struct {
ProxyServicePtr interface{} // 被调用的代理对象
SDID string // 原始对象ID
MethodName string // 被调用的方法名
MethodFullName string // 被调用方法全名,包含了包名、结构名、方法名,例如 github.com/alibaba/ioc-golang/test.(*App).Run
Params []reflect.Value // 请求参数列表
ReturnValues []reflect.Value // 返回参数列表
GrID int64 // 当前 goroutine ID
}
在调用原始对象的函数之前,所有注册在框架等函数拦截器的 BeforeInvoke
方法将依此被调用,此时上下文中 ReturnValues
字段为空 ;在调用原始对象的函数之后,所有注册在框架等函数拦截器的 BeforeInvoke
方法将依此被调用。
拦截器注册 API
在 init 方法中跟随 AOP 对象注册至框架,例如:
func init() {
aop.RegisterAOP(aop.AOP{
Name: "monitor",
InterceptorFactory: func() aop.Interceptor {
return getMonitorInterceptorSingleton() // 函数调用拦截器注册
},
GRPCServiceRegister: func(server *grpc.Server) {
monitorPB.RegisterMonitorServiceServer(server, newMonitorService())
},
})
}