依赖注入与 IoC 容器

本指南介绍 AdonisJS 中的依赖注入和 IoC 容器。你将学到:

  • 如何使用 @inject 装饰器实现依赖的自动解析
  • 构造函数注入与方法注入之间的区别
  • 何时以及如何手动使用 IoC 容器
  • 如何为复杂依赖注册 bindings 和单例
  • 如何使用抽象类实现适配器模式
  • 如何在测试期间替换依赖

概述

依赖注入是一种设计模式,它消除了手动创建和管理类依赖的需求。你不再在类内部创建依赖,而是将它们声明为构造函数参数或方法参数,容器会自动解析它们。

AdonisJS 包含一个强大的 IoC(控制反转)容器,它在整个应用中处理依赖注入。当你将某个类作为依赖进行类型标注时,容器会自动创建该类的实例,并在需要的地方注入它。

IoC 容器已经集成到 AdonisJS 的核心部分中,包括 控制器中间件事件监听器Ace 命令 。这意味着你可以在这些类中对依赖进行类型标注,框架在构造它们时会自动解析这些依赖。

Note

AdonisJS 使用 TypeScript 的 experimentalDecoratorsemitDecoratorMetadata 编译选项来启用依赖注入。它们在新 AdonisJS 项目的 tsconfig.json 文件中已预先配置好。

你的第一次依赖注入

让我们从一个实际的例子开始。我们将创建一个 AvatarService,用于为用户生成 Gravatar URL,然后将它注入到控制器中。

  1. 创建服务

    首先,创建一个将被注入的服务类。这个服务基于用户的邮箱地址生成 Gravatar 头像 URL。

    app/services/avatar_service.ts
    import User from '#models/user'
    import { createHash } from 'node:crypto'
    
    export class AvatarService {
      protected getGravatarAvatar(user: User) {
        const emailHash = createHash('md5').update(user.email).digest('hex')
        const url = new URL(emailHash, 'https://gravatar.com/avatar/')
    
        url.searchParams.set('size', '200')
        return url.toString()
      }
    
      getAvatarFor(user: User) {
        return this.getGravatarAvatar(user)
      }
    }
  2. 将服务注入到控制器

    接下来,创建一个使用 AvatarService 的控制器。@inject() 装饰器会告诉容器自动解析并注入该服务。

    app/controllers/users_controller.ts
    import { inject } from '@adonisjs/core'
    import type { HttpContext } from '@adonisjs/core/http'
    import { AvatarService } from '#services/avatar_service'
    import User from '#models/user'
    
    @inject()
    export default class UsersController {
      /**
       * The AvatarService is automatically injected by the container
       * when this controller is constructed
       */
      constructor(protected avatarService: AvatarService) {}
    
      async store({ request }: HttpContext) {
        /**
         * Create a new user (simplified for demonstration)
         */
        const user = await User.create(request.only(['email', 'username']))
        
        /**
         * Use the injected service to generate and save the avatar URL
         */
        const avatarUrl = this.avatarService.getAvatarFor(user)
        user.avatarUrl = avatarUrl
        await user.save()
        
        return user
      }
    }
  3. 注册路由

    最后,将控制器连接到路由上。当你访问该端点时,AdonisJS 会使用容器自动构造控制器。

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { controllers } from '#generated/controllers'
    
    router.post('/users', [controllers.Users, 'store'])

    @inject() 装饰器在控制器类上是必需的。没有它,容器就不知道要解析依赖。该装饰器利用 TypeScript 的反射能力在运行时检测构造函数依赖。

方法注入

方法注入的工作方式与构造函数注入类似,但容器不是为整个类解析依赖,而是为特定方法解析依赖。当只有一个方法需要某个特定依赖,或者你想保持类的构造函数简洁时,这非常有用。

使用方法注入时,@inject() 装饰器必须放在方法前面。

app/controllers/users_controller.ts
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import { AvatarService } from '#services/avatar_service'
import User from '#models/user'

