0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソースコードで理解するNestJS④: DependenciesScanner, NestContainer

Last updated at Posted at 2024-08-25

前回

前回までで、NestJSのルーティングがどのように実現されているのかを見ました。@Controller@Getといったデコレータによって付与されたメタデータをもとに、RoutesResolverRouterExplorerの働きによってルートパスとハンドラがHTTPサーバーに登録されます。

今回はNestJSのもう一つの重要トピックである依存性注入 (DI)を見ていきます。以前の記事で見た通り、鍵となるのはDependenciesScanner#insertModuleです。

@Injectableのおさらい

NestJSにおけるDIでは@Injectableデコレータが重要です。以前の記事で見た通り、このデコレータは以下のようにReflect.defineMetadataで修飾先のクラスのコンストラクタ関数にメタデータを付与する役割を持っています。

export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

二つ目のSCOPE_OPTIONS_METADATA@Injectableで指定するinjection scopeを表します。

DIを追う上ではあまり重要ではないので無視して、INJECTABLE_WATERMARKを追っていくことにします。リポジトリを検索すると、DependenciesScannerクラスで定義されたisInjectableメソッドに行き当たります。このメソッドはモジュールクラスのクラスコンストラクタを受け取り、それが@Injectable修飾されているかを返します。

isInjectableの利用元であるinsertModuleは以前の記事でも登場しました。

  public async insertModule(
    moduleDefinition: any,
    scope: Type<unknown>[],
  ): Promise<
    | {
        moduleRef: Module;
        inserted: boolean;
      }
    | undefined
  > {
    const moduleToAdd = this.isForwardReference(moduleDefinition)
      ? moduleDefinition.forwardRef()
      : moduleDefinition;

    if (
      this.isInjectable(moduleToAdd) ||
      this.isController(moduleToAdd) ||
      this.isExceptionFilter(moduleToAdd)
    ) {
      throw new InvalidClassModuleException(moduleDefinition, scope);
    }

    return this.container.addModule(moduleToAdd, scope);
  }

insertModuleは、モジュールのクラスコンストラクタであるmoduleDefinitionをDIコンテナであるNestConteinerのインスタンスであるthis.containerに登録し、モジュールクラスをインスタンス化して返します。

insertModuleがどのように使われているかを通じてDependenciesScannerの全体像を理解しましょう。

DependenciesScanner

