1 - iocli 工具

iocli 源码位于 github.com/alibaba/IOC-golang/iocli ,是一款命令行工具,提供了以下能力:

  • 代码调试

    开发者可以使用 iocli 作为调试客户端,调试基于 ioc-golang 框架开发的 go 应用程序。

  • 结构相关代码生成

    开发者可以为需要依赖注入的结构体增加注解,iocli 会识别这些注解,并产生符合要求的结构相关代码。包括结构描述信息、结构代理层、结构专属接口、结构 Get 方法等。

调试能力

ioc-golang 框架拥有基于 AOP 的 Go 运行时程序调试能力,帮助故障排查,性能分析,提高应用可观测能力。在 快速开始 部分展示了接口信息的查看、参数监听能力。在 基于 IOC-golang 的电商系统demo 中,可以展示基于 ioc-golang 的,业务无侵入的,方法粒度全链路追踪能力。

注解与代码生成

注解 是以特定字符串开头的注释,标注在期望注入的结构前。注解 只具备静态意义,即在代码生成阶段,被 iocl i工具扫描识别到,从而获取结构相关信息。注解 本身不具备程序运行时的意义。

iocli 可以识别以下注解 key,其中 = 后面的 value 为示例。

// +ioc:autowire=true
// +ioc:autowire:type=normal
// +ioc:autowire:paramLoader=paramLoader
// +ioc:autowire:paramType=Config
// +ioc:autowire:constructFunc=New
// +ioc:autowire:baseType=true
// +ioc:autowire:alias=MyAppAlias
// +ioc:tx:func=MyTransactionFunction
  • ioc:autowire (必填)

    bool 类型,为 true 则在代码生成阶段被识别到。

  • ioc:autowire:type (必填)

    string类型,表示依赖注入模型,目前支持以下五种,结构提供者可以选择五种中的一种或多种进行标注,从而生成相关的结构信息与 API,供结构使用者选用。

    // +ioc:autowire:type=singleton
    // +ioc:autowire:type=normal
    
    type MyStruct struct{
    
    }
    
    • singleton

      单例模型,使用该注入模型获取到的结构体,全局只存在一个对象。

    • normal

      多例模型,使用该注入模型,每一个标签注入字段、每一次 API 获取,都会产生一个新的对象。

    • config:

      配置模型是基于多例模型的封装扩展,基于配置模型定义的结构体方便从 yaml 配置文件中注入信息。参考例子 example/autowire/autowire_config

    • grpc:

      grpc 模型是基于单例模型的封装扩展,基于 grpc 模型可以方便地从 yaml 配置文件中读取参数,生成 grpc 客户端。参考例子 example/third_party/autowire/grpc

    • rpc:

      rpc 模型会在代码生成阶段产生 rpc 服务端注册代码,以及 rpc 客户端调用存根。参考例子 example/autowire/autowire_rpc

  • ioc:autowire:paramLoader(非必填)

    string类型,表示需要定制的“参数加载器“类型名

    参数加载器由结构定义者可选定制。可参考:extension/state/redis

    参数加载器需要实现Load方法:

    // ParamLoader is interface to load param
    type ParamLoader interface {
     Load(sd *StructDescriptor, fi *FieldInfo) (interface{}, error)
    }
    

    定义结构的开发者可以通过实现参数加载器,来定义自己的结构初始化参数。例如,一个 redis 客户端结构 ‘Impl’,需要从Config 参数来加载,如下所示 New 方法。

    type Config struct {
     Address  string
     Password string
     DB       string
    }
    
    func (c *Config) New(impl *Impl) (*Impl, error) {
     dbInt, err := strconv.Atoi(c.DB)
     if err != nil {
        return impl, err
     }
     client := redis.NewClient(&redis.Options{
        Addr:     c.Address,
        Password: c.Password,
        DB:       dbInt,
     })
     _, err = client.Ping().Result()
     if err != nil {
        return impl, err
     }
     impl.client = client
     return impl, nil
    }
    

    Config 包含的三个字段:Address Password DB,需要由使用者传入。

    从哪里传入?这就是参数加载器所做的事情。

    结构定义者可以定义如下加载器,从而将字段通过注入该结构的 tag 标签获取,如果tag信息标注了配置位置,则通过配置文件获取。

    type paramLoader struct {
    }
    
    func (p *paramLoader) Load(sd *autowire.StructDescriptor, fi *autowire.FieldInfo) (interface{}, error) {
     splitedTagValue := strings.Split(fi.TagValue, ",")
     param := &Config{}
     if len(splitedTagValue) == 1 {
        return nil, fmt.Errorf("file info %s doesn't contain param infomration, create param from sd paramLoader failed", fi)
     }
     if err := config.LoadConfigByPrefix("extension.normal.redis."+splitedTagValue[1], param); err != nil {
        return nil, err
     }
     return param, nil
    }
    

    例如

    type App struct {
     NormalDB3Redis normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/state/redis.Redis,address=127.0.0.1:6379&db=3"`
    }
    

    当然也可以从配置文件读入,tag中指定了key为 db1-redis

    type App struct {
     NormalDB3Redis normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/state/redis.Redis,db1-redis"`
    }
    

    ioc-go.yaml: autowire.normal.Redis.Impl.db1-redis.param 读入参数

    autowire:
      normal:
        github.com/alibaba/ioc-golang/extension/state/redis.Redis:
          db1-redis:
            param:
              address: localhost:6379
              db: 1
    

    我们提供了预置的参数加载器

    除非用户有强烈需求,我们更推荐用户直接使用我们预置的参数加载器:http://github.com/alibaba/ioc-golang/tree/master/autowire/param_loader。

    我们会先后尝试:标签重定向到配置、标签读入参数、配置文件的默认位置读入参数。每个注册到框架的结构都有唯一的ID,因此也会在配置文件中拥有配置参数的位置,这一默认位置在这里定义:http://github.com/alibaba/ioc-golang/blob/master/autowire/param_loader/default_config.go#L21,我们更希望和用户约定好这一点。

    当所有加载器都加载参数失败后,将会抛出错误。使用者应当查阅自己引入的结构加载器实现,并按照要求配置好参数。

  • ioc:autowire:paramType(非必填)

    string类型,表示依赖参数的类型名,在上述例子,该类型名为 Config

  • ioc:autowire:constructFunc(非必填)

    string类型,表示结构的构造方法名

    在给出 ioc:autowire:paramType 参数类型名的情况下,会使用参数的函数作为构造函数,例如在上述例子中,该构造方法为 Config 对象的 New 方法。

    如果没有给出 ioc:autowire:paramType 参数类型名,则会直接使用这一方法作为构造函数。

    我们要求该构造方法的函数签名是固定的,即:

    func (*$(结构名)) (*$(结构名), error)
    
  • ioc:autowire:baseType=true (非必填)

    该类型是否为基础类型

    go 基础类型不可直接通过&在构造时取地址,因此我们针对基础类型单独设计了该注解。在 配置扩展 中被使用较多。

  • ioc:autowire:alias=MyAppAlias (非必填)

    该类型的别名,可在标签、API获取、配置中,通过该别名替代掉较长的类型全名来指定结构。

  • ioc:tx:func=MyTransactionFunction(非必填)

    指定事务函数和回滚函数,参考事务例子 example/aop/transaction