export default class UsersController {
  /**
   * The @inject decorator on the method tells the container
   * to resolve the avatarService parameter automatically
   */
  @inject()
  async store({ request }: HttpContext, avatarService: AvatarService) {
    const user = await User.create(request.only(['email', 'username']))
    
    /**
     * Use the injected service directly as a method parameter
     */
    const avatarUrl = avatarService.getAvatarFor(user)
    user.avatarUrl = avatarUrl
    await user.save()
    
    return user
  }
}

请注意,HttpContext 始终是控制器方法的第一个参数,其后才是你想要注入的任何依赖。容器会自动区分 HTTP 上下文和可注入的依赖。

什么可以被注入?

你只能在其他类中对类进行类型标注并注入。由于 TypeScript 的类型和接口在编译时会被移除,运行时代码无法看到它们,因此容器无法解析它们。

如果一个类还有其他无法自动解析的依赖(例如配置对象),你必须将该类注册为容器中的 binding。我们将在 本指南后面 介绍 bindings。

import type 陷阱

一个会导致依赖注入静默失败的常见问题是:类被意外地使用 TypeScript 的 import type 语法导入。这种情况经常发生,因为具有自动导入功能的代码编辑器往往默认将类作为类型导入。

当你使用 import type 时,TypeScript 会在编译时完全移除该导入。容器在运行时没有类构造函数可以解析,因此你的依赖会变成 undefined

错误:作为类型导入
import { inject } from '@adonisjs/core'
import type { AvatarService } from '#services/avatar_service'

@inject()
export default class UsersController {
  constructor(protected avatarService: AvatarService) {}
}
正确:作为值导入
import { inject } from '@adonisjs/core'
import { AvatarService } from '#services/avatar_service'

@inject()
export default class UsersController {
  constructor(protected avatarService: AvatarService) {}
}

哪些类支持依赖注入

以下类会由容器自动构造,因此你可以使用构造函数注入,在某些情况下还可以使用方法注入。

类的类型构造函数注入方法注入
控制器
中间件
事件监听器✅(仅 handle 方法)
Bouncer 策略
Transformers
Ace 命令✅(仅生命周期方法)

对于你创建的任何其他类,你需要手动使用容器来构造它们,我们将在下一节介绍。

手动使用容器

虽然 AdonisJS 会使用容器自动构造控制器、中间件和其他框架类,但在某些场景下,你可能需要手动构造自己的类。例如,如果你正在实现一个队列系统,并希望每个 job 类都能受益于依赖注入,你就需要直接使用容器的 API。

使用 container.make 构造类

container.make 方法接受一个类构造函数并返回它的一个实例,同时自动解析所有用 @inject() 标记的构造函数依赖。

让我们用一个队列 job 场景来演示。我们将创建两个服务:

  • 一个用于记录日志的 LoggerService,以及一个依赖它的 UserService
  • 然后我们将在一个队列 job 中手动使用容器来构造 UserService,它会自动解析其 LoggerService 依赖。

在整个 AdonisJS 应用中,容器实例都可以通过 app 服务获取。

import { inject } from '@adonisjs/core'

class LoggerService {
  log(message: string) {
    console.log(message)
  }
}

@inject()
export class UserService {
  constructor(protected logger: LoggerService) {}
  
  createUser(data: any) {
    this.logger.log('Creating user...')
  }
}
import app from '@adonisjs/core/services/app'
import { UserService } from '#services/user_service'

export default class ProcessUserJob {
  async handle(userData: any) {
    /**
     * Manually construct UserService using the container.
     * Its LoggerService dependency is automatically resolved.
     */
    const userService = await app.container.make(UserService)    
    await userService.createUser(userData)
  }
}

容器会递归地解析整个依赖树。如果 UserService 有依赖,而这些依赖又有它们自己的依赖,容器会自动解析所有这些依赖。

使用 container.call 调用方法

