.

.

AOP-面向切面编程

AOP (面向切面编程)是一种编程设计思想,旨在通过拦截业务过程的切面,实现特定模块化的能力,降低业务逻辑之间的耦合度。

IOC-golang 框架为所管理的 注入至接口字段 的结构封装 AOP 切面,从而实现方法粒度的运维可观测能力。

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 - 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 实现的加载和工作流程:

  1. 在 init 函数中将 AOP 实现注册在框架上,引入这一 pkg。

    func init(){
      aop.RegisterAOP(aop.AOP{
      ...
      })
    }
    
  2. ioc.Load() 阶段,加载框架配置,调用 ConfigLoader 将框架配置传递给 AOP 实现。

  3. 依赖注入阶段,如首次出现构建代理对象的情况,将调用所有注册在框架的 AOP 实现的 InterceptorFactory,获取到所有单例的函数拦截器。封装入代理对象。InterceptorFactory 只会被调用一次。

  4. RPC 调用阶段,如首次出现 RPC 调用到情况,将调用所有注册在框架的 AOP 实现的 RPCInterceptorFactory,获取到所有单例的RPC 调用拦截器,应用在 RPC过程中。

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())
		},
	})
}