Go-Spring 实战第 14 课 —— Bean 注册函数:Provide、Module、Group 以及 Configuration

前面的文章中我们都是使用 gs.Provide() 注册 Bean 的。实际上 Go-Spring 还提供了其他注册 Bean 的方法。本文咱们就来详细看一下。

gs.Provide

gs.Provide() 是最常用的注册 Bean 的入口。它的作用是把一个对象或者构造函数交给 Go-Spring 容器。它的用法也最简单,通常在 Go init 函数里调用。

看个例子。

go 复制代码
func init() {
	gs.Provide(NewUserService)
}

gs.Provide() 会把 Bean 的元信息记录到 Go-Spring 的全局注册表。每次新建一个 IoC 容器时,Go-Spring 会将全局注册表里的 Bean 元信息并入当前容器,和直接注册到 IoC 容器里的 Bean 元信息合在一起,然后再统一处理。

什么是全局注册表?通常来说,应用启动只需要一个 IoC 容器,但是在测试模式下,每个测试函数都希望使用只属于自己的独立 IoC 容器,这样就会多次创建和启动 IoC 容器。为了实现数据隔离,Go-Spring 提出了全局注册表的概念。

通常来说,gs.Provide() 只要在 app 启动前执行就可以了。但为了规范和统一,也为了让代码语义更清楚,我们更建议在 Go init() 函数里调用。这样一来,init() 的职责也能更明确了------只注册元数据,不执行业务初始化逻辑。真正的程序初始化,应该放到 main() 开始之后执行。

app.Provide

通常情况下,我们只需要将 Bean 元信息注册到全局注册表。但是,有些情况下我们希望将 Bean 直接注册到当前 IoC 容器,尤其是在单测需要启动多个 IoC 容器的时候。每个测试可能都需要自己的 Bean 配置,这时候定制化的 Bean 就更应该直接放进当前 IoC 容器里了。

app.Provide() 就是这个入口,它可以将一个 Bean 直接注册到当前 IoC 容器里。它需要配合 gs.Configure() 回调函数一起使用。示例如下:

go 复制代码
func main() {
	gs.Configure(func(app gs.App) {
		app.Provide(NewAppSpecificComponent)
		app.Property("server.port", "8080")
	}).Run()
}

如果 app.Provide() 注册的 Bean 和其他方式注册的 Bean 同类型同名,那么不会自动覆盖旧的定义,而是直接报错。如果我们确实需要使用覆盖语义,可以使用名称或者条件等来激活不同的 Bean。

gs.Module

通常情况下,我们只需要注册单个 Bean 就好。但有时候,尤其是我们打算按模块组织能力时,会遇到同时注册多个 Bean 的情况。这里说的不是用一个函数同时创建多个 Bean,那样每个 Bean 的元信息是没法控制的。更准确地说,我们希望注册一组 Bean。

gs.Module() 就是为了满足这种需求而设计的。它接受一个回调函数,我们可以在回调函数里面根据配置信息注册多个 Bean。这些 Bean 可以是相同类型,也可以不是。

gs.Module() 是实现 Go-Spring Starter 机制的基石。像 Redis Starter、GORM Starter 等组件包,都可以使用 gs.Module() 来实现。

代码如下:

go 复制代码
func RedisModule(r gs.BeanProvider, p flatten.Storage) error {
	var instances map[string]RedisConfig
	if err := conf.Bind(p, &instances, "${redis.instances}"); err != nil {
		return err
	}
	for name, cfg := range instances {
		r.Provide(NewRedisClient, gs.ValueArg(cfg)).Name(name)
	}
	return nil
}

func init() {
	gs.Module(
		gs.OnProperty("redis.instances"),
		RedisModule,
	)
}

gs.Module() 同样将回调函数注册到全局注册表。多个 IoC 容器启动时,会分别基于这份全局模块列表做条件判断;只有条件满足的模块,才会在当前容器里展开注册。这样同一个模块声明可以服务多个容器,但每个容器最终得到的是自己的 Bean 定义集合。也就实现了数据隔离。