你可以使用 container.call 方法对任何类方法执行方法注入。当你希望将依赖注入到特定方法而非整个类时,这很有用。

container.call 方法接受类实例、方法名,以及一个运行时值的数组。运行时值作为最前面的参数传入,其后才是自动解析的依赖。

import { inject } from '@adonisjs/core'

class EmailService {
  send(to: string, message: string) {
    console.log(`Sending email to ${to}`)
  }
}

export class NotificationService {
  @inject()
  notify(userId: string, message: string, emailService: EmailService) {
    emailService.send(`user-${userId}@example.com`, message)
  }
}
import app from '@adonisjs/core/services/app'
import { NotificationService } from '#services/notification_service'

/**
 * Create the service instance
 */
const notificationService = await app.container.make(NotificationService)

/**
 * Call the method using the container.
 *
 * The EmailService is automatically resolved and injected.
 * The first two arguments are runtime values we provide.
 */
await app.container.call(
  notificationService,
  'notify',
  ['user-123', 'Welcome to our platform!']
)

Bindings

当类需要无法通过 @inject() 装饰器自动解析的依赖时,你就会使用 bindings 这一机制。例如,当一个类除了类依赖之外还需要一个配置对象或原始值时。

当某个类存在 binding 时,容器会禁用其自动解析逻辑,转而使用你的工厂函数来创建实例。

创建 binding

Bindings 必须在 服务提供者(Service Provider)register 方法中注册。你可以使用 node ace make:provider 命令创建一个新的提供者。

  1. 定义需要自定义构造的类

    让我们创建一个 Cache 类,它同时需要 RedisConnection 和一个配置对象。由于我们要将这个类注册为 binding,因此无需使用 @inject 装饰器。容器会改用我们的工厂函数。

    app/services/cache.ts
    import type { RedisConnection } from '@adonisjs/redis'
    
    export type CacheConfig = {
      ttl: string | number
      grace: boolean
    }
    
    export class Cache {
      constructor(
        public store: RedisConnection,
        public config: CacheConfig
      ) {}
      
      async get(key: string) {
        // Cache implementation
      }
      
      async set(key: string, value: any) {
        // Cache implementation
      }
    }
  2. 在提供者中注册 binding

    创建一个提供者,并在其 register 方法中注册 binding。工厂函数会接收一个 resolver,可用于创建其他类的实例。

    providers/cache_provider.ts
    import type { ApplicationService } from '@adonisjs/core/types'
    import redis from '@adonisjs/redis/services/main'
    import { Cache } from '#services/cache'
    
    export default class CacheProvider {
      constructor(protected app: ApplicationService) {}
      
      register() {
        this.app.container.bind(Cache, async (resolver) => {
          /**
           * Resolve redis dependency from the container
           */
          const redis = await resolver.make('redis')
          const store = redis.connection()
          
          /**
           * Get configuration from the app config
           */
          const config = this.app.config.get<CacheConfig>('cache')
    
          /**
           * Manually construct and return the Cache instance
           * with all its dependencies
           */
          return new Cache(store, config)
        })
      }
    }
  3. 使用 binding

    容器会使用你在 binding 中提供的工厂函数,而不是尝试自动解析依赖。

    import app from '@adonisjs/core/services/app'
    import { Cache } from '#services/cache'
    
    const cache = await app.container.make(Cache)
    
    await cache.set('user:1', { name: 'Virk' })
    const user = await cache.get('user:1')

Bindings 让你完全掌控类的构造方式。工厂函数会接收一个 resolver,你可以用它创建其他依赖,从而通过自定义逻辑构建复杂的依赖树。

单例

单例是仅构造一次然后被缓存的 bindings。对单例多次调用 container.make 会返回同一个实例。这对于应在整个应用中共享的服务非常有用,例如数据库连接或缓存层。

providers/cache_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import redis from '@adonisjs/redis/services/main'
import { Cache } from '#services/cache'

