1 - 总体介绍

Alibaba/IOC-golang 是一款 Go 语言依赖注入框架,提供了一套完善的 IoC 容器。

适配于任何使用 Go 语言开发的场景,帮助开发者管理依赖对象,降低代码耦合度。在运维场景下,可以通过扩展框架自动注入的代理 AOP 层实现各种运维能力。

1.1 - 功能

IOC-golang 框架提供的能力
  • 依赖注入

    支持任何结构、接口的依赖注入,具备完善的对象生命周期管理机制。

    可以接管对象的创建、参数注入、工厂方法。可定制化对象参数来源。

  • 结构代理层

    基于 AOP 的思路,为由框架接管的对象提供默认的结构代理层,在面向接口编程的情景下,可以使用基于结构代理 AOP 层扩展的丰富运维能力。例如接口查询,参数动态监听,方法粒度链路追踪,性能瓶颈分析,分布式场景下全链路方法粒度追踪等。

  • 代码生成能力

    我们提供了代码生成工具,开发者可以通过注解的方式标注结构,从而便捷地生成结构注册代码、结构代理、结构专属接口等。

  • 可扩展能力

    支持被注入结构的扩展、自动装载模型的扩展、调试 AOP 层的扩展。

  • 丰富的预置组件

    提供覆盖主流中间件的预制对象,方便直接注入使用。

1.2 - 设计理念

IOC-golang 框架设计理念
  • 通过标签与配置文件,实现依赖注入和参数读取

  • 通过注解,实现注册代码自动生成

  • 通过注入接口代理,实现 AOP 可扩展调试层

2 - 概念

框架基础概念中英文对照

中文英文描述
结构StructGo 语言的 struct 类型结构体
对象ObjectGo 语言中的实例化对象
接口InterfaceGo 语言中的 interface 类型接口
方法MethodGo 语言中的接口、对象大写字母开头的函数方法
依赖注入Dependency Injection(DI)
自动装载Auto Wire
结构描述符Struct Descriptor (SD)
结构描述键Struct Descriptor Identification(SDID)自动装载模型内索引结构的唯一ID。

2.1 - 依赖注入

概念

依赖注入(Dependency Injection)是指开发人员无需显式地创建对象,而是通过标签的方式声明字段,由框架负责将实例化对象写入该字段。

优点

依赖注入可以降低代码的耦合度,减少冗余代码量,优化代码逻辑。帮助开发人员面向对象编程,面向接口编程。

IOC-golang 的依赖注入能力

本框架从两个角度实现依赖注入:

  • 结构如何被提供

    开发人员需要准备好需要被注入的结构,将它注册到框架。注册到框架的代码可以由开发人员手动编写,开发人员也可以通过使用注解标记结构,使用 iocli 的代码生成能力自动生成注册代码,从而减少工作量。

    一个带有注解的结构

    // +ioc:autowire=true
    // +ioc:autowire:type=singleton
    
    type App struct {}
    

    由 iocli 工具生成,或用户手动编写的注册代码。

    func init() {
    	singleton.RegisterStructDescriptor(&autowire.StructDescriptor{
    		Interface: &App{},
    		Factory: func() interface{} {
    			return &App{}
    		},
    	})
    }
    
  • 结构如何被使用

    • 通过标签注入

      开发人员需要通过标签来标记需要注入的结构字段。

      需要通过标签注入依赖的结构,其本身必须也注册在框架上。

      // +ioc:autowire=true
      // +ioc:autowire:type=singleton
      
      type App struct {
      	MySubService Service `singleton:"main.ServiceImpl1"` 
      }
      
    • 通过 API 可获取对象,入参为 结构描述 ID 和构造参数(如需要)。

      appInterface, err := singleton.GetImpl("main.App")
      if err != nil {
        panic(err)
      }
      app := appInterface.(*App)
      
      redisInterface, err := normal.GetImpl("github.com/alibaba/ioc-golang/extension/normal/redis.Impl", &Config{
          Address: "localhost:6379"
      })
      if err != nil {
        panic(err)
      }
      redisClient := redisInterface.(*Impl)
      

2.2 - 自动装载模型

概念

自动装载(Autowire)模型封装了一类对象的装载方式,是本框架对装载过程抽象出的概念和接口。

用户需要定义的结构千变万化,如果每个结构描述符都要提供完整的装载过程信息,将是一件很麻烦的事情。我们可以将一类相似的结构抽象出一个自动装载模型,选择注册在该模型的所有对象都需要遵循该模型的加载策略,这大大降低了用户提供的结构描述符数据量,提高开发效率。

框架内置了两个基础自动装载模型:单例模型(singleton),多例模型(normal)

框架提供了两个扩展的自动装载模型:配置(config),gRPC 客户端(grpc)。其中配置模型是多例模型的扩展,gRPC 客户端是单例模型的扩展。框架内置了基于“配置自动装载模型”的多个结构。例如,用户可以用几行代码将 “gRPC 客户端存根”注册在 “grpc 装载模型” 之上【示例】,方便地从配置文件中的指定位置读入下游主机名、从标签读入客户端名称,注入客户端单例指针。

2.3 - 结构描述符

概念

结构描述符(Struct Descriptor, SD)用于描述一个被开发者定义的结构,包含对象生命周期的全部信息,例如结构类型是什么,实现了哪些接口,如何被构造等等。

SD可以通过注解的方式使用工具自动生成。但还是推荐开发人员了解本框架定义的结构生命周期和结构描述信息,以便更清晰地开发应用。

对象生命周期

开发人员在 Go 语言开发过程中需要时时关注对象的生命周期,一个常见的对象生命周期如下:

  1. 对象定义:开发人员编码,编写结构,实现接口,确定模型(单例、多例..) 。
  2. 加载全部依赖:依赖的下游对象创建、配置读入等。
  3. 对象创建:产生一个基于该对象的指针
  4. 对象构造:获取全部依赖,并将其组装到空对象中,产生可用对象。
  5. 对象使用:调用对象的方法,读写对象字段。
  6. 对象销毁:销毁对象,销毁无需再使用的依赖对象。

参数

本框架的“参数”概念,是一个结构体,该结构体包含了创建一个对象所需全部依赖,并提供了构造方法。

例如

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)

本框架定义的结构描述符如下:

type StructDescriptor struct {
	Factory       func() interface{} 
	ParamFactory  func() interface{}
	ParamLoader   ParamLoader
	ConstructFunc func(impl interface{}, param interface{}) (interface{}, error)
	DestroyFunc   func(impl interface{})

	autowireType    string
}
  • 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 【非必要】

    定义了对象的销毁过程,入参为对象指针。

  • autowireType 【必要】

    定义了对象的自动装载模型,例如单例模型、多例模型等,详情参阅 自动装载模型概念

结构描述ID

  • 定义

结构描述ID定义为:"$(包名).$(结构名)"

结构描述 ID (Struct Description Identification) 在本文档和项目中多处被缩写为 SDID。

SDID 是唯一的,用于索引结构的键,类型为字符串。

  • 使用

开发人员在使用 API 的方式从自动装载模型获取对象时,需要传入SDID来获取。

2.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
    

2.5 - 注解

概念

注解(annotation) 是 Go 代码中符合特定格式的注释,一般位于结构定义之前。可被命令行工具扫描,从而获取结构信息,自动生成需要的代码。 例如快速开始示例中的

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

type App struct {
	...
}

注解在 Go 应用开发的过程中,是一种直观、清晰的结构描述方式,通过使用注解进行标注,使用工具自动生成相关代码,可减少开发人员的工作量,降低代码重复率。

注解与代码生成

注解与代码生成能力,是为了让开发者无需关心SD(结构描述符)的组装和注册。开发者只需定义好结构,正确标注注解,即可使用 iocli 工具自动生成当前目录和子目录下的 SD 。

iocli 工具支持的注解

详情参阅 iocli #结构注解

3 - 快速开始

3.1 - 环境安装

安装 Go 环境

Go 下载地址

建议 go version >= 1.17

将 $GOPATH/bin 加入环境变量

安装 iocli 代码生成工具至 $GOPATH/bin

go install github.com/alibaba/ioc-golang/iocli@latest

3.2 - 实现依赖注入demo

使用 ioc-golang 实现依赖注入demo

教程

demogif

依赖注入和调试教程演示

我们将开发一个具有以下拓扑的工程,在本例子中,可以展示

  1. 注册代码生成
  2. 接口注入
  3. 对象指针注入
  4. API 获取对象
  5. 调试能力,查看运行中的接口、方法;以及实时监听参数值、返回值。

ioc-golang-quickstart-structure

用户所需编写的全部代码:main.go

package main

import (
	"fmt"
	"time"

	"github.com/alibaba/ioc-golang"
)

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

type App struct {
	// 将封装了代理层的 main.ServiceImpl1 指针注入到 Service 接口,单例模型,需要在标签中指定被注入结构
	ServiceImpl1 Service `singleton:"main.ServiceImpl1"`

	// 将封装了代理层的 main.ServiceImpl2 指针注入到 Service 接口,单例模型,需要在标签中指定被注入结构
	ServiceImpl2 Service `singleton:"main.ServiceImpl2"`

	// 将封装了代理层的 main.ServiceImpl1 指针注入到他的专属接口 'ServiceImpl1IOCInterface'
  // 注入专属接口的命名规则是 '${结构名}IOCInterface',注入专属接口无需指定被注入结构,标签值为空即可。
	Service1OwnInterface ServiceImpl1IOCInterface `singleton:""`

	// 将结构体指针注入当前字段
	ServiceStruct *ServiceStruct `singleton:""`
}

func (a *App) Run() {
	for {
		time.Sleep(time.Second * 3)
		fmt.Println(a.ServiceImpl1.GetHelloString("laurence"))
		fmt.Println(a.ServiceImpl2.GetHelloString("laurence"))

		fmt.Println(a.Service1OwnInterface.GetHelloString("laurence"))
		
		fmt.Println(a.ServiceStruct.GetString("laurence"))
	}
}

type Service interface {
	GetHelloString(string) string
}

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

type ServiceImpl1 struct {
}

func (s *ServiceImpl1) GetHelloString(name string) string {
	return fmt.Sprintf("This is ServiceImpl1, hello %s", name)
}

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

type ServiceImpl2 struct {
}

func (s *ServiceImpl2) GetHelloString(name string) string {
	return fmt.Sprintf("This is ServiceImpl2, hello %s", name)
}

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

type ServiceStruct struct {
}

func (s *ServiceStruct) GetString(name string) string {
	return fmt.Sprintf("This is ServiceStruct, hello %s", name)
}

func main() {
	// start
	if err := ioc.Load(); err != nil {
		panic(err)
	}

	// app, err := GetAppIOCInterface 也可以,获取到的是封装了代理层的接口,如下获取到的是未封装的结构体指针。
	app, err := GetApp()
	if err != nil {
		panic(err)
	}
	app.Run()
}

上述所说的“代理层”,是框架为“以接口形式注入/获取”的结构体,默认封装的代理,可以扩展一系列运维操作。我们推荐开发者在编写代码的过程中基于接口编程,则所有对象都可拥有运维能力。

编写完毕后,当前目录执行以下命令,初始化 go mod ,拉取最新代码,生成结构注册代码。(mac 环境可能因权限原因需要sudo):

% go mod init ioc-golang-demo
% export GOPROXY="https://goproxy.cn"
% go mod tidy
% go get github.com/alibaba/ioc-golang@master
% sudo iocli gen

会在当前目录生成:zz_generated.ioc.go,开发者无需关心这一文件,这一文件中就包含了上面使用的 GetApp 方法

//go:build !ignore_autogenerated
// +build !ignore_autogenerated

// Code generated by iocli

package main

import (
        autowire "github.com/alibaba/ioc-golang/autowire"
        normal "github.com/alibaba/ioc-golang/autowire/normal"
        "github.com/alibaba/ioc-golang/autowire/singleton"
        util "github.com/alibaba/ioc-golang/autowire/util"
)