DependenciesScannerは、Nestアプリケーションの依存関係を解決し、DIコンテナにそれを登録する役割を果たします。NestFactoryクラスのinitialzeで利用され、アプリケーションのエントリーポイントになるモジュール(AppModule)を起点にモジュールを再起的にスキャンします。


  private async initialize(
    module: any, // エントリーポイントになるAppModuleのコンストラクタ
    container: NestContainer,
    graphInspector: GraphInspector,
    config = new ApplicationConfig(),
    options: NestApplicationContextOptions = {},
    httpServer: HttpServer = null,
  ) {
        // 省略
    
          await dependenciesScanner.scan(module);
          await instanceLoader.createInstancesOfDependencies();
          dependenciesScanner.applyApplicationProviders();

        //省略

ここで、scanがメインの処理であるAppModuleからの再起的な依存解決を担当し、applyApplicationProvidersはグローバルに扱われるプロバイダーのうちEnhancer(Intercepter, Pipe, Guard, Filter)であるものの登録を担当しています。applyApplicationProvidersはひとまず置いておいて、scanを見ていきましょう。

dependenciesScanner.scan(module)

実装を見て見ましょう。各処理の責務をコメントで補足しています。


public async scan(
    module: Type<any>, // AppModuleのクラスコンストラクタ
    options?: { overrides?: ModuleOverride[] },
  ) {
    // InternalCoreModuleFactory.createで生成されるモジュールに対するscanForModules
    await this.registerCoreModule(options?.overrides);

    // メイン処理 (再起呼び出しされる)
    await this.scanForModules({
      moduleDefinition: module,
      overrides: options?.overrides,
    });

    // Moduleの解決結果をもとに、各モジュールでImport, Provider, Controller, Exportを反映
    await this.scanModulesForDependencies();

    // ルートモジュールから出発し、Importsを辿ってモジュール間の「遠さ」を計算して登録
    // 初期化順序や依存関係解決の最適化に利用
    this.calculateModulesDistance();

    // グローバルなEnhancerのスコープ情報をコントローラ, プロバイダーのメタデータに追加する
    this.addScopedEnhancersMetadata();

    // 各モジュールにグローバルスコープのプロバイダーをバインドし、利用可能にする
    this.container.bindGlobalScope();
  }

scanForModules

メイン処理であるscanForModulesを見ていきます。理解優先で一部単純化してあります。重要なのは冒頭のthis.insertOrOverrideModuleで、これは先述のinsertModuleを実行し、DIコンテナであるNestContainerへのクラスの登録とインスタンス化を行います。this.insertOrOverrideModule以降は、import辿って再起的にこれを実行するためのもろもろです。引数のscopeはAppModuleから現在のモジュールに至るまでの参照解決経路で登場する全てのモジュールのクラスコンストラクタを配列化したものになっています。

    public async scanForModules({
    moduleDefinition,   // 現在着目しているモジュールのクラスコンストラクタ
    scope = [],
    ctxRegistry = [],   // クラスコンストラクタ配列 (処理済みモジュール)
    overrides = [],
  }: ModulesScanParameters): Promise<Module[]> {

    // モジュールを追加、ないし上書きする
    // Overrideのことを一旦無視すると、`insertModule`を実行しているだけ
    const { moduleRef: moduleInstance, inserted: moduleInserted } =
      (await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
      {};

    // 無駄な処理をスキップするために処理済みのものを管理
    ctxRegistry.push(moduleDefinition);

    // 着目しているモジュールの@Moduleの引数でimport指定されているモジュール群を取得
    const modules = this.reflectMetadata(
          MODULE_METADATA.IMPORTS,
          moduleDefinition as Type<any>,
        );

    // 現在のモジュールより下位にあるモジュールのインスタンス全体
    let registeredModuleRefs = [];

    // importしているモジュールに関してループ
    for (const [index, innerModule] of modules.entries()) {

      // 処理済みモジュールについてはスキップ
      if (ctxRegistry.includes(innerModule)) {
        continue;
      }

      // モジュール探索を再起実行
      const moduleRefs = await this.scanForModules({
        moduleDefinition: innerModule,
        scope: [].concat(scope, moduleDefinition),
        ctxRegistry,
        overrides,
        lazy,
      });
      
      registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
    }
    
    if (!moduleInstance) {
      return registeredModuleRefs;
    }

    // グローバルスコープのプロバイダーをバインド
    if (moduleInserted) {
      this.container.bindGlobalsToImports(moduleInstance);
    }

    // 現在のモジュールと、それが依存するモジュールに関する再起処理結果を結合し、インスタンス配列を返す
    return [moduleInstance].concat(registeredModuleRefs);
  }

まとめると、scanはAppModuleから出発し、@Moduleの引数で指定されたimport情報のメタデータに基づいて再起的に参照解決を行い、NestContainerを更新します。次にこのNestContainerを見ていきます。

NestContainer

NestJS アプリケーション全体のモジュール構成と依存関係解決の中核を担っています。モジュールとプロバイダーの登録や解決、グローバルスコープの管理、動的モジュールの取り扱い、HTTPアダプターの設定などの重要な機能を実装しています。

DependenciesScannerから実行されるaddModuleは以下で定義されます。

  public async addModule(
    metatype: ModuleMetatype,
    scope: ModuleScope,
  ){
    // DIコンテナ内でモジュールを一意に識別するトークン (ハッシュ文字列)を生成する。
    // scopeやDynamicModuleの設定によって1つのモジュールクラスに対応した複数のモジュールが存在しうる
    const { type, dynamicMetadata, token } =
      await this.moduleCompiler.compile(metatype);

    // 省略

    return {
      moduleRef: await this.setModule(
        {
          token,
          type,
          dynamicMetadata,
        },
        scope,
      ),
      inserted: true,
    };
  }

モジュールの登録処理であるsetModuleは以下で、ここでモジュールをインスタンス化し、一意な識別子をキーとして、MapであるModulesContainerにそのインスタンスを追加しています。

    
    private async setModule(
        { token, dynamicMetadata, type }: ModuleFactory, 
        scope: ModuleScope,
    ){
        const moduleRef = new Module(type, this);
        moduleRef.token = token;
        moduleRef.initOnPreview = this.shouldInitOnPreview(type);
    
        // ModulesContainerにトークンをキーとしてモジュールインスタンスを登録
        this.modules.set(token, moduleRef);

        // type: モジュールのクラスコンストラクタ
        const updatedScope = [].concat(scope, type);

        // 動的モジュールによって導入された依存関係の処理。
        // 動的モジュールのメタデータの保存と、コンテナへのモジュール追加 (addModule)
        await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);
        
        if (this.isGlobalModule(type, dynamicMetadata)) {
          moduleRef.isGlobal = true;
          this.addGlobalModule(moduleRef);
        }
        
        return moduleRef;
    }

コメントで書いた通り、静的モジュールと動的モジュールは異なる取り扱いを受けます。静的モジュールの依存関係はscanForModules処理されますが、動的モジュールは上記addDynamicMetadata内で別途処理されています。

まとめ

今回は、NestJSのもう一つの重要な機能である依存性注入 (DI) について解説しました。特に、DependenciesScannerクラスがこのプロセスの中核を担っており、scanForModulesメソッドを通じてAppModuleから再帰的に依存関係をスキャンし、DIコンテナであるNestContainerを更新します。このスキャンと登録のプロセスにより、NestJSはモジュール間の依存関係を把握することができます。

次回は、NestContainer#setModuleで各モジュールのインスタンスが生成されたあと、どのように依存関係が解決され、各種プロバイダーのインスタンス化と依存性注入が行われるかを見ることにします。鍵になるのは、NestFactoryinitializeで登場したInstanceLoader#applyApplicationProvidersです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?