export default class CacheProvider {
  constructor(protected app: ApplicationService) {}
  
  register() {
    /**
     * Use singleton instead of bind.
     * The Cache instance will be created once and reused.
     */
    this.app.container.singleton(Cache, async (resolver) => {
      const redis = await resolver.make('redis')
      const store = redis.connection()
      const config = this.app.config.get<CacheConfig>('cache')
      return new Cache(store, config)
    })
  }
}

现在每次调用 app.container.make(Cache) 都会返回完全相同的 Cache 实例,这既高效,又能在需要时确保共享状态。

别名

别名为 bindings 提供了基于字符串的替代名称,让你可以使用描述性名称而非类构造函数来请求依赖。

要创建别名,请使用 container.alias() 注册它,并使用 TypeScript 模块增强更新 ContainerBindings 接口以获得类型安全。

providers/cache_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import redis from '@adonisjs/redis/services/main'
import { Cache } from '#services/cache'

/**
 * Declare the alias type for TypeScript
 */
declare module '@adonisjs/core/types' {
  interface ContainerBindings {
    cache: Cache
  }
}

export default class CacheProvider {
  constructor(protected app: ApplicationService) {}
  
  register() {
    /**
     * Register the Cache binding as a singleton
     */
    this.app.container.singleton(Cache, async (resolver) => {
      const store = redis.connection()
      const config = this.app.config.get<CacheConfig>('cache')
      return new Cache(store, config)
    })
    
    /**
     * Create an alias so we can reference Cache by the string 'cache'
     */
    this.app.container.alias('cache', Cache)
  }
}

现在你可以使用字符串别名来请求缓存。TypeScript 会知道 app.container.make('cache') 返回一个 Cache 实例,从而为你提供完整的自动补全和类型检查。

import app from '@adonisjs/core/services/app'

/**
 * Request the cache using the string alias instead of the class
 */
const cache = await app.container.make('cache')

绑定已有的值

有时你已经拥有一个实例,并希望将它直接注册到容器中,而不是提供工厂函数。bindValue 方法会将一个已有的值绑定到类构造函数或别名上,此后每次请求时容器都会返回那个确切的值。

providers/app_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import { Config } from '#services/config'

export default class AppProvider {
  constructor(protected app: ApplicationService) {}

  register() {
    /**
     * Create the instance ourselves with specific configuration
     */
    const config = new Config({
      environment: 'production',
      debug: false,
    })

    /**
     * Bind the existing instance directly.
     * Every request for Config returns this exact object.
     */
    this.app.container.bindValue(Config, config)
  }
}

与接受工厂函数的 bindsingleton 不同,bindValue 接受实例本身。当你需要注册那些在容器外部创建的对象时(例如在启动时解析的配置对象,或从外部库接收的实例),这非常有用。

bindValue 方法在请求作用域的 resolver 上也可用,这正是 HttpContextLogger 等请求特定实例在 HTTP 请求期间被注册的方式。示例请参阅 HTTP 请求期间的依赖注入

HTTP 请求期间的依赖注入

HttpContext 对象为每个 HTTP 请求接收一个隔离的容器 resolver。这允许你注册仅在该特定请求期间存在的单例实例,这对于请求作用域的服务(如日志记录器或用户会话)非常有用。

这些请求作用域的 bindings 应在 app/middleware/container_bindings_middleware.ts 文件中注册,该文件在新的 AdonisJS 应用中会被预先创建并自动注册。

