What is it, naokirin?

Micronautで簡単なHTTPサーバーを実装してみる

記事を書いたばかりですが、Micronautに触った続きとして、もう少し実際の実装部分を掘り下げたところを書いてみます。

前回の記事は以下です。Micronautの導入やIntelliJ IDEAでの開発方法、Micronautによるサーバーの実行方法はこちらを参考にしてください。

naokirin.hatenablog.com

前回同様、今回も Kotlin で書いています。

簡単なレスポンスを返すHTTPサーバーとテストコード

まずは簡単なレスポンスを返すHTTPサーバーを実装してみます。

これはほぼ前回の記事で達成していたことなので Controller の生成コマンドとコードだけ示します。

$ mn create-controller helloController
// src/main/kotlin/hello/world/HelloController.kt

package hello.world

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/")
class HelloController {

    @Get("/hello/{name}")
    fun hello(name: String): String {
        return "Hello, $name"
    }
}

さて、これで十分ですが、テストコードも書いてみましょう。テストコード用のファイルはすでにコマンド実行時に生成されています。

Kotlin の場合は Spek を利用する形式になっています。もちろん好みに応じて変更もできますが、今回は Spek をそのまま利用してみましょう。

// src/test/kotlin/hello/world/HelloControllerTest.kt

package hello.world

import io.micronaut.context.ApplicationContext
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.http.HttpStatus
import org.jetbrains.spek.api.Spek
import org.jetbrains.spek.api.dsl.describe
import org.jetbrains.spek.api.dsl.it
import org.junit.Assert.*

object HelloControllerTest : Spek({

    // テスト用のサーバー、クライアント
    var server: EmbeddedServer? = null
    var client: HttpClient? = null

    describe("/hello") {
        beforeEachTest {
            server = ApplicationContext.run(EmbeddedServer::class.java)
            client = server?.applicationContext
                    ?.createBean(HttpClient::class.java, server?.url)
        }

        afterEachTest {
            server?.stop()
            client?.stop()
        }

        it ("should return Hello, name") {
            val body = client?.toBlocking()?.retrieve("/hello/Bob")
            assertNotNull(body)
            assertEquals("Hello, Bob", body)
        }

        it ("should occur an error without name") {
            try {
                client?.toBlocking()?.retrieve("/hello")
            }
            catch (e: HttpClientResponseException) {
                assertEquals(HttpStatus.NOT_FOUND, e.status)
                assertEquals("Page Not Found", e.message)
            }
        }
    }
})

Spek はいわゆる RSpec 等の BDDと呼ばれるような形式のフレームワークとなっており、 group, describe, it などを利用します。

細かいところは公式ドキュメントに譲りますが、内部のアサーションについては Spek では提供されていないので、JUnit や kotlin-test などを利用することになります。

spekframework.org

IntelliJ IDEA を利用している場合、このテストは一般的なテスト実行方法と同様に実行できます。

最も簡単な実行方法は、testディレクトリを Project のビューで右クリックして、 Run を実行することです。

f:id:naokirin:20190302154349p:plain:w600

サーバーをいちいち実行することなく、非常に簡単にテストで確認できるので良いですね。

少し複雑な API に対応してみる

ここまでは簡単なURIでのデータ取得をしましたが、実際にパスに渡せる値が数字に制限されていたり、文字列の種類を制限したりしたいこともあるかと思います。

先程 @Get で指定した文字列 /hello/{name}{name} の部分で受け付けるフォーマットを指定することができます。

指定例 挙動 呼び出し例
/hello/{name:2} 2文字まで /hello/bo
/hello{/name} name部分がなくても良い /hello , /hello/bob
/hello{/name:[a-zA-Z]+} 正規表現に従う /hello/abc
/hello{?pages,max} クエリパラメータ /hello?pages=2&max=10
/hello{/path:.*}{.ext} 正規表現+拡張子 /hello/foo/bar.txt

それでは使ってみましょう。

今回は、 name に指定できるのを [a-zA-Z] だけにして、クエリパラメータとして時刻を受け付けて、それに合わせて返す挨拶を変えてみましょう。

    // src/main/kotlin/hello/world/HelloController.kt の一部

    @Get("/hello/{name:[a-zA-Z]+}{?hour}")
    fun hello(name: String, hour: Int?) = when {
        hour == null -> "Hello, $name"
        4 <= hour && hour < 12 -> "Good morning, $name"
        12 <= hour && hour < 18 -> "Good afternoon, $name"
        18 <= hour && hour < 21 -> "Good evening, $name"
        21 <= hour && hour < 2 -> "Good night, $name"
        else -> "Hello, $name"
    }

それでは、実行してみましょう。

http://localhost:8080/hello/Bob?hour=12 にアクセスすると、 Good afternoon, Bob と返ってきます。

他にもパラメータを変えてみたりすることで、返ってくる文字列が変化することを確認できると思います。

ちなみに、 name 部分に [a-zA-Z] の範囲にない文字列(数字など)を渡すと Page Not Found となります。

GET 以外も書いてみよう

ここまでは GETリクエストばかり書いていましたが、HTTPリクエストにはGET以外のメソッドもあります。

以下に挙げる、8つのメソッドに対応するアノテーションが存在します。

@Get , @Post , @Delete , @Put , @Patch , @Head , @Options , @Trace

今回は @Post を使ってみましょう。

    // src/main/kotlin/hello/world/HelloController.kt の一部

    @Consumes(MediaType.TEXT_PLAIN)
    @Post("/hello")
    fun postHello(@Body body: String): HttpResponse<Map<String, String>> {
        return HttpResponse.ok(mapOf("msg" to "OK", "body" to body))
    }

それでは POST できるか試してみましょう。

$ curl -X POST -H "Content-Type: text/plain" -d 'My name is Bob.' http://localhost:8080/hello

結果としては以下のJSONが返ってきます。

{"msg":"OK","body":"My name is Bob."}%

正直、 引数の @Body と 戻り値の HttpResponse 以外はそれほど @Get との差はありません。

では @Consumes はなんでしょう。

勘の良い方は渡しているものから気がつくかもしれませんが、Post時のメディアタイプとして、 text/plain を受け付けるようにしています。何も指定していない場合は、 application/json がデフォルトになります。

ちなみに、 レスポンスのメディアタイプは @Produces で指定できます。

また @Consumes , @Produces の名前の通り、許可するメディアタイプを複数指定できます。

まとめ

ここまでで、ざっくりとHTTPサーバー実装に必要な情報をまとめつつ書いてきました。

本格的なサーバーを実装する場合には、バックエンドのDBとの連携や妥当性検証、セキュリティのための対応などもあります。

しかしながら、ここまで書いてきたような非常に簡単な実装のみでサーバーを実装できるので、ちょっとしたAPIサーバーの実装などはサクッとできるかもしれません。

より現実的なサーバーとなるとまだまだ必要な知識は増えますが、導入から実際に動くものを作るまでのわかりやすさは良いのではないでしょうか。