第 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。
评论
加载评论中…