iocli 操作命令

  • iocli init

    生成初始化工程

  • iocli gen

    递归遍历当前目录下的所有 go pkg ,根据注解生成结构体相关代码。

  • iocli list

    查看应用所有接口和方法信息,默认端口 :1999

  • iocli watch [structID] [methodName]

    监听一个方法的实时调用信息

  • iocli monitor [structID] [methodName]

    开启调用监控,structID 和 methodName 可不指定,则监控所有接口方法。

  • iocli trace [structID] [methodName]

    以当前方法为入口,开启调用链路追踪

具体操作参数可通过 -h 查看。

2 - 注入标签格式规范

IOC-golang 依赖 Go 原生支持的字段标签来作为依赖注入标签,标签的格式定义如下

拥有注入标签的结构,必须要在 ioc 框架的管理下才能正确注入,因此需要在结构体上方增加注解 +ioc:autowire ...=

// +ioc:autowire=true
// +ioc:autowire:type=singleton

type DemoStruct struct{
  ${大写字母开头的字段名} ${字段类型} `${自动装载模型}:"${结构ID},${结构key}或${参数字段1}=${参数值1}&..."`
}

${字段类型}

其中${字段类型} 可有如下选择

  • 接口

    • 结构专属接口

      结构专属接口已经包含了结构ID信息,其标签内 ${结构ID} 为空即可。

    • 开发者提供的接口


      由于结构专属接口与结构体实现处在同一 pkg 下,如果期望通过依赖注入进行接口-实现分离,则需要由开发者给出接口,同时必须给出期望注入的${结构ID} 。并保证程序在任何位置引入了实现类pkg,保证在程序启动时,期望注入的结构可被注册,从而被框架所管理,完成注入逻辑。


  • 结构体指针。

    结构体指针已经包含了结构ID信息,其标签内 ${结构ID} 为空即可。