app/middleware/container_bindings_middleware.ts
import { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { Logger } from '@adonisjs/core/logger'

export default class ContainerBindingsMiddleware {
  handle(ctx: HttpContext, next: NextFn) {
    /**
     * Register the HttpContext itself as a binding.
     * Any class that type-hints HttpContext will receive
     * this exact context instance for the current request.
     */
    ctx.containerResolver.bindValue(HttpContext, ctx)
    
    /**
     * Register the request-specific logger.
     * All classes resolved during this request that depend
     * on Logger will receive this logger instance.
     */
    ctx.containerResolver.bindValue(Logger, ctx.logger)
    
    return next()
  }
}

在当前 HTTP 请求的整个过程中,任何将 HttpContextLogger 作为依赖进行类型标注的类,都会接收到此处注册的确切实例。这确保了请求特定数据的一致性与正确的作用域。

将抽象类用作接口

由于 TypeScript 接口在运行时会被移除,你无法使用它们来对依赖进行类型标注。不过,你可以使用抽象类来实现相同的多态行为。这使你能够实现适配器设计模式,即多个实现都遵循同一个共同的契约。

让我们构建一个可以与不同支付提供商配合工作的支付系统,同时保持业务逻辑与具体提供商无关。

定义契约

首先,创建一个抽象类,定义所有支付提供商必须实现的契约。它充当你的接口,但在运行时仍然可用,供容器用作注入令牌。

app/services/payment_service.ts
/**
 * Abstract class acts as the interface.
 * Different payment providers will implement this.
 */
export default abstract class PaymentService {
  abstract charge(amount: number): Promise<void>
  abstract refund(amount: number): Promise<void>
}

创建具体实现

现在为不同的支付提供商创建具体实现。每个提供商都用各自的特定逻辑来实现抽象类的方法,但它们都遵循相同的契约。

app/services/stripe_provider.ts
import PaymentService from './payment_service.js'

export default class StripeProvider implements PaymentService {
  async charge(amount: number) {
    console.log(`Charging ${amount} via Stripe`)
  }

  async refund(amount: number) {
    console.log(`Refunding ${amount} via Stripe`)
  }
}
app/services/paypal_provider.ts
import PaymentService from './payment_service.js'

export default class PaypalProvider implements PaymentService {
  async charge(amount: number) {
    console.log(`Charging ${amount} via PayPal`)
  }

  async refund(amount: number) {
    console.log(`Refunding ${amount} via PayPal`)
  }
}

请注意,两个实现都遵循完全相同的契约。你的应用代码可以依赖 PaymentService,而无需知道正在使用哪个具体的提供商。

配置使用哪个实现

注册一个 binding,告诉容器当有人请求抽象的 PaymentService 时应注入哪个具体实现。这里就是你决定应用使用 Stripe、PayPal 还是其他提供商的地方。

providers/app_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import PaymentService from '#services/payment_service'
import StripeProvider from '#services/stripe_provider'

export default class AppProvider {
  constructor(protected app: ApplicationService) {}

  register() {
    /**
     * Bind the abstract PaymentService to the concrete StripeProvider.
     * Any class that type-hints PaymentService will receive StripeProvider.
     */
    this.app.container.bind(PaymentService, () => {
      return new StripeProvider()
    })
  }
}

在代码中使用抽象

现在你的业务逻辑可以依赖抽象的 PaymentService,而不会与任何特定提供商耦合。

app/controllers/checkout_controller.ts
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import PaymentService from '#services/payment_service'

export default class CheckoutController {
  /**
   * Type-hint the abstract PaymentService.
   * The container injects StripeProvider (based on our binding).
   */
  @inject()
  async store({ request }: HttpContext, paymentService: PaymentService) {
    const amount = request.input('amount')
    
    /**
     * We call methods on PaymentService, but the actual
     * implementation is StripeProvider. Our code doesn't
     * know or care which provider is used.
     */
    await paymentService.charge(amount)
    
    return { success: true }
  }
}

这一模式的强大之处在于其灵活性。要从 Stripe 切换到 PayPal,你只需将 AppProvider 中的 binding 从 new StripeProvider() 改为 new PaypalProvider()。你的控制器和所有其他业务逻辑都保持完全不变。这种分离使你的代码更易于维护和测试。

上下文依赖

上下文依赖允许你根据请求方来注入同一个抽象类的不同实现。当应用的不同部分需要同一服务的不同配置或实现时,这非常有用。

在上一节 PaymentService 示例的基础上,设想你的应用不同部分有不同的业务需求。用户订阅应通过 Stripe 处理(利用其周期性计费功能),而一次性商品购买则应通过 PayPal(以支持更广泛的付款方式)。

设置需求不同的服务

让我们创建两个都需要 PaymentService 的服务,但它们应根据各自特定的业务需求使用不同的支付提供商。

app/services/subscription_service.ts
import { inject } from '@adonisjs/core'
import PaymentService from '#services/payment_service'

@inject()
export default class SubscriptionService {
  /**
   * SubscriptionService will receive a PaymentService instance
   * configured for Stripe (for recurring billing)
   */
  constructor(protected paymentService: PaymentService) {}
  
  async createSubscription(userId: number, plan: string) {
    await this.paymentService.charge(999)
  }
}
app/services/order_service.ts
import { inject } from '@adonisjs/core'
import PaymentService from '#services/payment_service'

@inject()
export default class OrderService {
  /**
   * OrderService will receive a PaymentService instance
   * configured for PayPal (for one-time purchases)
   */
  constructor(protected paymentService: PaymentService) {}
  
  async createOrder(userId: number, items: any[]) {
    await this.paymentService.charge(2499)
  }
}

两个服务都对 PaymentService 进行了类型标注,但它们需要不同的实现。如果没有上下文依赖,你只能全局绑定一个提供商,从而迫使两个服务使用相同的实现。

注册上下文 bindings

在服务提供者中使用 when().asksFor().provide() API 注册上下文依赖。这会告诉容器:当某个特定类请求某个依赖时,提供某个特定实现。

providers/app_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import PaymentService from '#services/payment_service'
import StripeProvider from '#services/stripe_provider'
import PaypalProvider from '#services/paypal_provider'
import SubscriptionService from '#services/subscription_service'
import OrderService from '#services/order_service'

export default class AppProvider {
  constructor(protected app: ApplicationService) {}

  register() {
    /**
     * When SubscriptionService asks for PaymentService,
     * provide the Stripe implementation (for recurring billing)
     */
    this.app.container
      .when(SubscriptionService)
      .asksFor(PaymentService)
      .provide(() => {
        return new StripeProvider()
      })

    /**
     * When OrderService asks for PaymentService,
     * provide the PayPal implementation (for one-time purchases)
     */
    this.app.container
      .when(OrderService)
      .asksFor(PaymentService)
      .provide(() => {
        return new PaypalProvider()
      })
  }
}

现在 SubscriptionService 在被构造时会自动接收 StripeProvider,而 OrderService 会接收 PaypalProvider,全部都通过同一个 PaymentService 类型标注实现。这让你的服务代码保持整洁并专注于业务逻辑,同时又能根据上下文对注入哪个依赖进行细粒度的控制。

上下文 binding 模式与上一节的适配器模式结合使用时尤为强大。每个服务对它所使用的支付提供商保持完全无感知,却又都能获得其特定用例所需的确切实现。

解析优先级

当容器解析依赖时,它会按特定顺序检查多个来源。理解这个优先级有助于你预测哪个实现会被注入,并调试意外行为。

容器按以下顺序解析依赖,在第一个匹配处停止:

优先级来源说明
1Swaps通过 container.swap() 注册的测试覆盖
2上下文 bindings通过 when().asksFor().provide() 注册的 bindings
3Resolver 值绑定到某个作用域 resolver 的值(例如请求作用域的 bindings)
4容器值通过 container.bindValue() 注册的值
5容器 bindings通过 container.bind()container.singleton() 注册的工厂函数
6自动构造容器使用 @inject() 自行构造类

Swaps 拥有最高优先级,因为它们是为测试而设计的。当你替换一个依赖时,无论存在任何其他 bindings,你都希望使用伪实现。这确保你的测试保持隔离和可预测。

上下文 bindings 排在其后,因为它们代表有意的、针对特定上下文的覆盖。当你明确地说“当 ClassA 请求 ServiceB 时,提供这个特定实现”时,该意图应覆盖通用 bindings。

如果没有 swap 或上下文 binding 匹配,容器会检查绑定到当前 resolver 作用域的值。在 HTTP 请求期间,HttpContext 和请求作用域的 Logger 就是以这种方式绑定的,确保每个请求都获得它自己的实例。

接下来检查容器级别的值和 bindings。这些是你在服务提供者中注册的应用范围内的单例和工厂。

最后,如果完全不存在 binding,容器会尝试自动构造。它使用 TypeScript 的反射元数据来识别用 @inject() 标记的构造函数依赖,并递归地解析它们。

这个优先级顺序意味着你可以分层配置依赖:在提供者中定义通用 bindings,为特定类按上下文覆盖它们,并在测试期间完全替换它们——所有这些都无需修改原始代码。

在测试期间替换依赖

容器提供了一个简单直接的 API,用于在测试期间将依赖替换为伪实现。这让你能够隔离地测试代码,而无需访问真实的外部服务。

container.swap 方法会用一个临时实现替换某个 binding,而 container.restore 会将其还原为原始实现。在替换期间,代码库中任何对被替换类进行类型标注的部分都会接收到伪实现。

tests/functional/users/list.spec.ts
import { test } from '@japa/runner'
import app from '@adonisjs/core/services/app'
import UserService from '#services/user_service'

test('get all users', async ({ client, cleanup }) => {
  /**
   * Create a fake implementation that extends the real service
   */
  class FakeUserService extends UserService {
    /**
     * Override the all() method to return fake data
     * instead of querying the database
     */
    all() {
      return [
        { id: 1, username: 'virk', email: '[email protected]' },
        { id: 2, username: 'romain', email: '[email protected]' }
      ]
    }
  }
  
  /**
   * Swap UserService with our fake implementation.
   * Any code that resolves UserService during this test
   * will receive FakeUserService instead.
   */
  app.container.swap(UserService, () => {
    return new FakeUserService()
  })
  
  /**
   * Restore the original binding after the test completes.
   * The cleanup hook ensures this runs even if the test fails.
   */
  cleanup(() => app.container.restore(UserService))
  
  /**
   * Make the HTTP request. The controller will receive
   * FakeUserService, which returns our fake data.
   */
  const response = await client.get('/users')
  
  response.assertStatus(200)
  response.assertBodyContains({ 
    users: [
      { username: 'virk' },
      { username: 'romain' }
    ]
  })
})

在测试依赖外部 API、支付网关、邮件服务或任何其他你不希望在自动化测试期间交互的资源的代码时,替换尤其有价值。伪实现可以模拟各种场景(成功、失败、边界情况),而无需真实的基础设施。

容器事件

容器在解析 bindings 时会发出事件,允许你观察并对依赖解析做出响应。这对于调试、监控或实现横切关注点很有用。

容器只发出一种事件类型:container_binding:resolved。每当容器成功解析一个类实例时,无论是通过自动解析、bindings 还是单例,都会触发该事件。

你可以使用应用的事件发射器监听此事件。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'

emitter.on('container_binding:resolved', (event) => {
  /**
   * event.binding contains the class constructor or string alias
   * that was resolved
   */
  console.log('Resolved binding:', event.binding)
  
  /**
   * event.value contains the actual instance that was created
   */
  console.log('Instance:', event.value)
})

事件对象提供两个属性。首先,event.binding 包含被解析的 binding 键(类构造函数或字符串别名)。其次,event.value 包含容器创建并返回的实际实例。

该事件会为每一次解析触发,包括嵌套依赖。例如,如果容器解析依赖于 LoggerServiceUserService,你将收到两个事件:一个针对 LoggerService,一个针对 UserService

另见