前回のまとめなど
前回の記事では、@Controller
デコレータの実装から、NestJSの各種組み込みデコレータが対象のクラスやメソッドにReflectionによるメタデータを付与することを責務としていることを見ました。
デコレータがメタデータの付与に特化しているため、実際のルーティングやDIといった機能は、これらのメタデータを利用するコンポーネントが実現します。今回の記事では、特に@Controller
で付与されたメタデータがどのように利用され、ルーティングを実現しているのかを見ていきます。
該当するソースコード
@Controller
のおさらい
前回見た@Controller
のソースコードのおさらいです。
export function Controller(
prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {
// ------------------------------------------------------
// 省略: 引数に応じたpath, host, scopeOptions, versionOptionsの設定
// ------------------------------------------------------
return (target: object) => {
Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
Reflect.defineMetadata(PATH_METADATA, path, target);
Reflect.defineMetadata(HOST_METADATA, host, target);
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
Reflect.defineMetadata(VERSION_METADATA, versionOptions, target);
};
}
reflect-metadataの Reflect.defineMetadata
を用いて、
CONTROLLER_WATERMARK
PATH_METADATA
HOST_METADATA
SCOPE_OPTIONS_METADATA
VERSION_METADATA
という 5つのメタデータを対象のクラスに付与しています。これらのメタデータキーのそれぞれについて見ていくことにします。なお、メタデータの全体は以下で定義されています。
PATH_METADATA
最初に最も重要なものを見ることにします。本メタデータはなんとなく予想が着くでしょう。ルートパスを保持しています。@Controller
の引数で指定されたpath
が直接格納されることからして、ルーティングを理解する上で最も重要なのは間違いなくこれです。
PATH_METADATA
メタデータはコントローラーやそのメソッドに設定されたルートパスを表す
本メタデータの利用者(Reflect.getMetadata
の実行箇所)として重要なのはRouterExplorer
.extractRouterPath
で、設定されたメタデータからpathを取得し、返却しています。そして、このextractRouterPath
を利用しているのはRoutesResolver
.registerRouters
で、これがNestJSにおけるルーティング処理の中心となっているようです。
public registerRouters(
routes: Map<string | symbol | Function, InstanceWrapper<Controller>>,
moduleName: string,
globalPrefix: string,
modulePath: string,
applicationRef: HttpServer,
) {
// ルートマップ内の各コントローラをループ処理
routes.forEach(instanceWrapper => {
//省略
// **************************************************
// 2. コントローラに関連するルートパスを抽出
const routerPaths = this.routerExplorer.extractRouterPath(
metatype as Type<any>,
);
// **************************************************
// 省略
// 各ルートパスを処理
routerPaths.forEach(path => {
// グローバルプレフィックスやモジュールパスを使用して最終的なパスを生成
const pathsToLog = this.routePathFactory.create({
ctrlPath: path,
modulePath,
globalPrefix,
});
// 省略
const routePathMetadata: RoutePathMetadata = {
ctrlPath: path,
modulePath,
globalPrefix,
controllerVersion,
versioningOptions,
};
// ルートの探索と登録を実行
this.routerExplorer.explore(
instanceWrapper,
moduleName,
applicationRef,
host,
routePathMetadata,
);
});
});
}
ルーティングの核心に迫るため、this.routerExplorer.explore
に潜ってみましょう。ここで各ルートをHTTPサーバーにバインドし、実際のリクエストに対して適切なコントローラメソッドが呼び出されるように処理されているはずです。ここで、ルーターとはHttpServer
インターフェースを持つもの、例えば express.Router
などになります。
ルーターへの登録
this.routerExplorer.explore
の処理は以下のように進みます。
-
this.routerExplorer.explore
コントローラのインスタンスのメタデータを調べ、関連するルートパス情報を組み立てる - ->
RouterExplorer#applyPathsToRouterProxy
ルートパスのそれぞれに対して以下を実行する - ->
RouterExplorer#applyCallbackToRouter
与えられたルートパスに対してリクエストハンドラをルーターに登録
そして、applyCallbackToRouter
は、各ルート定義に対して以下の処理を行います。
routerMethodRef の取得
RouterMethodFactory
を使用して、指定された HTTP メソッドに対応するルーティング関数を取得し、router (HttpServerインターフェース... 実態としてはexpress.Routerなど) にバインドします。これにより、後続の処理で routerMethodRef
が正しい HTTP メソッドに基づいて呼び出されます。
const routerMethodRef = this.routerMethodFactory
.get(router, requestMethod)
.bind(router);
プロキシハンドラの作成
createCallbackProxy
によりルートハンドラをラップし、実行時コンテキストやエラーハンドリングを含む、「リクエストに対して実際に実行されるメソッド = プロキシ」を作成します。また、ここでスコープに関する条件分岐が入っています。
const proxy = isRequestScoped
? this.createRequestScopedHandler(
instanceWrapper,
requestMethod,
this.container.getModuleByKey(moduleKey),
moduleKey,
methodName,
)
: this.createCallbackProxy(
instance,
targetCallback,
methodName,
moduleKey,
requestMethod,
);
ここでは立ち入りませんが、実際に適用されるメソッドはさらに、いくつかのフィルタが適用されたものになります。
host
に基づいたフィルタ
routeHandler = this.applyHostFilter(host, proxy);
バージョンに基づいたフィルタ
routeHandler = this.applyVersionFilter(
router,
routePathMetadata,
routeHandler,
);
先述のメタデータ一覧を思い出してもらうと、
HOST_METADATA
-
VERSION_METADATA
の二つがありました。そう、これらのメタデータはルーティングの文脈においてこのapplyHostFilter
,applyVersionFilter
で使用されています。
HOST_METADATA
メタデータは特定のルートのフィルタ条件として適用されるホスト名またはホストパターンを表す
VERSION_METADATA
メタデータはコントローラーやメソッドに設定されたAPIバージョンを表す
ルートパスとハンドラの登録:
最後に、routerMethodRef を使用して、各ルートパスに対してリクエストハンドラをルーターに登録します。
routerMethodRef(path, routeHandler);
こうして、ルートパスへの各メソッド(Get, Postなど)ごとのリクエストが、対応したルートハンドラ(実際はそのプロキシ)と紐づけられます。
その他メタデータ
ここまでで、PATH_METADATA
を中心に、HOST_METADATA
,VERSION_METADATA
も含めてメタデータがどのように利用されるかを見てきました。他はおまけのようなものです。ついでなのでさらっと見ていきます。
CONTROLLER_WATERMARK
リポジトリ内で検索してヒットするのは4件のみで、そのうち3件は
- /packages/common/constant.tsでの定義 (上記)
- controller.decorator.tsでの
Reflect.defineMetadata
(前回見たもの) - ユニットテストファイル (.spec.ts)
なので、実質1箇所のみで使用されています。
/**
* @param metatype
* @returns `true` if `metatype` is annotated with the `@Controller()` decorator.
*/
private isController(metatype: Type<any>): boolean {
return !!Reflect.getMetadata(CONTROLLER_WATERMARK, metatype);
}
このprivateメソッドを利用しているのは1箇所"insertModule`というメソッドであり、その中の例外判定の文脈です。
public async insertModule(
moduleDefinition: any,
scope: Type<unknown>[],
){
//// 略
if (
this.isInjectable(moduleToAdd) ||
this.isController(moduleToAdd) ||
this.isExceptionFilter(moduleToAdd)
) {
throw new InvalidClassModuleException(moduleDefinition, scope);
}
//// 略
}
ModuleにModuleとして不正なデコレータが付与されている場合に例外とするために使用されており、このことから、CONTROLLER_WATERMARK
(より一般にXXXXX_WATERMARK
)というメタデータはデコレータの種別を判定するためのものであることがわかります。
insertModule
はNestJSのDIで重要な役割を果たしていると思われますが、ここは別の機会で取り上げることとします。
CONTROLLER_WATERMARK
メタデータは、デコレータの種別を表す
SCOPE_OPTIONS_METADATA
こちらについては別の機会に改めて取り上げることとします。結論だけ書いておきましょう。
SCOPE_OPTIONS_METADATA
メタデータは、特定のプロバイダーやコントローラーに適用されるスコープを表す
次回
今回は、@Controller
によって指定されたメタデータがどのようにNestJSのリクエストルーティングで使用されているのかを見ました。次回は、RoutesResolver
, RouterExplorer
を掘り下げ、そしてNestJSのルーティングの全体像を明らかにしていくことにします。