記事を書いたばかりですが、Micronautに触った続きとして、もう少し実際の実装部分を掘り下げたところを書いてみます。
前回の記事は以下です。Micronautの導入やIntelliJ IDEAでの開発方法、Micronautによるサーバーの実行方法はこちらを参考にしてください。
前回同様、今回も 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 などを利用することになります。
IntelliJ IDEA を利用している場合、このテストは一般的なテスト実行方法と同様に実行できます。
最も簡単な実行方法は、testディレクトリを Project のビューで右クリックして、 Run を実行することです。
サーバーをいちいち実行することなく、非常に簡単にテストで確認できるので良いですね。
少し複雑な 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サーバーの実装などはサクッとできるかもしれません。
より現実的なサーバーとなるとまだまだ必要な知識は増えますが、導入から実際に動くものを作るまでのわかりやすさは良いのではないでしょうか。