func init() {
        normal.RegisterStructDescriptor(&autowire.StructDescriptor{
                Factory: func() interface{} {
                        return &app_{}
                },
        })
        singleton.RegisterStructDescriptor(&autowire.StructDescriptor{
                Factory: func() interface{} {
                        return &App{}
                },
        })
  ...
func GetServiceStructIOCInterface() (ServiceStructIOCInterface, error) {
        i, err := singleton.GetImplWithProxy(util.GetSDIDByStructPtr(new(ServiceStruct)), nil)
        if err != nil {
                return nil, err
        }
        impl := i.(ServiceStructIOCInterface)
        return impl, nil
}

查看当前目录文件

% tree
.
├── go.mod
├── go.sum
├── main.go
└── zz_generated.ioc.go

0 directories, 4 files

执行程序

go run .

控制台打印输出:

  ___    ___     ____                           _                         
 |_ _|  / _ \   / ___|           __ _    ___   | |   __ _   _ __     __ _ 
  | |  | | | | | |      _____   / _` |  / _ \  | |  / _` | | '_ \   / _` |
  | |  | |_| | | |___  |_____| | (_| | | (_) | | | | (_| | | | | | | (_| |
 |___|  \___/   \____|          \__, |  \___/  |_|  \__,_| |_| |_|  \__, |
                                |___/                               |___/ 
Welcome to use ioc-golang!
[Boot] Start to load ioc-golang config
[Config] Load default config file from ../conf/ioc_golang.yaml
[Config] Load ioc-golang config file failed. open /Users/laurence/Desktop/workplace/alibaba/conf/ioc_golang.yaml: no such file or directory
 The load procedure is continue
[Boot] Start to load debug
[Debug] Debug port is set to default :1999
[Boot] Start to load autowire
[Autowire Type] Found registered autowire type normal
[Autowire Struct Descriptor] Found type normal registered SD main.serviceStruct_
[Autowire Struct Descriptor] Found type normal registered SD main.app_
[Autowire Struct Descriptor] Found type normal registered SD main.serviceImpl1_
[Autowire Struct Descriptor] Found type normal registered SD main.serviceImpl2_
[Autowire Type] Found registered autowire type singleton
[Autowire Struct Descriptor] Found type singleton registered SD main.App
[Autowire Struct Descriptor] Found type singleton registered SD main.ServiceImpl1
[Autowire Struct Descriptor] Found type singleton registered SD main.ServiceImpl2
[Autowire Struct Descriptor] Found type singleton registered SD main.ServiceStruct
[Debug] Debug server listening at :1999
This is ServiceImpl1, hello laurence
This is ServiceImpl2, hello laurence
This is ServiceImpl1, hello laurence
This is ServiceStruct, hello laurence
...

可看到,依赖注入成功,程序正常运行。

调试程序

可看到打印出的日志中包含,说明 Debug 服务已经启动。

[Debug] Debug server listening at :1999

新开一个终端,使用 iocli 的调试功能,查看所有拥有代理层的结构和方法。默认端口为 1999。

% iocli list
main.ServiceImpl1
[GetHelloString]

main.ServiceImpl2
[GetHelloString]

监听方法的参数和返回值。以监听 main.ServiceImpl 结构的 GetHelloString 方法为例,每隔三秒钟,函数被调用两次,打印参数和返回值。

% iocli watch main.ServiceImpl1 GetHelloString
========== On Call ==========
main.ServiceImpl1.GetHelloString()
Param 1: (string) (len=8) "laurence"

========== On Response ==========
main.ServiceImpl1.GetHelloString()
Response 1: (string) (len=36) "This is ServiceImpl1, hello laurence"

========== On Call ==========
main.ServiceImpl1.GetHelloString()
Param 1: (string) (len=8) "laurence"

========== On Response ==========
main.ServiceImpl1.GetHelloString()
Response 1: (string) (len=36) "This is ServiceImpl1, hello laurence"
,,,

注解分析

// +ioc:autowire=true
代码生成工具会识别到标有 +ioc:autowire=true 注解的对象

// +ioc:autowire:type=singleton
标记注入模型为 singleton 单例模型还有 normal 多例模型等扩展

更多

您可以参阅 示例 查阅更多例子和框架高级用法

4 - 示例

最新示例代码,位于 ioc-golang/example

4.1 - redis 客户端注入

简介

本示例展示了注入 Redis 客户端能力。

在应用开发过程中,通过 SDK 操作 Redis 是一个常见的诉求。

ioc-golang 框架提供了注入 Redis 连接的能力,开发者可以在配置文件中指定好 Redis 地址、密码、db名等信息,通过标签注入连接,无需手动创建、组装。

注入模型与结构

多例(normal)依赖注入模型

预定义的 redis 结构

关键代码

import(
	normalMysql "github.com/alibaba/ioc-golang/extension/normal/mysql"
)

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

type App struct {
	NormalRedis    normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl"`
	NormalDB1Redis normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,db1-redis"`
	NormalDB2Redis normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,db2-redis"`
	NormalDB3Redis normalRedis.Redis `normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,address=127.0.0.1:6379&db=3"`
}
  • 被注入字段类型

    normalRedis.Redis 接口

  • 标签

    开发人员可以为 normalRedis.Redis 类型的字段增加 normal:"Impl,$(configKey),$(tableName)" 标签。从而注入Redis sdk。

    例子中的 normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl" 的意义为,将配置文件内 autowire.normal.<github.com/alibaba/ioc-golang/extension/normal/redis.Impl>.param定义的值作为参数。

    例子中的 normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,db1-redis" 的意义为,将配置文件内 autowire.normal.<github.com/alibaba/ioc-golang/extension/normal/redis.Impl>.db1-redis.param定义的值作为参数。

    例子中的 normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,db2-redis"的意义为,将配置文件内 autowire.normal.<github.com/alibaba/ioc-golang/extension/normal/redis.Impl>.db2-redis.param定义的值作为参数。

    例子中的 normal:"github.com/alibaba/ioc-golang/extension/normal/redis.Impl,address=127.0.0.1:6379&db=3" 的意义为,使用标签内定义的 key-value 作为参数配置。

    默认参数加载策略详情请参阅参数加载器

    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
    

运行示例

例子会注入多个位于127.0.0.1:6379 的Redis 客户端,数据库id分别为 0、1、2、3.

需要确保您本地运行了redis,可通过 docker run -p6379:6379 redis:latest 快速启动一个。

注入后,方可调用该接口提供的方法。可获取裸 Redis 连接,也可以直接使用封装好的 API 操作Redis。

cd example/autowire_redis_client/cmd
go run .
  ___    ___     ____            ____           _                         
 |_ _|  / _ \   / ___|          / ___|   ___   | |   __ _   _ __     __ _ 
  | |  | | | | | |      _____  | |  _   / _ \  | |  / _` | | '_ \   / _` |
  | |  | |_| | | |___  |_____| | |_| | | (_) | | | | (_| | | | | | | (_| |
 |___|  \___/   \____|          \____|  \___/  |_|  \__,_| |_| |_|  \__, |
                                                                    |___/ 
Welcome to use ioc-golang!
[Boot] Start to load ioc-golang config
[Config] Load config file from ../conf/ioc_golang.yaml
[Boot] Start to load debug
[Debug] Debug mod is not enabled
[Boot] Start to load autowire
[Autowire Type] Found registered autowire type singleton
[Autowire Struct Descriptor] Found type singleton registered SD main.App
[Autowire Type] Found registered autowire type normal
[Autowire Struct Descriptor] Found type normal registered SD github.com/alibaba/ioc-golang/extension/normal/redis.Impl
client0 get  db0
client1 get  db1
client2 get  db2
client3 get  db3

可看到打印出了已经写入四个 redis db 的值。

4.2 - GORM 客户端注入

简介

本示例展示了注入基于 GORM 的数据库客户端能力。

在应用开发过程中,通过 SDK 操作数据库是一个常见的诉求,GORM 是应用较广泛的 Go 数据库 sdk。

ioc-golang 框架提供了注入数据库连接的能力,开发者可以在配置文件中指定好数据库地址、密码信息,通过标签注入连接,无需手动创建、组装 GORM 客户端。

注入模型与结构

多例(normal)依赖注入模型

预定义的 mysql 结构

关键代码

import(
	normalMysql "github.com/alibaba/ioc-golang/extension/normal/mysql"
)

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

type App struct {
	MyDataTable normalMysql.Mysql `normal:"github.com/alibaba/ioc-golang/extension/normal.Impl,my-mysql,mydata"`
}
  • 被注入字段类型

    normalMysql.Mysql 接口

  • 标签

    开发人员可以为 normalMysql.Mysql 类型的字段增加 normal:"github.com/alibaba/ioc-golang/extension/normal.Impl,$(configKey),$(tableName)" 标签。从而注入指定数据库的指定表 sdk。

    例子中的 normal:"github.com/alibaba/ioc-golang/extension/normal.Impl,my-mysql,mydata" 的意义为,将配置文件内 autowire.normal.<github.com/alibaba/ioc-golang/extension/normal/mysql.Impl>.my-mysql.param定义的值作为参数。

    autowire:
      normal:
        github.com/alibaba/ioc-golang/extension/normal/mysql.Impl:
          my-mysql:
            param:
              host: "127.0.0.1"
              port: 3306
              username: "root"
              password: "root"
              dbname: "test"
    

    例子会建立一个位于127.0.0.1:3306 的数据库连接,用户名为root、密码为 root、数据库名为test、表名为mydata。

    注入后,方可调用该接口提供的方法,获取裸 gorm 连接,或者直接使用封装好的 API 操作数据表。

    type Mysql interface {
    	GetDB() *gorm.DB
    	SelectWhere(queryStr string, result interface{}, args ...interface{}) error
    	Insert(toInsertLines UserDefinedModel) error
    	Delete(toDeleteTarget UserDefinedModel) error
    	First(queryStr string, findTarget UserDefinedModel, args ...interface{}) error
    	Update(queryStr, field string, target interface{}, args ...interface{}) error
    }
    

4.3 - gRPC 客户端注入

简介

本示例展示了基于 ioc-golang 框架的 gRPC 客户端注入能力。

在进行微服务开发过程中,服务间通信尤为重要,gRPC 是被应用最为广泛的 RPC 框架之一。

在常规开发中,开发者需要从配置中手动读取下游主机名,启动 grpc 客户端。针对一个接口的网络客户端往往是单例模型,如果多个服务都需要使用同一客户端,则还需要开发者维护这个单例模型。

基于 ioc-golang 框架的 gRPC 客户端注入能力,我们可以将客户端的生命周期交付给框架管理,并赋予客户端调试能力,开发者只需要关注注册和使用。

示例介绍

本示例实现了以下拓扑

debug

在这个例子中,App 结构会依此调用所有依赖对象,进而调用一个单例模型注入的 gRPC 客户端,该客户端发起网络请求,并获得结果。

依赖注入模型

grpc 依赖注入模型

关键代码

import(
  "github.com/alibaba/ioc-golang/extension/autowire/grpc"
	googleGRPC "google.golang.org/grpc"
)
func init() {
	// register grpc client
	grpc.RegisterStructDescriptor(&autowire.StructDescriptor{
		Interface: new(api.HelloServiceClient),
		Factory: func() interface{} {
			return new(api.HelloServiceClient)
		},
		ParamFactory: func() interface{} {
			return &googleGRPC.ClientConn{}
		},
		ConstructFunc: func(impl interface{}, param interface{}) (interface{}, error) {
			conn := param.(*googleGRPC.ClientConn)
			fmt.Println("create conn target ", conn.Target())
			return api.NewHelloServiceClient(conn), nil
		},
	})
}

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

type App struct {
	HelloServiceClient api.HelloServiceClient `grpc:"hello-service"`
}

需要在代码中手动注册 gRPC 客户端。在需要使用的地方,增加 grpc:"xxx" 标签

框架会默认从 autowire.grpc.xxx 读取参数, 在例子中,为autowire.grpc.hello-service

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

运行示例

  1. 启动 grpc Server

    % cd example/denbug/grpc_server
    % go run .
    
  2. 新开一个终端,启动客户端。

    % cd example/autowire_grpc_client/cmd
    % go run .
      ___    ___     ____            ____           _                         
     |_ _|  / _ \   / ___|          / ___|   ___   | |   __ _   _ __     __ _ 
      | |  | | | | | |      _____  | |  _   / _ \  | |  / _` | | '_ \   / _` |
      | |  | |_| | | |___  |_____| | |_| | | (_) | | | | (_| | | | | | | (_| |
     |___|  \___/   \____|          \____|  \___/  |_|  \__,_| |_| |_|  \__, |
                                                                        |___/ 
    Welcome to use ioc-golang!
    [Boot] Start to load ioc-golang config
    [Config] Load config file from ../conf/ioc_golang.yaml
    [Boot] Start to load debug
    [Debug] Debug mod is not enabled
    [Boot] Start to load autowire
    [Autowire Type] Found registered autowire type grpc
    [Autowire Struct Descriptor] Found type grpc registered SD github.com/alibaba/ioc-golang/example/autowire_grpc_client/api.HelloServiceClient
    [Autowire Type] Found registered autowire type singleton
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/autowire_grpc_client/cmd/struct1.Struct1
    [Autowire Struct Descriptor] Found type singleton registered SD main.App
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/autowire_grpc_client/cmd/service1.Impl1
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/autowire_grpc_client/cmd/service2.Impl1
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/autowire_grpc_client/cmd/service2.Impl2
    create conn target  localhost:8080
    App call grpc get: Hello laurence
    ExampleService1Impl1 call grpc get :Hello laurence
    ExampleService2Impl1 call grpc get :Hello laurence_service2_impl1
    ExampleService2Impl2 call grpc get :Hello laurence_service2_impl2
    ExampleStruct1 call grpc get :Hello laurence_service1_impl1
    

小节

gRPC 客户端注入能力,所代表的是 ioc-golang 框架具备网络模型注入的能力。

针对特定网络框架,可以在其服务提供者接口处,提供客户端注入代码,从而便于客户端引入后直接注入。

4.4 - 配置注入

简介

本示例展示了从配置文件注入字段的能力。

在应用开发过程中,从配置文件中读入配置是一个常见的诉求。例如读取数据库的账号密码、下游服务的主机名,以及一些业务配置等。

ioc-golang 框架提供了便捷的基于文件注入配置的能力,使开发者无需手动解析配置文件,无需手动组装对象。

注入模型与结构

config 依赖注入模型

关键代码:

import (
  github.com/alibaba/ioc-golang/extension/config
)

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

type App struct {
	DemoConfigString *config.ConfigString `config:",autowire.config.demo-config.string-value"`
	DemoConfigInt    *config.ConfigInt    `config:",autowire.config.demo-config.int-value"`
	DemoConfigMap    *config.ConfigMap    `config:",autowire.config.demo-config.map-value"`
	DemoConfigSlice  *config.ConfigSlice  `config:",autowire.config.demo-config.slice-value"`
}
  • 被注入字段类型

    目前支持 ConfigString,ConfigInt,ConfigMap,ConfigSlice 四种类型。

    需要以 指针 的形式声明字段类型

  • 标签与注入位置

    开发人员可以给结构增加 ``config:",xxx" 标签, 标注需要注入的值类型,以及该字段位于配置文件的位置。

    例子中的

    config:",autowire.config.demo-config.string-value"

    的意义为,将配置文件内 autowire.config.demo-config.string-value 的值注入到该字段。

    对应配置文件:ioc_golang.yaml 中的字符串 “stringValue”

    autowire:
      config:
        demo-config:
          int-value: 123
          int64-value: 130117537261158665
          float64-value: 0.001
          string-value: stringValue
          map-value:
            key1: value1
            key2: value2
            key3: value3
            obj:
              objkey1: objvalue1
              objkey2: objvalue2
              objkeyslice: objslicevalue
          slice-value:
            - sliceValue1
            - sliceValue2
            - sliceValue3
            - sliceValue4
    

运行示例

cd example/autowire_config/cmd
go run .
  ___    ___     ____            ____           _                         
 |_ _|  / _ \   / ___|          / ___|   ___   | |   __ _   _ __     __ _ 
  | |  | | | | | |      _____  | |  _   / _ \  | |  / _` | | '_ \   / _` |
  | |  | |_| | | |___  |_____| | |_| | | (_) | | | | (_| | | | | | | (_| |
 |___|  \___/   \____|          \____|  \___/  |_|  \__,_| |_| |_|  \__, |
                                                                    |___/ 
Welcome to use ioc-golang!
[Boot] Start to load ioc-golang config
[Config] merge config map, depth: [0]
[Boot] Start to load debug
[Debug] Debug mod is not enabled
[Boot] Start to load autowire
[Autowire Type] Found registered autowire type singleton
[Autowire Struct Descriptor] Found type singleton registered SD main.App
[Autowire Type] Found registered autowire type normal
[Autowire Type] Found registered autowire type config
[Autowire Struct Descriptor] Found type config registered SD github.com/alibaba/ioc-golang/extension/config.ConfigInt64
[Autowire Struct Descriptor] Found type config registered SD github.com/alibaba/ioc-golang/extension/config.ConfigInt
[Autowire Struct Descriptor] Found type config registered SD github.com/alibaba/ioc-golang/extension/config.ConfigMap
[Autowire Struct Descriptor] Found type config registered SD github.com/alibaba/ioc-golang/extension/config.ConfigSlice
[Autowire Struct Descriptor] Found type config registered SD github.com/alibaba/ioc-golang/extension/config.ConfigString
[Autowire Struct Descriptor] Found type config registered SD github.com/alibaba/ioc-golang/extension/config.ConfigFloat64
2022/06/06 18:01:22 load config path autowire.config#demo-config#float64-value error =  property [autowire config#demo-config#float64-value]'s key config#demo-config#float64-value not found
stringValue
123
map[key1:value1 key2:value2 key3:value3 obj:map[objkey1:objvalue1 objkey2:objvalue2 objkeyslice:objslicevalue]]
[sliceValue1 sliceValue2 sliceValue3 sliceValue4]
130117537261158665
0
stringValue
123
map[key1:value1 key2:value2 key3:value3 obj:map[objkey1:objvalue1 objkey2:objvalue2 objkeyslice:objslicevalue]]
[sliceValue1 sliceValue2 sliceValue3 sliceValue4]
130117537261158665
0

可看到依次打印出了不同结构的注入配置。

4.5 - 通过 API 获取对象

简介

本示例展示了通过 API 调用的方式获取对象的能力。

在应用开发过程中,部分依赖通过注入的方式预置在字段中,也有一部分依赖是在程序运行过程中动态生成的。常规做法是通过手动拼装结构的方式,或者通过调用构造函数的方式获取对象。

ioc-golang 框架提供了中心化的获取对象 API。

  • 该 API 推荐被结构提供者封装,从而提供一个具象的 API 供用户调用。该 API 传入所需配置结构,返回具体的接口。

extension/normal/redis/redis.go

func GetRedis(config *Config) (Redis, error) {
	mysqlImpl, err := normal.GetImpl(SDID, config)
	if err != nil {
		return nil, err
	}
	return mysqlImpl.(Redis), nil
}
  • 如果结构提供者并没有提供上述 API,用户同样也可以直接调用,传入参数并获取对象。

对象获取 API

  • 多例(normal)

    autowire/normal/normal.go

    func GetImpl(sdID string, param interface{}) (interface{}, error) {}
    

    每次调用多例获取 API,将创建一个新对象。

  • 单例(singleton)

    autowire/singleton/singleton.go

    func GetImpl(sdID string) (interface{}, error) {}
    

    单例模型全局只拥有一个对象,通过 API 只能获取而不能创建,在框架启动时所有单例模型指针将基于配置/标签创建好。

关键代码

import(
	"github.com/alibaba/ioc-golang/autowire/normal"
	"github.com/alibaba/ioc-golang/autowire/singleton"
	"github.com/alibaba/ioc-golang/extension/normal/redis"
)


func (a *App) Run() {
  // 通过 normal 提供的全局 API,传递结构描述 ID 和配置结构,创建多例对象
	normalRedis, err := normal.GetImpl("github.com/alibaba/ioc-golang/extension/normal/redis.Impl", &redis.Config{
		Address: "localhost:6379",
		DB:      "0",
	})
  // 通过 redis 结构提供者定义好的 GetRedis 方法,传递配置结构,创建多例对象
	normalRedis2, err := redis.GetRedis(&redis.Config{
		Address: "localhost:6379",
		DB:      "0",
	})
  ...
}

func main() {
	if err := ioc.Load(); err != nil {
		panic(err)
	}
  // 通过 singleton 提供的全局 API,传递结构描述 ID 获取单例对象
	appInterface, err := singleton.GetImpl("main.App")
	if err != nil {
		panic(err)
	}
	app := appInterface.(*App)

	app.Run()
}

运行示例

需要确保您本地运行了redis,可通过 docker run -p6379:6379 redis:latest 快速启动一个。

例子会通过 API 获取 App 对象和 redis 对象,并调用 redis 对象提供的方法。

 cd example/get_impl_by_api/cmd
 go run .
  ___    ___     ____            ____           _                         
 |_ _|  / _ \   / ___|          / ___|   ___   | |   __ _   _ __     __ _ 
  | |  | | | | | |      _____  | |  _   / _ \  | |  / _` | | '_ \   / _` |
  | |  | |_| | | |___  |_____| | |_| | | (_) | | | | (_| | | | | | | (_| |
 |___|  \___/   \____|          \____|  \___/  |_|  \__,_| |_| |_|  \__, |
                                                                    |___/ 
Welcome to use ioc-golang!
[Boot] Start to load ioc-golang config
[Config] Load config file from ../conf/ioc_golang.yaml
Load ioc-golang config file failed. open ../conf/ioc_golang.yaml: no such file or directory
The load procedure is continue
[Boot] Start to load debug
[Debug] Debug mod is not enabled
[Boot] Start to load autowire
[Autowire Type] Found registered autowire type normal
[Autowire Struct Descriptor] Found type normal registered SD github.com/alibaba/ioc-golang/extension/normal/redis.Impl
[Autowire Type] Found registered autowire type singleton
[Autowire Struct Descriptor] Found type singleton registered SD main.App
get val =  db0

可看到打印出了 redis 中的数据。

4.6 - 使用基于monkey指针的调试功能

简介

本示例展示了 ioc-golang 框架提供的基于monkey指针的调试功能。

注入到接口的对象已经默认支持调试层。本例子针对注入到接口体指针的对象,可通过这种方式赋予其代理层运维能力。

基于monkey指针的调试功能对于程序性能有损耗,请您不要在追求性能的场景下开启调试能力。

本框架基于 AOP 的思路,为每个注册在框架的结构方法都封装了一组拦截器。基于这些拦截器,可以实现具有扩展性的调试功能。

调试能力包括:

  • 基于 ioc-debug 协议,暴露调试端口
  • 查看所有接口、实现、方法列表
  • 监听、修改任意方法的入参和返回值
  • 性能瓶颈分析【开发中】
  • 可观测性【开发中】

示例介绍

本示例实现了以下拓扑

debug

在这个例子中,App 结构会依此调用所有依赖对象,进而调用一个单例模型注入的 gRPC 客户端,该客户端发起网络请求,并获得结果。

我们将开启 debug 模式,通过 iocli 工具查看接口、实现、方法,并监听通过 gRPC Client 发送的所有请求和返回值。

运行示例

  1. 启动 grpc Server

    % cd example/denbug/grpc_server
    % go run .
    
  2. 新开一个终端,启动客户端。

    注意 GOARCH 环境变量和 ‘-gcflags="-N -l" -tags iocdebug’ 编译参数, amd机器无需指定 GOARCH 环境变量。

    正确在 ioc_golang.yaml 中开启debug模式后,会打印

    [Debug] Debug port is set to default :1999 的日志。

    % cd example/debug/cmd
    % GOARCH=amd64 go run -gcflags="-N -l" -tags iocdebug  .
      ___    ___     ____            ____           _                         
     |_ _|  / _ \   / ___|          / ___|   ___   | |   __ _   _ __     __ _ 
      | |  | | | | | |      _____  | |  _   / _ \  | |  / _` | | '_ \   / _` |
      | |  | |_| | | |___  |_____| | |_| | | (_) | | | | (_| | | | | | | (_| |
     |___|  \___/   \____|          \____|  \___/  |_|  \__,_| |_| |_|  \__, |
                                                                        |___/ 
    Welcome to use ioc-golang!
    [Boot] Start to load ioc-golang config
    [Config] Load default config file from ../conf/ioc_golang.yaml
    [Config] merge config map, depth: [0]
    [Boot] Start to load debug
    [Debug] Debug port is set to default :1999
    [Boot] Start to load autowire
    [Autowire Type] Found registered autowire type singleton
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/debug/cmd/service2.Impl2
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/debug/cmd/struct1.Struct1
    [Autowire Struct Descriptor] Found type singleton registered SD main.App
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/debug/cmd/service1.Impl1
    [Autowire Struct Descriptor] Found type singleton registered SD github.com/alibaba/ioc-golang/example/debug/cmd/service2.Impl1
    [Autowire Type] Found registered autowire type grpc
    [Autowire Struct Descriptor] Found type grpc registered SD github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient
    [Debug] Debug server listening at :1999
    create conn target  localhost:8080
    App call grpc get: Hello laurence
    ExampleService1Impl1 call grpc get :Hello laurence_service1_impl1
    ExampleService2Impl1 call grpc get :Hello laurence_service2_impl1
    ExampleService2Impl2 call grpc get :Hello laurence_service2_impl2
    ExampleStruct1 call grpc get :Hello laurence_service1_impl1
    

    每隔 5s,所有的对象都会发起一次 gRPC 请求。

  3. 新开一个终端,查看所有接口、实现和方法。

    % iocli list
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient
    [SayHello]
    
    github.com/alibaba/ioc-golang/example/debug/cmd/service1.Impl1
    [Hello]
    
    github.com/alibaba/ioc-golang/example/debug/cmd/service2.Impl1
    [Hello]
    
    github.com/alibaba/ioc-golang/example/debug/cmd/service2.Impl2
    [Hello]
    
    github.com/alibaba/ioc-golang/example/debug/cmd/struct1.Struct1
    [Hello]
    
    main.App
    [Run]
    
  4. 监听 gRPC Client 的所有流量,每隔 5s 会打印出相关的请求、返回值信息。

    % iocli watch github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient  SayHello
    
    ========== On Call ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Param 1: (*context.emptyCtx)(0xc0000280e0)(context.Background)
    
    Param 2: (*api.HelloRequest)(0xc000260280)(name:"laurence")
    
    Param 3: ([]grpc.CallOption) (len=2 cap=2) {
    (grpc.MaxRecvMsgSizeCallOption) {
    MaxRecvMsgSize: (int) 1024
    },
    (grpc.MaxRecvMsgSizeCallOption) {
    MaxRecvMsgSize: (int) 1024
    }
    }
    
    
    ========== On Response ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Response 1: (*api.HelloResponse)(0xc000260380)(reply:"Hello laurence")
    
    Response 2: (interface {}) <nil>
    
    
    ========== On Call ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Param 1: (*context.emptyCtx)(0xc0000280e0)(context.Background)
    
    Param 2: (*api.HelloRequest)(0xc0003988c0)(name:"laurence_service1_impl1")
    
    Param 3: ([]grpc.CallOption) <nil>
    
    
    ========== On Response ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Response 1: (*api.HelloResponse)(0xc000398980)(reply:"Hello laurence_service1_impl1")
    
    Response 2: (interface {}) <nil>
    
    
    ========== On Call ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Param 1: (*context.emptyCtx)(0xc0000280e0)(context.Background)
    
    Param 2: (*api.HelloRequest)(0xc000260480)(name:"laurence_service2_impl1")
    
    Param 3: ([]grpc.CallOption) <nil>
    
    
    ========== On Response ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Response 1: (*api.HelloResponse)(0xc000260540)(reply:"Hello laurence_service2_impl1")
    
    Response 2: (interface {}) <nil>
    
    
    ========== On Call ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Param 1: (*context.emptyCtx)(0xc0000280e0)(context.Background)
    
    Param 2: (*api.HelloRequest)(0xc00041c200)(name:"laurence_service2_impl2")
    
    Param 3: ([]grpc.CallOption) <nil>
    
    
    ========== On Response ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Response 1: (*api.HelloResponse)(0xc00041c2c0)(reply:"Hello laurence_service2_impl2")
    
    Response 2: (interface {}) <nil>
    
    
    ========== On Call ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Param 1: (*context.emptyCtx)(0xc0000280e0)(context.Background)
    
    Param 2: (*api.HelloRequest)(0xc000260700)(name:"laurence_service1_impl1")
    
    Param 3: ([]grpc.CallOption) <nil>
    
    
    ========== On Response ==========
    github.com/alibaba/ioc-golang/example/debug/api.HelloServiceClient.SayHello()
    Response 1: (*api.HelloResponse)(0xc0002607c0)(reply:"Hello laurence_service1_impl1")
    
    Response 2: (interface {}) <nil>
    ...
    

小结

通过 Debug 能力,开发人员可以在测试环境内动态地监控流量,帮助排查问题。

也可以基于 ioc-golang 提供的拦截器层,注册任何自己期望的调试拦截器,扩展调试、可观测、运维能力。

4.7 - RPC 能力

使用 RPC 能力

简介

本示例展示了基于 IOC-golang 框架的 RPC 能力

在微服务开发过程中,暴露一些对象的方法给外部程序调用是一种常见的场景。

在 IOC-golang 的 RPC 能力的使用场景下,客户端可以直接注入下游接口的客户端存根,以方法调用的形式发起RPC请求。

关键代码

服务端

需要暴露的RPC服务结构使用 // +ioc:autowire:type=rpc 标注,例如 service.go:

import (
	"github.com/alibaba/ioc-golang/example/autowire_rpc/server/pkg/dto"
)

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

type ServiceStruct struct {
}

func (s *ServiceStruct) GetUser(name string, age int) (*dto.User, error) {
	return &dto.User{
		Id:   1,
		Name: name,
		Age:  age,
	}, nil
}

使用 iocli 工具生成相关代码。

sudo iocli gen

% tree
.
├── api
│   └── zz_generated.ioc_rpc_client_servicestruct.go
├── service.go
└── zz_generated.ioc.go

会在当前文件目录下生成 zz_generated.ioc.go 包含了服务提供者的结构描述信息。也会在当前目录下创建 api/ 文件夹,并创建当前结构的客户端存根文件 zz_generated.ioc_rpc_client_servicestruct.go

客户端

可以通过标签注入的方法,注入客户端存根,存根中给出下游地址。默认服务暴露端口为2022

import(
  "github.com/alibaba/ioc-golang/example/autowire_rpc/server/pkg/service/api"
)
// +ioc:autowire=true
// +ioc:autowire:type=singleton

type App struct {
	ServiceStruct *api.ServiceStructIOCRPCClient `rpc-client:",address=127.0.0.1:2022"`
}

func (a *App) Run() {
	for {
		time.Sleep(time.Second * 3)
		usr, err := a.ServiceStruct.GetUser("laurence", 23) // RPC调用
		if err != nil {
			panic(err)
		}
		fmt.Printf("get user = %+v\n", usr)
	}
}

运行示例

启动服务端

服务端需要确保引入对应服务提供者结构包: _ "github.com/alibaba/ioc-golang/example/autowire_rpc/server/pkg/service"

package main

import (
	"github.com/alibaba/ioc-golang"
	_ "github.com/alibaba/ioc-golang/example/autowire_rpc/server/pkg/service"
)

func main() {
	// start
	if err := ioc.Load(); err != nil {
		panic(err)
	}
	select {}
}

启动服务端进程,默认 rpc 服务监听 2022 端口

% cd server
% go run .
...
[negroni] listening on :2022

启动客户端

开启另一个终端,进入client目录,启动进程,不断发起调用,获得返回值。

% cd client
% go run .
...
get user = &{Id:1 Name:laurence Age:23}
get user = &{Id:1 Name:laurence Age:23}
get user = &{Id:1 Name:laurence Age:23}

5 - 任务

5.1 - 电商系统搭建

使用 ioc-golang 搭建一个电商系统

6 - 引用

6.1 - iocli 工具

iocli 是一款命令行工具,提供了以下能力:

  • 代码调试

    开发者启动 ioc-golang 框架提供的调试能力,iocli 作为调试客户端。

  • 结构描述注册信息生成

    开发者可以为需要依赖注入的结构体增加注解,iocli 会识别这些注解,自动生成SD(结构描述符)代码,注册至框架。

安装

go install github.com/alibaba/ioc-golang/iocli@latest

代码调试能力

ioc-golang 框架拥有首创的基于 AOP 思路的 Go 运行时程序调试能力。基于 ioc debug 协议

代码调试示例可参考 调试能力示例快速开始

基于该思路,我们可以扩展出更丰富的 cli 端调试能力,例如:

  • 流量过滤、监控
  • 参数编辑
  • 故障注入
  • 耗时瓶颈分析

结构注解与SD代码生成

iocli 可以一键生成当前目录和子目录下的所有标记结构的描述符

sudo iocli gen

可以识别以下注解:

// +ioc:autowire=true
// +ioc:autowire:type=normal
// +ioc:autowire:interface=Redis
// +ioc:autowire:paramLoader=paramLoader
// +ioc:autowire:paramType=Config
// +ioc:autowire:constructFunc=New
// +ioc:autowire:baseType=true
// +ioc:autowire:alias=myAppAliasName
  • ioc:autowire

    bool 类型,为 true 则该结构被识别到。

  • ioc:autowire:type

    string类型,表示依赖注入模型,目前支持四种:

    • singleton

      单例模型,该结构体全局只能产生一个对象,无论是 API 获取还是字段注入。

    • normal

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

    • config:

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

    • grpc:

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

  • ioc:autowire:interface(非必填)

    string类型,表示实现的接口名,如果不存在这个标注,将作为结构体指针注入给使用方。

  • ioc:autowire:paramLoader(非必填)

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

    参数加载器由结构定义者可选定制。可参考:ioc-go-extension/normal/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:"Impl,address=127.0.0.1:6379&db=3"`
    }
    

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

    type App struct {
    	NormalDB3Redis normalRedis.Redis `normal:"Impl,db1-redis"`
    }
    

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

    autowire:
      normal:
        Redis:
          Impl:
            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类型,表示结构的构造方法名,作为依赖参数的一个函数。

    在上述例子中,该方法名为 New。

    思路为:依赖参数是构造对象的前提,因此该方法放置在参数下,会被框架调用。对于不依赖外部传递参数,但拥有构造函数的对象,可以使用一个空Struct 作为参数,在这一Struct 内实现构造方法。

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

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

    该类型是否为基础类型

    go 基础类型不可直接通过&在构造时取地址,因此我们针对基础类型单独设计了该注解。在 http://github.com/alibaba/ioc-golang/extension/tree/master/config 配置扩展中被使用较多。

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

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

6.2 - 标签定义规范

6.3 - 配置文件结构规范

默认配置层级

配置文件 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

6.4 - ioc_debug 协议

我们期望推出一个通用的代码调试协议,目前在设计阶段

proto 位于:debug.proto

7 - 开发者文档

框架可扩展性

ioc-golang 全面拥抱可扩展性,我们希望您在框架内接触到的任何概念都是可横向扩展的。可扩展性不意味着任何事情都要手动编写,而是在拥有足够预置实现方案的基础之上,支持针对特殊场景的定制化。如果您的方案足够通用,也可以提交至开源侧,让更多人可以享受你的方案带来的便利。

ioc-golang 的可扩展性体现在多个方面,其中最重要的是依赖注入过程的可扩展性,这也是框架能的核心能力。

依赖注入的可扩展性包含三个维度,从具体到抽象分别是:

  • 对象的可扩展性

    对象的可扩展性,即针对确定的一个结构(非单例),你可以通过传入不同的参数来获取多个期望的对象。这个过程往往被结构使用者关注,他需要思考如何传入参数,获得对象,调用对象从而实现正确的业务逻辑。通过这一可扩展性,结构使用者可以扩展出多个对象实例。

  • 结构的可扩展性

    结构的可扩展性,即针对一个确定的自动装载模型,你可以通过定义自己的结构描述信息,将你的结构体注册在框架上,以供使用。这个过程是结构提供者关心的,他需要思考选用哪个自动装载模型,思考提供的结构的全部生命周期信息。通过这一可扩展性,结构提供者可以为框架注入多种多样的结构,这些结构都会按照被选择的自动装载模型执行加载逻辑。

    框架提供了一些预置的结构,例如 redis 客户端、gorm客户端等等,开发者可以直接传入参数,注入或通过API获取,直接使用,但这些预置的结构一定无法覆盖业务需求的。开发者注册自己的任何结构到框架上,都是使用了结构的可扩展性,这些被注册的结构和框架提供的预置结构,都是同一层面的概念。

  • 自动装载模型的可扩展性

    自动装载模型描述了一类结构的装载方式,例如是否单例模型、参数从哪里加载、注入标签应该符合什么格式等等。这个过程也是结构提供者关心的。

    框架提供了一些预置的自动装载模型,例如单例模型、多例模型、配置模型、rpc 模型等,开发者可以根据按照业务需要,将一个或多个结构注册在期望的自动装载模型上,当已有的自动装载模型不足以适配业务场景,开发者可以调用API进行定制化

7.1 - 项目结构

IOC-golang 框架项目结构
  • autowire: 提供单例模型、多例模型两种基本注入模型

  • config: 配置加载模块,负责解析用户yaml配置文件

  • debug: 调试模块:提供调试 API、提供调试注入层实现

  • extension: 组件扩展目录:提供基于多种注入模型的预置实现结构:

    • autowire:自动装载模型扩展

      • grpc:grpc 客户端模型定义

      • config:配置模型定义

    • config:配置注入模型扩展结构

      • string,int,map,slice
    • normal:多例模型扩展结构

      • redis

      • mysql

      • rocketmq

      • nacos

    • singleton:单例模型扩展结构

      • http-server
  • example: 示例仓库

  • iocli: 代码生成/程序调试 工具

    提供基于注解的结构描述信息自动生成能力

7.2 - 开发接口或结构

基于 IOC-golang 框架开发接口或结构

7.3 - 开发自动装载模型

基于 IOC-golang 框架开发自动装载模型

7.4 - 开发 debug 拦截器

开发基于 IOC-golang 框架 debug 模式的拦截器