2

IOC-Golang 的 AOP 原理与应用-51CTO.COM

 1 year ago
source link: https://developer.51cto.com/article/712902.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.

作者 |   李志信(冀锋)

​AOP 与 IOC 的关系

AOP (面向切面编程)是一种编程设计思想,旨在通过拦截业务过程的切面,实现特定模块化的能力,降低业务逻辑之间的耦合度。这一思路在众多知名项目中都有实践。例如 Spring 的切点 PointCut 、 gRPC的拦截器 Interceptor 、Dubbo 的过滤器 Filter。AOP 只是一种概念,这种概念被应用在不同的场景下,产生了不同的实现。

我们首先讨论比较具体的 RPC 场景,以 gRPC 为例。

图片

图片摘自 grpc.io

针对一次 RPC 过程,gRPC 提供了可供用户扩展的 Interceptor 接口,方便开发者写入与业务相关的拦截逻辑。例如引入鉴权、服务发现、可观测等能力,在 gRPC 生态中存在很多基于 Interceptor 的扩展实现,可参考 go-grpc-middleware[1]。这些扩展实现归属于 gRPC 生态,限定于 Client 和 Server 两侧的概念,限定于 RPC 场景。

我们将具象的场景抽象化,参考 Spring 的做法。

Spring 具备强大的依赖注入能力,在此基础之上,提供了适配与业务对象方法的 AOP 能力,可以通过定义切点,将拦截器封装在业务函数外部。这些 “切面”、“切点” 的概念,都是限定于 Spring 框架内,由其依赖注入(也就是 IOC)能力所管理。

我想表达的观点是,AOP 的概念需要结合具体场景落地,必须受到来自所集成生态的约束。我认为单独提 AOP 的概念,是不具备开发友好性和生产意义的,例如我可以按照面向过程编程的思路,写一连串的函数调用,也可以说这是实现了 AOP,但其不具备可扩展性、可迁移性、更不具备通用性。这份约束是必要的,可强可弱,例如 Spring 生态的 AOP,较弱的约束具备较大的可扩展性,但实现起来相对复杂,发者需要学习其生态的众多概念与 API,再若 Dubbo 、gRPC 生态的适配于 RPC 场景的 AOP,开发者只需要实现接口并以单一的 API 注入即可,其能力相对局限。

上述 “约束” 在实际开发场景可以具象为依赖注入,也就是 IOC。开发者需要使用的对象由生态所纳管、封装,无论是 Dubbo 的 Invoker、还是 Spring 的 Bean,IOC 过程为 AOP 的实践提供了约束借口,提供了模型,提供了落地价值。

图片

Go 生态与 AOP

AOP 概念与语言无关,虽然我赞成使用 AOP 的最佳实践方案需要 Java 语言,但我不认为 AOP 是 Java 语言的专属。在我所熟悉的 Go 生态中,依然有较多基于 AOP 思路的优秀项目,这些项目的共性,也如我上一节所阐述的,都是结合特定生态,解决特定业务场景问题,其中解决问题的广度,取决于其 IOC 生态的约束力。IOC 是基石,AOP 是 IOC 生态的衍生物,一个不提供 AOP 的 IOC 生态可以做的很干净很清爽,而一个提供 AOP 能力的 IOC 生态,可以做的很包容很强大。

上个月我开源了 IOC-golang [2]服务框架,专注于解决 Go 应用开发过程中的依赖注入问题。很多开发者把这个框架和 Google 开源的 wire [3]框架做比较,认为没有 wire 清爽好用,这个问题的本质是两个生态的设计初衷不同。wire 注重 IOC 而非 AOP,因此开发者可以通过学习一些简单的概念和 API,使用脚手架和代码生成能力,快速实现依赖注入,开发体验很好。IOC-golang 注重基于 IOC 的 AOP 能力,并拥抱这一层的可扩展性,把 AOP 能力看作这一框架和其他 IOC 框架的差异点和价值点。

相比于解决具体问题的 SDK,我们可以把依赖注入框架的 IOC 能力看作“弱约束的IOC场景”,通过两个框架差异点比较,抛出两个核心的问题:

Go 生态在 “弱约束 IOC 的场景” 需不需要 AOP?

GO 生态在 “弱约束 IOC 的场景” 的 AOP 可以用来做什么?

我的观点是:Go 生态一定是需要 AOP 的,即使在“弱约束 IOC 场景”,依然可以使用 AOP 来做一些业务无关的事情,比如增强应用的运维可观测能力。由于语言特性,Go 生态的 AOP 不能和 Java 划等号,Go 不支持注解,限制了开发者使用编写业务语义 AOP 层的便利性,所以我认为 Go 的 AOP 并不适合处理业务逻辑,即使强行实现出来,也是反直觉的。我更接受把运维可观测能力赋予 Go 生态的 AOP 层,而开发者对于 AOP 是无感知的。

例如,对于任何接口的实现结构,都可以使用 IOC-golang 框架封装运维 AOP 层,从而让一个应用程序的所有对象都具备可观测能力。除此之外,我们也可以结合 RPC 场景、服务治理场景、故障注入场景,产生出更多 “运维” 领域的扩展思路。

IOC-golang 的 AOP 原理

使用 Go 语言实现方法代理的思路有二,分别为通过反射实现接口代理,和基于 Monkey 补丁的函数指针交换。后者不依赖接口,可以针对任何结构的方法封装函数代理,需要侵入底层汇编代码,关闭编译优化,对于 CPU 架构有要求,并且在处理并发请求时会显著削弱性能。

前者的生产意义较大,依赖接口,也是本节所讨论的重点。

3.1 IOC-golang 的接口注入

在本框架开源的第一篇文章中有提到,IOC-golang 在依赖注入的过程具备两个视角,结构提供者和结构使用者。框架接受来自结构提供者定义的结构,并按照结构使用者的要求把结构提供出来。结构提供者只需关注结构本体,无需关注结构实现了哪些接口。而结构使用者需要关心结构的注入和使用方式:是注入至接口?注入至指针?是通过 API 获取?还是通过标签注入获取?

  • 通过标签注入依赖对象
// +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 的方式获取对象

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
}

使用 IOC-golang 框架的开发者更推荐通过API 的方式获取接口对象,通过调用自动装载模型(例如singleton)的 GetImplWithProxy 方法,可以获取代理结构,该结构可被断言为一个接口供使用。这个接口并非结构提供者手动创建,而是由 iocli 自动生成的“结构专属接口”,在下文中将予以解释。

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-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,专属接口包含了结构的全部方法。专属接口的作用有二:

  • 减轻开发者工作量,方便直接通过 API 的方式 Get 到代理结构,方便直接作为字段注入。
  • 结构专属接口可以直接定位结构 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 的应用

理解了上文中提到的实现思路,我们可以认为,使用 IOC-golang 框架开发的应用程序中,从框架注入、获取的所有接口对象都是具备运维能力的。我们可以基于 AOP 的思路,扩展出我们期望的能力。我们提供了一个简易的电商系统 demo shopping-system[4],展示了在分布式场景下 IOC-golang 基于 AOP 的可视化能力。感兴趣的开发者可以参考 README,在自己的集群里运行这个系统,感受其运维能力底座。

4.1 方法、参数可观测

  • 查看应用接口和方法
% iocli list
github.com/alibaba/ioc-golang/extension/autowire/rpc/protocol/protocol_impl.IOCProtocol
[Invoke Export]

github.com/ioc-golang/shopping-system/internal/auth.Authenticator
[Check]

github.com/ioc-golang/shopping-system/pkg/service/festival/api.serviceIOCRPCClient
[ListCards ListCachedCards]
  • 监听调用参数

通过 iocli watch命令, 我们可以监听鉴权接口的 Check 方法的调用:

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。

IOC-golang 开源至今已经突破 700 star,其热度的增长超出了我的想象,也希望这个项目能带来更大的开源价值与生产价值,欢迎越来越多的开发者参与到这个项目的讨论和建设中。

参考链接:

[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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK