【问题标题】:Creating Per-Provider Loggers in Wire Dependency Injection在 Wire Dependency Injection 中创建 Per-Provider Loggers
【发布时间】:2021-09-30 21:06:49
【问题描述】:

我正在使用github.com/google/wire 在我正在处理的an open source example project 中进行依赖注入。

我在一个名为interfaces的包中有以下接口:

type LoginService interface {
    Login(email, password) (*LoginResult, error)
}

type JWTService interface {
    Generate(user *models.User) (*JWTGenerateResult, error)
    Validate(tokenString string) (*JWTValidateResult, error)
}

type UserDao interface {
    ByEmail(email string) (*models.User, error)
}

我的实现看起来像这样:

type LoginServiceImpl struct {
    jwt interfaces.JWTService
    dao interfaces.UserDao
    logger *zap.Logger
}

func NewLoginService(jwt interfaces.JWTService, dao interfaces.UserDao, \
        logger *zap.Logger) *LoginServiceImpl {
    return &LoginServiceImpl{jwt: jwt, dao: dao, logger: logger }
}

type JWTServiceImpl struct {
    key [32]byte
    logger *zap.Logger
}

func NewJWTService(key [32]byte, logger *zap.Logger) (*JWTServiceImpl, error) {
    r := JWTServiceImpl {
        key: key,
        logger: logger,
    }

    if !r.safe() {
        return nil, fmt.Errorf("unable to create JWT service, unsafe key: %s", err)
    }

    return &r, nil
}

type UserDaoImpl struct {
    db: *gorm.DB
    logger: *zap.Logger
}

func NewUserDao(db *gorm.DB, logger *zap.Logger) *UserDao {
    return &UserDaoImpl{ db: db, logger: logger }
}

我将在这里排除其他工厂函数和实现,因为它们看起来都非常相似。它们可能会返回错误或绝对可靠。

我还有另一个有趣的工厂用于创建数据库连接,我将只展示接口而不是实现:

func Connect(config interfaces.MySQLConfig) (*gorm.DB, error) { /* ... */ }

现在,解决问题。在我的命令行入口点中,我正在创建一个记录器:

logger, err := zap.NewDevelopment()

对于上面的每个工厂方法,我需要提供一个记录器而不是同一个记录器实例,就像这些方法调用如下:

logger, err := zap.NewDevelopment()

// check err

db, err := database.Connect(config)

// check err

userDao := dao.NewUserDao(db, logger.Named("dao.user"))
jwtService, err := service.NewJWTService(jwtKey)

// check err

loginService := service.NewLoginService(jwtService, userDao, logger.Named("service.login"))

我的wire.ProviderSet 结构如下所示:

wire.NewSet(
    wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
    wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
    wire.Bind(new(interfaces.UserDao), new(*dao.UserDaoImpl)),
    service.NewLoginService,
    service.NewJWTService,
    dao.NewUserDao,
    database.Connect,
)

我已经阅读了用户指南、教程和最佳实践,但我似乎找不到将唯一的 zap.Logger 路由到这些工厂方法中的每一个,并随机路由 [32]byte 的方法用于 JWT 服务。

由于我的根记录器不是在编译时创建的,而且这些工厂方法中的每一个都需要自己独特的记录器,我如何告诉wire 将这些实例绑定到相应的工厂方法?我很难理解如何将相同类型的自定义实例路由到不同的工厂方法。

总结:

Wire 似乎倾向于在编译时做所有事情,将依赖注入配置存储在静态包级变量中。对于我的大多数用例来说,这没问题。

对于我的其余用例,我需要在运行依赖注入之前手动创建一些实例,并能够将各种 *zap.Logger 实例路由到需要它的每个服务。

