はじめに
ピクトリンク事業部でSREエンジニアをしている山根です。
この記事では、WebフレームワークKtorでRoutingに対して前処理を追加する方法について説明します。
ktor.io
Ktorとは
Ktorとは、Kotlinで書かれた非同期Webフレームワークです。
Ktorを使用すると、簡単にWebアプリケーションやAPIを構築できます。
以下にKtorの主な特徴を示します。
- 非同期処理: コルーチンを使用して非同期処理を簡単に実装できます。
- 軽量: 必要な機能だけを選んで使用できるため、軽量なアプリケーションを構築できます。
- 柔軟なルーティング: DSLを使用して直感的にルーティングを定義できます。
- 拡張性: プラグインを使用して機能を簡単に拡張できます。
詳細については、公式ドキュメントを参照してください。
PipelineとPhase
KtorにおけるPipelineとPhaseは、リクエスト処理の流れを管理するための重要な概念です。
また、Pluginを利用することで、Pipelineに処理を追加できます。
まとめると以下のような感じです。
- Pipeline
リクエストやレスポンスの処理を段階的に行うための仕組みです。各段階(Phase)で特定の処理を行い、次の段階に進むことができます。 - Phase
Pipelineの中の特定の処理段階を表します。KtorにはデフォルトでいくつかのPhaseが定義されていますが、独自のPhaseを追加することも可能です。 - Plugin
Pipelineに処理を追加するための仕組みです。Pluginを利用することで、Pipelineに処理を追加できます。
Routingの前処理(Plugin)を実装する
Ktorでは、Pipelineを利用してRoutingの前処理(Plugin)を実装できます。
例えば、リクエストヘッダーのチェック処理や、認証処理などをPipelineに登録することで、Routingの前処理を実装することができます。
以下は、PipelineにRoutingの前処理を実装する例です。
基底インターフェースの定義
まず、Routingの前処理の基底となるインターフェースを定義します。
import io.ktor.server.application.* import io.ktor.server.routing.* import io.ktor.util.pipeline.* /** * interceptorのPluginの基本設定を定義するインターフェース */ internal interface BaseInterceptorPluginConfig { // Plugin名 val name: String // Pluginの処理 suspend fun execute(call: ApplicationCall) } /** * Callのフェーズ前でHookするための設定 */ internal object InterceptorHook : Hook<suspend (ApplicationCall) -> Unit> { private val InterceptorPhase: PipelinePhase = PipelinePhase("Interceptor") override fun install( pipeline: ApplicationCallPipeline, handler: suspend (ApplicationCall) -> Unit ) { pipeline.insertPhaseBefore(ApplicationCallPipeline.Call, InterceptorPhase) pipeline.intercept(InterceptorPhase) { handler(call) } } } /** * InterceptorHookが発火した時に、 * 指定の処理を実行するPluginを生成する */ internal fun createInterceptorPlugin(pluginConfig: BaseInterceptorPluginConfig) = createRouteScopedPlugin( name = pluginConfig.name, ) { on(InterceptorHook) { call -> pluginConfig.execute(call) } } /** * 複数のinterceptorのPluginを対象のRoute設定に対して登録する * * @param interceptorPlugins 登録するinterceptorのPluginの可変長引数 * @param build Route設定 * @return Pluginの登録が完了したRoute */ fun Route.interceptors(vararg interceptorPlugins: RouteScopedPlugin<*>, build: Route.() -> Unit): Route { interceptorPlugins.map { install(it){} } this.build() return this }
ヘッダーをチェックするInterceptorの実装
次に、リクエストヘッダーのチェックを行うInterceptorを実装します。
import io.ktor.http.HttpHeaders.UserAgent import io.ktor.server.application.* /** * ヘッダーのチェックを行うInterceptor */ val headerCheckInterceptor = createInterceptorPlugin(HeaderCheckInterceptorPluginConfig()) private class HeaderCheckInterceptorPluginConfig : BaseInterceptorPluginConfig { override val name: String = "headerCheckInterceptorPlugin" override suspend fun execute(call: ApplicationCall) { // User-Agentが存在しない場合は例外を投げる call.request.headers.get(UserAgent) ?: throw IllegalArgumentException("User-Agent is not found") } }
RoutingにInterceptorを設定
最後に、実装したInterceptorをRoutingに設定します。
import com.example.plugins.headerCheckInterceptor import com.example.plugins.interceptors import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Application.configureRouting() { routing { get("/") { call.respondText("Hello World!") } // ヘッダーのチェックを行うInterceptorを設定 interceptors(headerCheckInterceptor) { get("/checked") { call.respondText("Checked Headers.") } } } }
上記の例では、/checkedエンドポイントにアクセスする際に、ヘッダーのチェックを行うInterceptorを設定しています。
このようにすることで、Routingの前処理をPipelineを利用して実装できます。
まとめ
KtorのPipelineに処理を追加する実装をご紹介しました。
Pipelineを利用することで、リクエスト処理の流れを柔軟に制御することができたり、認証処理をカスタマイズすることができます。
是非、KtorのPipelineを利用して、アプリケーションの処理をカスタマイズしてみてください。
おまけ: リクエストとレスポンスの処理を行うPluginの例
リクエストを受けた時、レスポンスを返す時に処理をいれるPluginの例をご紹介します。
以下は、nginxやapacheなどのリバースプロキシを経由してリクエストが来た場合に、X-Request-Idヘッダーの値をログに出力するPluginを実装します。
import io.ktor.server.application.* import org.slf4j.MDC /** * nginx や apache などのリバースプロキシを経由してリクエストが来た場合に、 * X-Request-Id ヘッダーの値をログに出力するPlugin */ val RequestIdPlugin = createApplicationPlugin(name = "RequestIdPlugin") { val key = "requestId" // リクエストが来たら、X-Request-Id ヘッダーの値を MDC に設定する onCallReceive { call -> val value = call.request.headers["X-Request-Id"] MDC.put(key, value) } // レスポンスを返す前に MDC から X-Request-Id を削除する onCallRespond { _ -> MDC.remove(key) } }
このPluginを有効にするには以下のように、installを使って設定します。
import com.example.plugins.RequestIdPlugin import io.ktor.server.application.* fun main(args: Array<String>) { io.ktor.server.netty.EngineMain.main(args) } fun Application.module() { // RequestIdPluginを登録 install(RequestIdPlugin) configureRouting() }
Application.moduleを適用するために、application.yamlに以下のように設定します。
ktor: application: modules: - com.example.ApplicationKt.module
このように、簡単にPipelineを利用してPhaseに対して前処理や後処理を実装することができます。