gs.Module() 的第一个参数是属性条件,通常使用 gs.OnProperty(...),因为大多数情况下我们是根据配置来控制整个模块是否生效的。

gs.Module() 的第二个参数是模块函数,它接受 gs.BeanProvider 和 flatten.Storage 作为参数,并返回一个错误。gs.BeanProvider 用于向当前 IoC 容器直接注册 Bean。flatten.Storage 用于读取配置。

IoC 容器启动时,会根据 Module 的条件来判断是否执行模块函数。如果条件不满足,那么模块函数就不会被调用,也就是这一组 Bean 集体不会注册。

在上面的代码里,redis.instances 这个配置是否存在,决定了 Redis 模块是否激活。这种方式很适合 Starter、可选组件,以及按配置展开多实例资源的场景。

gs.Group

gs.Group() 是 gs.Module() 的一个特殊版本,用来控制一组相同类型 Bean 的注册。它接受一个配置项,这个配置项必须写成 ${...} 形式。配置项下面的子 key 会成为创建出来的 Bean 的名称,并且一个子 key 对应一个 Bean。

示例如下:

go 复制代码
type HTTPClientConfig struct {
	BaseURL string        `value:"${baseURL}"`
	Timeout time.Duration `value:"${timeout:=30s}"`
}

type HTTPClient struct {
	baseURL string
	client  *http.Client
}

func NewHTTPClient(c HTTPClientConfig) (*HTTPClient, error) {
	return &HTTPClient{
		baseURL: c.BaseURL,
		client:  &http.Client{Timeout: c.Timeout},
	}, nil
}

func (c *HTTPClient) Close() error {
	return nil
}

func init() {
	gs.Group("${http.clients}", NewHTTPClient, nil)
}

在上面的代码中,gs.Group() 会根据 ${http.clients} 配置项的内容,展开出多个 Bean。

上面的代码可以配合下面的配置使用。

yaml 复制代码
http:
  clients:
    serviceA:
      baseURL: "http://a.example.com"
      timeout: 30s
    serviceB:
      baseURL: "http://b.example.com"
      timeout: 60s

根据上面的配置,Go-Spring 会注册 serviceA 和 serviceB 两个 Bean 实例。它们都是 *HTTPClient 类型,但名字不同,构造参数也分别来自各自的配置节点。

在这种情况下,依赖方通常需要按照名称区分需要注入的 Bean 实例。

go 复制代码
type ReportService struct {
	Client *HTTPClient `autowire:"serviceA"`
}

如果每个实例都需要释放资源,那么可以给 gs.Group() 传入销毁函数。这个销毁函数会绑定到每一个由配置项展开出来的 Bean 上。

go 复制代码
func init() {
	gs.Group("${http.clients}", NewHTTPClient, (*HTTPClient).Close)
}

gs.Group() 在创建多实例 Client 场景时非常有用,比如多个 HTTP 客户端、多个数据库连接、多个 Redis 客户端。只要这些实例创建方式相同,只是配置不同,就可以优先考虑使用 gs.Group()。

Configuration

Configuration 是一种接近 Java Spring 配置类的 Bean 注册方式,它可以将配置 Bean 的方法返回值导出成新的 Bean。这样,我们就可以把一组相关 Bean 的创建逻辑收拢到同一个配置对象里。

看个例子。

go 复制代码
type DatabaseConfiguration struct {
	MaxOpenConns int `value:"${db.max-open-conns:=10}"`
}

func (c *DatabaseConfiguration) NewDataSource() *DataSource {
	return NewDataSource(c.MaxOpenConns)
}

func (c *DatabaseConfiguration) NewUserRepository(ds *DataSource) *UserRepository {
	return NewUserRepository(ds)
}

func init() {
	gs.Provide(new(DatabaseConfiguration)).Configuration()
}

