文档目录
glen
· 预计阅读 3 分钟

第 4 章 Service Provider 与依赖注入

本文导航
第 4 章 Service Provider 与依赖注入

第 4 章 Service Provider 与依赖注入

Owl 的装配由两部分组成:

  • Provider 负责把「能力」注册进容器(数据库、Redis、路由、权限引擎……);
  • Binds 负责把「业务对象」注册进容器(Repository、Service、Handle)。

两者最终都汇入同一个 dig 容器。这一章说明怎么写 Provider,以及依赖注入的常见用法和错误。

Provider 契约

type ServiceProvider interface {
	Register()               // 向容器注册构造函数
	Boot()                   // 所有依赖就绪后的收尾
	Conf() map[string]string // 文件名 -> 默认配置内容
	Description() string     // 描述,日志和配置补键时使用
}

Provider 结构体同样必须有 app foundation.Application 字段。

三个方法的时机

方法 时机 典型用途
Conf() 装配早期,所有 Register 之前 返回默认 yaml,框架写盘或补键
Register() 配置全部落盘之后 注册构造函数,不读业务配置
Boot() 所有子应用 Bootstrap、迁移之后 需要依赖已就绪的初始化

Register() 里不要做耗时操作,也不要假设数据库已连接。需要连接的动作放到 Boot() 或首次 Invoke 时。

写一个自定义 Provider

以 JWT 为例,目标是把 *JWTService 注册进容器,并生成 conf/jwt.yaml:

package jwt

import (
	_ "embed"

	"bit-labs.cn/owl/contract/foundation"
	"bit-labs.cn/owl/provider/conf"
)

//go:embed jwt.yaml
var exampleConf string

type JwtServiceProvider struct {
	app foundation.Application
}

func (s *JwtServiceProvider) Description() string { return "JWT 生成与校验" }

func (s *JwtServiceProvider) Register() {
	s.app.Register(func(c *conf.Configure) *JWTService {
		var opt JWTOptions
		_ = c.GetConfig("jwt", &opt)
		return NewJWTService(opt)
	})
}

func (s *JwtServiceProvider) Boot() {}

func (s *JwtServiceProvider) Conf() map[string]string {
	return map[string]string{"jwt.yaml": exampleConf}
}

jwt.yaml:

signing-key: change-me
issuer: bit-labs
expire: 7200

要点:

  • Register() 里用 app.Register(构造函数),等价于 app.Provide;
  • 构造函数参数由 dig 解析,*conf.Configure 是框架已注册的基础依赖;
  • Conf() 的 key 是文件名,value 是默认内容;
  • Provider 自己也要有 app 字段。

写完 Provider 后,在子应用里登记:

func (i *SubAppAdmin) ServiceProviders() []foundation.ServiceProvider {
	return []foundation.ServiceProvider{
		&jwt.JwtServiceProvider{},
		&router.RouterServiceProvider{},
		&db.DBServiceProvider{},
	}
}

依赖注入的两种用法

在 Binds 中声明构造函数

func (i *SubAppOrder) Binds() []any {
	return []any{
		repository.NewOrderRepository,
		service.NewOrderService,
		v1.NewOrderHandle,
	}
}
func NewOrderRepository(gdb *gorm.DB) *OrderRepository {
	return &OrderRepository{db: gdb}
}

func NewOrderService(repo OrderRepositoryInterface, locker redis.LockerFactory) *OrderService {
	return &OrderService{repo: repo, locker: locker}
}

func NewOrderHandle(svc *OrderService) *OrderHandle {
	return &OrderHandle{svc: svc}
}

dig 看到 NewOrderHandle 需要一个 *OrderService,就会去找能生产它的构造函数,依此类推。

在运行期 Invoke

需要在 RegisterRouters() 或 Bootstrap() 里拿依赖时,用 Invoke:

func (i *SubAppOrder) RegisterRouters() {
	err := i.app.Invoke(func(
		engine *gin.Engine,
		handle *v1.OrderHandle,
	) {
		group := engine.Group("/api/v1")
		group.GET("/orders", handle.Retrieve)
	})
	owl.PanicIf(err)
}

Invoke 会立即构造参数链上所有依赖。如果缺少某个类型,会返回错误,通常应 owl.PanicIf(err) 让问题在启动时暴露。

如何知道容器里有什么

框架启动日志会打印 Provider 列表:

============================核心服务提供者==========================================
*conf.ConfServiceProvider 配置加载
*router.RouterServiceProvider HTTP 路由与中间件
...

业务对象不会逐个打印,但可以通过「启动是否报 missing type」判断是否漏注册。

常见错误

1. 把实例放进 Binds

// 错误:这里直接调用构造函数,返回的是实例
return []any{
	service.NewOrderService(repo, locker),
}

Binds() 要的是 NewOrderService 这个函数值,不是调用结果。

2. 忘了注册 Provider

例如 Service 依赖 redis.LockerFactory,但 ServiceProviders() 里没有 &redis.RedisServiceProvider{},启动时会报缺失类型。

3. 在 Register 里读取尚未加载的配置

Register() 阶段 NewConfigure 还没执行,读配置会拿到零值。需要配置的构造函数,把 *conf.Configure 作为参数,让框架在 Invoke 时注入。

4. 循环依赖

A 的构造函数依赖 B,B 又依赖 A,dig 会报 cycle。正确做法是抽出共享的下层,或改用事件解耦。

自检

  • [ ] Provider 有 app foundation.Application 字段;
  • [ ] Conf() 返回的默认配置能被框架写盘/补键;
  • [ ] Register() 只注册构造函数;
  • [ ] Binds() 里是函数值,不是调用结果;
  • [ ] 新增依赖时同步补了对应 Provider。

评论

加载评论中…

上一篇 第 3 章 SubApp 契约:子应用必须实现的 10 个方法 下一篇 第 5 章 路由、菜单与权限标识

BitLabs © 2026 · 论坛讨论 · 博客长文 · 文档手册