${自动装载模型}

内置:autowire, normal, grpc, rpc-client, config

如开发者提供了 扩展的自动装载模型,也可以在这里指定。

${结构ID}

结构ID字符串,参考 结构ID

如注入字段的 ${字段类型} 为结构体指针或者 结构专属接口${结构ID} 为空即可。

${结构key}或${参数字段1}=${参数值1}&…

指定构造参数的值

  • 如果为多例模型,在此位置指定结构key,参考 默认配置格式与参数加载位置 中的例子
  • 如果在此位置给出 ${参数字段1}=${参数值1}&${参数字段2}=${参数值2} 则会以此处的参数值构造依赖对象并注入。参数值可以从配置文件中读取,也可以从环境变量中读取,一个复杂的例子如下:

参考 example/third_party/state/redis


// +ioc:autowire=true
// +ioc:autowire:type=singleton
// +ioc:autowire:paramType=Param
// +ioc:autowire:constructFunc=Init
// +ioc:autowire:alias=AppAlias

type App struct {
	NormalRedis    normalRedis.RedisIOCInterface `normal:""` // 配置文件默认位置加载参数
	NormalDB1Redis normalRedis.RedisIOCInterface `normal:",db1-redis"` // 配置文件当前结构的索引 db1-redis 位置加载参数
	NormalDB2Redis normalRedis.RedisIOCInterface `normal:",db2-redis"` // 配置文件当前结构的索引 db2-redis 位置加载参数
	NormalDB3Redis normalRedis.RedisIOCInterface `normal:",address=127.0.0.1:6379&db=3"` // 直接给出静态参数
	NormalDB4Redis normalRedis.RedisIOCInterface `normal:",address=${REDIS_ADDRESS_EXPAND}&db=5"`  // 从环境变量中读取参数
	NormalDB5Redis normalRedis.RedisIOCInterface `normal:",address=${autowire.normal.<github.com/alibaba/ioc-golang/extension/state/redis.Redis>.nested.address}&db=15"`  // 从配置文件的给定位置中读取参数

	privateClient *redis.Client
}

其配置文件如下:

autowire:
  normal:
    github.com/alibaba/ioc-golang/extension/state/redis.Redis:
      db1-redis: 
        param: # NormalDB1Redis 读取参数位置
          address: localhost:6379
          db: 1
      db2-redis: # NormalDB2Redis 读取参数位置
        param:
          address: localhost:6379
          db: 2
      param: # NormalRedis 读取参数位置
        address: localhost:6379
        db: 0
      expand:
        address: ${REDIS_ADDRESS_EXPAND}
        db: 15
      nested: # NormalDB5Redis 读取参数位置
        address: ${autowire.normal.<github.com/alibaba/ioc-golang/extension/state/redis.Redis>.expand.address}
        db: ${autowire.normal.<github.com/alibaba/ioc-golang/extension/state/redis.Redis>.expand.db}

3 - 配置文件格式规范

默认配置格式与参数加载位置

配置文件 ioc_golang.yaml 的默认参数默认配置格式为:

autowire:
  ${自动装载模型名}:
    ${SDID}:
      ${多例模式下的索引key1}:
        param:
          field1: value1 # 多例模式创建对象的构造参数
      ${多例模式下的索引key2}:
        param:
          field1: value2 # 多例模式创建对象的构造参数
      param:
        field1: value3 # 单例模式或多例模式未指定索引key 创建对象的构造参数

例如,一个基于多例模型的结构 Impl,实现了 Redis 接口,其多个实例参数可以配置为:

autowire:
  normal:
    github.com/alibaba/ioc-golang/extension/normal/redis.Impl:
      db1-redis:
        param:
          address: localhost:6379
          db: 1
      db2-redis:
        param:
          address: localhost:6379
          db: 2
      param:
        address: localhost:6379
        db: 0

拥有 注入标签 normal:",db2-redis" 的字段,会从配置中读入参数,位置为标签指向的对象名db2-redis,db 为2。

NormalDB2Redis normalRedis.RedisIOCInterface `normal:",db2-redis"`

address: localhost:6379
db: 2

拥有标签 normal:"" 的字段,会从配置中读入参数,db 为 0。

NormalDBRedis normalRedis.RedisIOCInterface `normal:""`

address: localhost:6379
db: 0

定制化参数加载器

参数加载器(param_loader) 可以在自动装载模型或 SD (结构描述符) 中定制化,例如一个基于 grpc 自动装载模型注入的客户端,我们已经在 grpc 自动装载模型中 定制化 了参数加载位置,为 autowire.grpc.${tag-value} ,因此对于如下字段:

HelloServiceClient api.HelloServiceClient `grpc:"hello-service"`

会从如下位置读取参数:address 设置为 “localhost:8080”

autowire:
  grpc:
    hello-service:
      address: localhost:8080

同理,也可以由服务提供者在 SD 中传入参数加载器,进行定制化参数加载源。

配置文件中的特殊引用

声明环境变量

在配置文件中,可以使用 ${ENV_KEY} 的格式,引入环境变量。当环境变量存在时,将会被替换为环境变量的值。

config:
  app:
    config-value: myValue
    config-value-from-env: ${MY_CONFIG_ENV_KEY} # 如环境变量存在,读取环境变量替换该字段

声明配置引用(nested)

config:
  app:
    config-value: myValue
    config-value-from-env: ${MY_CONFIG_ENV_KEY}  # 如环境变量存在,读取环境变量替换该字段
    nested-config-value: ${config.app.config-value} # 引用 config.app.config-value 的值,为 myValue
    nested-config-value-from-env: ${config.app.config-value-from-env} # 引用 config.app.config-value-from-env 的值,为环境变量替换后的值

更多配置文件的加载与使用例子,参考 example/config_file

4 - 常见问题排查

默认配置层级

配置文件 ioc_golang.yaml 的默认参数默认配置层级依此为:

autowire:
  $(自动装载模型名):
    $(SDID):
      $(多例模式下的对象名):
        param:
      param:

例如,一个基于多例模型的结构 Impl,实现了 Redis 接口,其多个实例参数可以配置为:

autowire:
  normal:
    github.com/alibaba/ioc-golang/extension/normal/redis.Impl:
        db1-redis:
          param:
            address: localhost:6379
            db: 1
        db2-redis:
          param:
            address: localhost:6379
            db: 2
        param:
          address: localhost:6379
          db: 0

拥有标签 normal:"Impl,db2-redis" 的字段,会从配置中读入参数,位置为标签指向的对象名db2-redis

NormalDB2Redis normalRedis.Redis `normal:"Impl,db2-redis"`

address: localhost:6379
db: 2

拥有标签 normal:"Impl" 的字段,会从配置中读入参数。

NormalDBRedis normalRedis.Redis `normal:"Impl"`
address: localhost:6379
db: 0

定制化参数加载模型

参数加载模型可以由自动装载模型和 SD (结构描述符)定制化,例如一个基于 grpc 自动装载模型注入的客户端

HelloServiceClient api.HelloServiceClient `grpc:"hello-service"`

从如下位置读取参数:

autowire:
  grpc:
    hello-service:
      address: localhost:8080