AOP 与 IOC 的关系
Go 生态与 AOP
IOC-golang 的 AOP 原理
3.1 IOC-golang 的接口注入
-
通过标签注入依赖对象
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 将实现注入至结构体指针
ServiceStruct *ServiceStruct `singleton:""`
// 将实现注入至接口
ServiceImpl Service `singleton:"main.ServiceImpl1"`
}
App 的 ServiceStruct 字段是具体结构的指针,字段本身已经可以定位期望被注入的结构,因此不需要在标签中给定期望被注入的结构名。对于这种注入到结构体指针的字段,无法通过注入接口代理的方式提供 AOP 能力,只能通过上文提到的 monkey 补丁方案,这种方式不被推荐。 App 的 ServiceImpl 字段是一个名为 Service 的接口,期望注入的结构指针是 main.ServiceImpl。本质上是一个从结构到接口的断言逻辑,虽然框架可以进行接口实现的校验,但仍需要结构使用者保证注入的接口实现了该方法。对于这种注入到接口的方式,IOC-golang 框架自动为 main.ServiceImpl 结构创建代理,并将代理结构注入在 ServiceImpl 字段,因此这一接口字段具备了 AOP 能力。 因此,ioc 更建议开发者面向接口编程,而不是直接依赖具体结构,除了 AOP 能力之外,面向接口编程也会提高 go 代码的可读性、单元测试能力、模块解耦合程度等。
-
通过 API 的方式获取对象
func GetServiceStructSingleton() (*ServiceStruct, error) {
i, err := singleton.GetImpl("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(*ServiceStruct)
return impl, nil
}
func GetServiceStructIOCInterfaceSingleton() (ServiceStructIOCInterface, error) {
i, err := singleton.GetImplWithProxy("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(ServiceStructIOCInterface)
return impl, nil
}
这两种通过 API 获取对象的方式可以由 iocli 工具自动生成,注意,这些代码的作用都是方便开发者调用 API ,减少代码量,而 ioc 自动装载的逻辑内核并不是由工具生成的,这是与 wire 提供的依赖注入实现思路的不同点之一,也是很多开发者误解的一点。
-
IOC-golang 的结构专属接口。
// +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 到代理结构,方便直接作为字段注入。 2、结构专属接口可以直接定位结构 ID,因此在注入专属接口的时候,标签无需显式指定结构类型:
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 注入 ServiceImpl 结构专属接口,无需在标签中指定结构ID
ServiceOwnInterface ServiceImplIOCInterface `singleton:""`
}
因此,随便找一个现有的 go 工程,其中使用结构指针的位置,我们推荐替换成结构专属接口,框架默认注入代理;对于其中已经使用了接口的字段,我们推荐直接通过标签注入结构,也是由框架默认注入代理。按照这种模式开发的工程,其全部对象都将具备运维能力。 3.2 代理的生成与注入
上一小节所提到的“注入至接口”的对象,都被被框架默认封装了代理,具备运维能力,并提到了 iocli 会为所有结构产生“专属接口”。在本节中,将解释框架如何封装代理层,如何注入至接口的。
-
代理结构的代码生成与注册
在前文提到生成的 zz.generated.ioc.go 代码中包含结构专属接口,同样,其中也包含结构代理的定义。还是以上文中提到的 ServiceImpl 结构为例,它生成的代理结构如下:
type serviceImpl1_ struct {
GetHelloString_ func(name string) string
}
func (s *serviceImpl1_) GetHelloString(name string) string {
return s.GetHelloString_(name)
}
代理结构命名为小写字母开头的 $(结构名)_,其实现了“结构专属接口” 的全部方法,并将所有方法调用代理至 $(方法名)_ 的方法字段,该方法字段会被框架以反射的方式实现。
func init(){
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{} {
return &serviceImpl1_{} // 注册代理结构
},
})
}
-
代理对象的注入 上述内容描述了代理结构的定义和注册过程。当用户期望获取封装了AOP层的代理对象,将首先加载真实对象,然后尝试加载代理对象,最终通过反射实例化代理对象,注入接口,从而赋予接口运维能力。该过程可由下图展示:
IOC-golang 基于 AOP 的应用
4.1 方法、参数可观测
-
查看应用接口和方法
% iocli list
github.com/alibaba/ioc-golang/extension/autowire/rpc/protocol/protocol_impl.IOCProtocol
[ ]
github.com/ioc-golang/shopping-system/internal/auth.Authenticator
[ ]
github.com/ioc-golang/shopping-system/pkg/service/festival/api.serviceIOCRPCClient
[ ]
-
监听调用参数
iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
发起针对入口的调用 curl -i -X GET 'localhost:8080/festival/listCards?user_id=1&num=10'
可查看到被监听方法的调用参数和返回值,user id 为1。 % iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
========== On Call ==========
github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()
Param 1: (int64) 1
========== On Response ==========
github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()
Response 1: (bool) true
4.2 全链路追踪
基于 IOC-golang 的 AOP 层,可以提供用户无感知、业务无侵入的分布式场景下全链路追踪能力。即一个由本框架开发的系统,可以以任何一个接口方法为入口,采集到方法粒度的跨进程调用全链路。 基于 shopping-system 的全链路耗时信息,可以排查到名为 festival 进程的 gorm.First() 方法是系统的瓶颈。 这个能力的实现包括两部分,分别是进程内的方法粒度链路追踪,和进程之间的 RPC 调用链路追踪。IOC 旨在打造开发者开箱即用的应用开发生态组件,这些内置的组件与框架提供的 RPC 能力都具备了运维能力。
-
基于 AOP 的进程内链路追踪 IOC-golang 提供的链路追踪能力的进程内实现,是基于 AOP 层做的,为了做到业务无感知,我们并没有通过 context 上下文的方式去标识调用链路,而是通过 go routine id 进行标识。通过 go runtime 调用栈,来记录当前调用相对入口函数的深度。
-
基于 IOC 原生 RPC 的进程间链路追踪 IOC-golang 提供的原生 RPC 能力,无需定义 IDL文件,只需要为服务提供者标注 // +ioc:autowire:type=rpc ,即可生成相关注册代码和客户端调用存根,启动时暴露接口。客户端只需要引入这一接口的客户端存根,即可发起调用。这一原生 RPC 能力基于 json 序列化和 http 传输协议,方便承载链路追踪 id。
展望
参考链接:
[1]https://github.com/grpc-ecosystem/go-grpc-middleware
[2]https://github.com/alibaba/ioc-golang
[3]https://github.com/google/wire
[4]https://github.com/ioc-golang/shopping-system
开发者评测局第五期--Severless函数计算专场来啦
Beats耳机、机械键盘、千元天猫超市卡等你来拿,参与Severless评测抢“鲜”体验限量高级版产品。发布你的评测就有机会赢取大奖!
点击阅读原文查看详情。
文章评论