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.BeanProviderflatten.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 会注册 serviceAserviceB 两个 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.* 的方法。在上面的例子中,NewDataSourceNewUserRepository 都会被识别为构造方法,它们分别创建 *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_NewDataSourceDatabaseConfiguration_NewUserRepository 这两个 Bean 作为构造函数的依赖使用。在 Go 里面,方法和函数其实没有太大不同,接收者可以理解成函数的第一个参数。这样就容易理解为什么上面的代码能工作了。

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

如何选择

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

相关推荐
架构源启2 小时前
Spring AI进阶系列(11) Spring AI Multi-Agent 协作系统:辩论、投票与共识机制实战
java·人工智能·spring
一条泥憨鱼2 小时前
深入理解Java反射(超详细)
java·开发语言·spring·mybatis·反射
ForgeAI码匠3 小时前
后台权限不只是菜单隐藏:Forge Admin 的 RBAC 权限链路拆解
java·spring boot·spring·状态模式
锋行天下17 小时前
一句mysql复杂查询搞崩一个壮汉
后端·mysql·go
callJJ18 小时前
Nacos 详解——从概念到实战
java·spring boot·spring·spring cloud·微服务·nacos
小马爱打代码18 小时前
Spring源码 第三篇:Spring 源码深度拆解:循环依赖 + 三级缓存
java·spring·缓存
用户3983461612019 小时前
Go-Spring 实战第 13 课 —— Bean 元信息:名称、生命周期、接口导出、条件和显式依赖
spring·go
小马爱打代码20 小时前
Spring源码 第二篇:Spring Bean 生命周期:从实例化到销毁全源码解析
java·后端·spring
C137的本贾尼20 小时前
Spring AI Alibaba 进阶:DashScope 原生能力与最佳实践
人工智能·spring