在上面的示例中,DatabaseConfiguration 本身先作为普通 Bean 进行注册。它可以接收配置绑定,也可以接收其他注入。随后,我们通过 .Configuration() 调用告诉 Go-Spring:这个 Bean 是一个特殊的配置 Bean,它的公开方法返回值也可以注册成 Bean。

默认情况下,Go-Spring 会扫描公开方法里匹配正则 New.* 的方法。在上面的例子中,NewDataSource 和 NewUserRepository 都会被识别为构造方法,它们分别创建 *DataSource 和 *UserRepository 类型的 Bean。随后,NewUserRepository 需要的 ds 参数,也会从容器中按照类型选择出来。

和普通构造函数一样,这些被识别为子 Bean 的方法可以只返回一个对象 T,也可以返回 (T, error)。

另外,配置类方法导出的子 Bean 会得到自动生成的名称,形式是"配置 Bean 名称 + 方法名",中间用下划线连接,例如 DatabaseConfiguration_NewDataSource。

如果需要调整扫描范围,我们可以显式地声明包含和排除规则。示例如下:

go 复制代码
func init() {
	gs.Provide(new(DatabaseConfiguration)).
		Configuration(gs.Configuration{
			Includes: []string{"New.*", "Create.*"},
			Excludes: []string{".*Internal$"},
		})
}

Configuration 用法非常特殊,我们承认它的独特价值,但是也要警惕它的复杂性和隐蔽性。

如果把扫描过程拆开看,Configuration 的注册方式和下面这种只使用 gs.Provide() 的方式非常相似。代码如下:

go 复制代码
func init() {
	cfg := gs.Provide(new(DatabaseConfiguration))

	gs.Provide((*DatabaseConfiguration).NewDataSource, cfg).
		Name("DatabaseConfiguration_NewDataSource")

	gs.Provide((*DatabaseConfiguration).NewUserRepository, cfg).
		Name("DatabaseConfiguration_NewUserRepository")
}

上面代码里的 cfg 表示在容器中注册的一个 Bean。然后它被 DatabaseConfiguration_NewDataSource 和 DatabaseConfiguration_NewUserRepository 这两个 Bean 作为构造函数的依赖使用。在 Go 里面,方法和函数其实没有太大不同,接收者可以理解成函数的第一个参数。这样就容易理解为什么上面的代码能工作了。

但同时我们也可以看到,手写 gs.Provide() 更加灵活,我们可以给每个 Bean 单独添加更多的元信息。这一点使用 Configuration 就办不到了。

如何选择

我们看到 Go-Spring 提供了多种 API 来满足不同的需求,但是我们一定要优先使用最简单的方式。当其他方式确实能简化代码、简化表达时,再考虑使用它们。

相关推荐
苏supper32 分钟前
Spring Boot多环境配置与打包运行
spring boot·后端·spring
wang_shu_mo_ran8 小时前
Spring IoC和DI概念篇
java·后端·spring
我的div丢了肿么办9 小时前
go语言中如何安装第3方的包
后端·go
驭渊的小故事9 小时前
MyBatis 入门:从 JDBC 到优雅操作数据库(一)
笔记·sql·spring
萧瑟余晖10 小时前
Spring IoC容器与Bean生命周期详解
spring
Wx-bishekaifayuan10 小时前
springboot房屋租赁系统11574-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·sql·spring·课程设计
tachibana213 小时前
与其他语言相比,使用 Go 有什么好处?
开发语言·后端·golang·go
OxYGC1 天前
[AI工程] Spring AI 第廿一篇:存量 REST 接口的 MCP 化改造实录——工具从哪条路径注册、参数描述怎么写、身份怎么过去
java·人工智能·spring
卓怡学长1 天前
w198基于springboot始于足下健康打卡平台的设计与实现
java·spring boot·spring·intellij-idea
JWASX1 天前
Java 转 go 学习 - 项目管理
go