基本上,我需要wireservices.NewUserDao(Connect(mysqlConfig), logger.Named("dao.user"),但我不知道如何在wire 中表达这一点,并在运行时将变量与wire 的编译时方法合并。

如何在wire 中执行此操作?

【问题讨论】:

  • 您可以尝试将其缩小为单个可执行文件吗?即使您有多个问题。
  • 简而言之:“我需要构建一个wire 集合并将实时实例路由到依赖注入到我的工厂方法中,而不仅仅是编译时工厂和结构”
  • @mh-cbon 我已经更新了我的问题,最后有一个简短的总结,详细说明了问题以及我需要做什么。

标签: go dependency-injection


【解决方案1】:

按照documentation 中的建议,我不得不稍微改变一下我正在做的事情:

如果需要注入像string 这样的通用类型,请创建一个新的字符串类型以避免与其他提供程序发生冲突。例如:

type MySQLConnectionString string

添加自定义类型

文档非常简洁,但我最终做的是创建一堆类型:

type JWTKey [32]byte
type JWTServiceLogger *zap.Logger
type LoginServiceLogger *zap.Logger
type UserDaoLogger *zap.Logger

更新生产者函数

我更新了我的生产者方法以接受这些类型,但不必更新我的结构:

// LoginServiceImpl implements interfaces.LoginService
var _ interfaces.LoginService = (*LoginServiceImpl)(nil)

type LoginServiceImpl struct {
    dao interfaces.UserDao
    jwt interfaces.JWTService
    logger *zap.Logger
}

func NewLoginService(dao interfaces.UserDao, jwt interfaces.JWTService, 
        logger LoginServiceLogger) *LoginServiceImpl {
    return &LoginServiceImpl {
        dao: dao,
        jwt: jwt,
        logger: logger,
    }
}

上面这部分是有道理的;给出不同的类型意味着wire 无需弄清楚。

创建注入器

接下来,我必须创建虚拟注入器,然后使用wire 生成相应的wire_gen.go。这并不容易而且非常不直观。遵循文档时,事情不断发生并给我非常无益的错误消息。

我有一个 cmd/ 包,我的 CLI 入口点位于 cmd/serve/root.go 中,它从命令行以 ./api serve 运行。我在cmd/serve/injectors.go 中创建了我的注入器函数,请注意// +build wireinject 和以下换行符需要通知 Go 该文件用于代码生成而不是代码本身。

经过多次反复试验,我最终得出以下代码:

// +build wireinject

package serve

import /*...*/

func initializeLoginService(
        config interfaces.MySQLConfig,
        jwtKey service.JWTKey,
        loginServiceLogger service.LoginServiceLogger,
        jwtServiceLogger service.JWTServiceLogger,
        userDaoLogger service.UserDaoLogger,
        databaseLogger database.DatabaseLogger,
    ) (interfaces.LoginService, error) {
    
    wire.Build(
        // bind interfaces to implementations
        wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
        wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
        wire.Bind(new(interfaces.UserDao), new(*dao.UserDao)),
        // services
        service.NewLoginService,
        service.NewJWTService,
        // daos
        dao.NewUserDao,
        // database
        database.Connect,
    )

    return nil, nil
}

wire.Bind 调用通知wire 对给定接口使用哪个实现,因此它将知道返回*LoginServiceImplservice.NewLoginService 应该用作interfaces.LoginService

wire.Build 调用中的其余实体只是工厂函数。

向注入器传递值

我遇到的一个问题是我试图将值传递给wire.Buildlike the documentation describes

有时,将基本值(通常为 nil)绑定到类型很有用。您可以将值表达式添加到提供程序集,而不是让注入器依赖于一次性提供程序函数。

type Foo struct {
    X int
}

func injectFoo() Foo {
    wire.Build(wire.Value(Foo{X: 42}))
    return Foo{}
}

...

需要注意的是,表达式会被复制到注入器的包中;对变量的引用将在注入器包的初始化期间进行评估。如果表达式调用任何函数或从任何通道接收,Wire 将发出错误。

这让我感到困惑;听起来你在尝试运行注入器时只能真正使用常量值,但是there are two lines in the docs in the "injectors" section

与提供者一样,注入器可以在输入上进行参数化(然后将其发送给提供者),并且可以返回错误。 wire.Build 的参数与wire.NewSet 相同:它们构成一个提供程序集。这是在该注入器的代码生成期间使用的提供程序集。

这些行带有以下代码:

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

这是我错过的,也是导致我在这方面浪费大量时间的原因。 context.Context 在这段代码中似乎没有在任何地方被传递,而且它是一种常见的类型,所以我只是耸了耸肩,并没有从中吸取教训。

我定义了我的注入器函数来获取 JWT 密钥、MySQL 配置和记录器类型的参数:

func initializeLoginService(
        config interfaces.MySQLConfig,
        jwtKey service.JWTKey,
        loginServiceLogger service.LoginServiceLogger,
        jwtServiceLogger service.JWTServiceLogger,
        userDaoLogger service.UserDaoLogger,
        databaseLogger database.DatabaseLogger,
    ) (interfaces.LoginService, error) {
    // ...
    return nil, nil
}

然后,我尝试将它们注入wire.Build

wire.Build(
    // ...
    wire.Value(config),
    wire.Value(jwtKey),
    wire.Value(loginServiceLogger),
    // ...
)

当我尝试运行wire 时,它抱怨这些类型被定义了两次。我对这种行为非常困惑,但最终了解到wire自动将所有函数参数发送到wire.Build

再一次:wire 自动将所有注入器函数参数发送到wire.Build

这对我来说并不直观,但我学到了 wire 的工作方式。

总结

wire 没有提供一种方法来区分其依赖注入系统中相同类型的值。因此,您需要用类型定义来包装这些简单类型,让wire 知道如何路由它们,所以不要使用[32]byte,而是type JWTKey [32]byte

要将实时值注入到您的 wire.Build 调用中,只需更改您的注入器函数签名以将这些值包含在函数参数中,wire 就会自动将它们注入到 wire.Build 中。

运行cd pkg/my/package && wire 在该目录中为您定义的注入器创建wire_gen.go。完成此操作后,将来对go generate 的调用将在发生更改时自动更新wire_gen.go

我已将 wire_gen.go 文件签入到我的版本控制系统 (VCS) 中,即 Git,由于这些生成的构建工件感觉很奇怪,但这似乎是通常完成的方式。排除wire_gen.go 可能更有利,但如果这样做,您需要找到每个包含带有// +build wireinject 标头的文件的包,在该目录中运行wire,然后运行go generate可以肯定。

希望这可以清除wire 处理实际值的方式:使用类型包装器使它们类型安全,然后简单地将它们传递给您的注入器函数,然后wire 完成其余的工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-14
    • 1970-01-01
    • 2017-10-05
    • 1970-01-01
    • 1970-01-01
    • 2020-10-09
    相关资源
    最近更新 更多