Kotlinの練習のため、JAX-RSによる簡単なAPIを作ってみたので、その際にやったことを記録しておきます。
サーバーサイドKotlinだとSpring Bootの記事はいっぱい出てきますが、Java EE(JAX-RS)の記事となると、結構古いものが多かったので、たいしたことはやってませんがアップしておくことにしました。
なお、ビルドはgradleで行い、Docker上のPayara Microで動かします。
はじめに
環境
今回の作業環境は以下の通りです。ちょっとバージョンが違うくらいでは問題ないとは思いますが。
- Java8 + Kotlin 1.2
- Payara Micro 4.1.2.174(Payara Micro公式Dockerイメージを使用)
- Gradle 4.5
- Docker for Mac(Docker CE 17.09.0)
- macOS Sierra
下記GitHubリポジトリにあるコードを使う場合、
- Java8
- Docker for Mac
が入っていればOKのはずです(それ以外はGradleの依存関係で持ってこれる)。
GitHubリポジトリ
今回作成したファイルは、GitHubリポジトリにアップしてありますので、適宜参照してください。
APIの仕様
GETとPOSTのAPIを1つずつ作ります。APIの中身は何でもよいのですが、製品情報を検索するAPIとしています。
いずれも、JAX-RSのAPIを作るのが目的なので、データベース等は使用せず、プログラム内で応答データを生成しています。
GET
GETのAPIは、URLでパラメータ(製品ID)を受け取りJSONデータ(当該製品IDの製品データ1件)を返却します。データが存在しなかった場合は返却データは空とします。
[Request URL]
http://localhost:8080/jaxrs-kotlin-example/api/product/1
(最後の「1」がパラメータの製品ID)
[Response]
{
"category": "tablet",
"id": 1,
"name": "Tablet-A"
}
POST
POSTのAPIは、JSONで検索条件(製品カテゴリー)を受け取り、検索結果のJSONデータを返します。指定したカテゴリーに合致するデータが存在しない場合は、応答データ中の製品リストが空になります(GETの場合と異なり、JSON自体は返却されます)。
[Request]
{
"category": "tablet"
}
[Response]
{
"category": "tablet",
"description": "jaxrs-kotlin-example product list",
"productList": [
{
"category": "tablet",
"id": 1,
"name": "Tablet-A"
},
{
"category": "tablet",
"id": 2,
"name": "Tablet-B"
}
]
}
準備
build.gradle
Spring Bootだと、Spring Initializrが使えるので、build.gradleを生成してもらえばよいのですが、今回はそうもいかないので、Kotlinのマニュアルも参考にしつつSpring Initializrが生成してくれたbuild.gradleも参考にしつつ、作成しました。
buildscript {
ext.kotlin_version = '1.2.21'
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
repositories {
mavenCentral()
}
}
apply plugin: 'kotlin'
apply plugin: 'war'
ext.kotlin_version = '1.2.21'
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions.jvmTarget = '1.8'
}
compileTestKotlin {
kotlinOptions.jvmTarget = '1.8'
}
repositories {
mavenCentral()
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compileOnly 'javax.ws.rs:javax.ws.rs-api:2.0.1'
}
task wrapper(type: Wrapper) {
gradleVersion = '4.5'
}
ソースディレクトリ
デフォルトの構成であるsrc/main/kotlinにソースを配置します。今回はweb.xml等は作成しないので、src/main/webappは作成していません。
APIコードの作成
ExampleApplication.kt
JAX-RSのApplicationクラスを作ります。javax.ws.rs.Applicationクラスを継承したクラスを作成し、@ApplicationPathアノテーションを付与します。Kotlinのクラス記法になっている以外、通常のJAX-RSアプリと変わりません。
package kazusato.example.jaxrskotlin
import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application
@ApplicationPath("/api")
class ExampleApplication : Application() {
}
Product.kt
製品データを保持するクラスです。Kotlinのデータクラスとして作成しています。GET APIの応答、POST APIの応答の一部として使用します。
package kazusato.example.jaxrskotlin.data
data class Product(
var id: Long = 0,
var name: String = "",
var category: String = ""
)
ProductSearchCriteria.kt
POST APIで検索条件を指定するのに使用します。JAXBでJSONデータから変換するため、引数なしのコンストラクターが存在しないとエラーになります。そのため、クラス定義での引数デフォルト値の設定は必須です。
package kazusato.example.jaxrskotlin.data
data class ProductSearchCriteria(
var category: String = ""
)
ProductSearchResult.kt
POST APIの応答で使用します。検索結果のデータを保持するためのリストを持ち、当該リストにデータを登録するための関数を定義しています。
package kazusato.example.jaxrskotlin.data
data class ProductSearchResult(
var category: String = "",
var description: String = "jaxrs-kotlin-example product list",
var productList: MutableList<Product> = mutableListOf<Product>()
) {
fun addAll(products: Collection<Product>) {
productList.addAll(products)
}
}
ProductResource.kt
APIの処理を記述するリソースクラスです。GET用関数、POST用関数をそれぞれ定義しています。応答用のデータもこのクラスの中で生成しています。
package kazusato.example.jaxrskotlin.api
import kazusato.example.jaxrskotlin.data.Product
import kazusato.example.jaxrskotlin.data.ProductSearchCriteria
import kazusato.example.jaxrskotlin.data.ProductSearchResult
import javax.ws.rs.Consumes
import javax.ws.rs.GET
import javax.ws.rs.POST
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
@Path("/product")
class ProductResource {
private val allProductList = listOf(
Product(1L, "Tablet-A", "tablet"),
Product(2L, "Tablet-B", "tablet"),
Product(3L, "Phone-A", "phone")
)
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getProduct(@PathParam("id") id: Long): Product? = findProductById(id)
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun searchProduct(criteria: ProductSearchCriteria): ProductSearchResult {
val result = ProductSearchResult(category=criteria.category)
val products = findProductsByCategory(criteria.category)
result.addAll(products)
return result
}
private fun findProductById(id: Long): Product? =
allProductList.filter { p -> p.id == id }.firstOrNull()
private fun findProductsByCategory(category: String): List<Product> =
allProductList.filter { p -> p.category == category}
}
getProduct関数
HTTP GETメソッド用の関数です。ポイントとしては、URL中に指定するパラメータを、関数に対するアノテーション
@Path("/{id}")
で指定する点、関数の引数に対するアノテーション
@PathParam("id")
で指定する点と、返却される応答データをJSONとするため、関数に対するアノテーション
@Produces(MediaType.APPLICATION_JSON)
を指定する点です。
searchProduct関数
HTTP POSTメソッド用の関数です。API呼び出し時のデータもJSONで受け取るので、関数に対するアノテーションとして
@Consumes(MediaType.APPLICATION_JSON)
を付与しています。
findProductById関数、findProductsByCategory関数
検索条件に合致したデータを抽出して返却します。findProductByIdの方は、データがあれば一意に決まるのと、存在しない場合はnullを返したい(API的には空の応答としたい)ので、最後にfirstOrNull()を呼んでいます。
ビルド
build.gradleでwarプラグインを設定しているので、build.gradleが置かれているディレクトリで
$ ./gradlew war
を実行すれば、コンパイルを実行してbuild/libs以下にwarファイルが生成されます(Gradleラッパーを使用しています)。
warファイルの中身
せっかくなので
$ jar tvf build/libs/jaxrs-kotlin-example.war
を実行して、生成されたwarファイルの中身を開いて見てみます。
0 Fri Jan 26 16:54:04 JST 2018 META-INF/
25 Fri Jan 26 12:45:48 JST 2018 META-INF/MANIFEST.MF
0 Fri Jan 26 16:54:04 JST 2018 WEB-INF/
0 Fri Jan 26 16:54:04 JST 2018 WEB-INF/classes/
0 Fri Jan 26 15:28:56 JST 2018 WEB-INF/classes/kazusato/
0 Fri Jan 26 15:28:56 JST 2018 WEB-INF/classes/kazusato/example/
0 Fri Jan 26 15:28:56 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/
0 Fri Jan 26 16:54:04 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/api/
4708 Fri Jan 26 16:54:04 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/api/ProductResource.class
0 Fri Jan 26 16:54:00 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/data/
3666 Fri Jan 26 15:28:56 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/data/Product.class
2547 Fri Jan 26 15:28:56 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/data/ProductSearchCriteria.class
5152 Fri Jan 26 16:54:00 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/data/ProductSearchResult.class
664 Fri Jan 26 15:28:56 JST 2018 WEB-INF/classes/kazusato/example/jaxrskotlin/ExampleApplication.class
0 Fri Jan 26 12:54:08 JST 2018 WEB-INF/classes/.gitkeep
0 Fri Jan 26 16:54:04 JST 2018 WEB-INF/lib/
12366 Fri Jan 26 12:53:26 JST 2018 WEB-INF/lib/kotlin-stdlib-jdk8-1.2.21.jar
3135 Fri Jan 26 12:53:26 JST 2018 WEB-INF/lib/kotlin-stdlib-jdk7-1.2.21.jar
932477 Fri Jan 26 12:32:02 JST 2018 WEB-INF/lib/kotlin-stdlib-1.2.21.jar
17536 Sun Jan 07 15:03:54 JST 2018 WEB-INF/lib/annotations-13.0.jar
kotlin-stdlib-1.2.21.jarが900KB超で、結構大きいですね。API自体がシンプルなので、warファイルのサイズ(891376バイト)の大部分はこのJARが占めているようです。
DockerでのAPI実行
最初に記載した通り、今回はPayara MicroをDocker上で動かしてAPIを動作させます。Payara Microは、Glassfishから派生したJava EEアプリケーションサーバPayaraの軽量版で、70MB以下のJAR1個で動くというコンパクトさながら、Eclipse MicroProfileに対応し、JAX-RS、CDI、JTA、JPAといったJava EEの主要APIが使用可能です。
Dockerイメージの作成
Payara Microは公式Dockerイメージが公開されていますので、これを使用します。
以下のDockerfileを使用して、アプリ(warファイル)をデプロイ、起動します。
FROM payara/micro:174
LABEL maintainer="kazusato"
COPY files/jaxrs-kotlin-example.war /opt/payara/deployments
USER root
RUN mkdir /var/log/payara && \
chown payara:payara /var/log/payara
USER payara
ENTRYPOINT [""]
なお、ENTRYPOINTを設定しなくても、アプリを動かすことはできるのですが、リモートデバッグしようとしたときにjavaコマンドにオプションを渡せなくて困ったのでこのようにしています(詳細は別記事に書きます)。
ちなみに、Payara Micro公式イメージのDockerfileでは、ENTRYPOINTとは以下の通り設定されています。
ENTRYPOINT ["java", "-jar", "/opt/payara/payara-micro.jar"]
docker build実行
docker buildに使用するシェルスクリプトは以下の通りです。
# !/bin/sh
cp ../build/libs/jaxrs-kotlin-example.war files
docker build -t kazusato/jaxrs-kotlin-example:0.1 \
--force-rm --no-cache .
スクリプトの作り上、warファイル作成後、dockerディレクトリにcdした上で、
bin/build.sh
を実行するとDockerイメージのビルドができます。
正常にビルドできると、以下のように表示されるはずです。
$ bin/build.sh
Sending build context to Docker daemon 901.6kB
Step 1/7 : FROM payara/micro:174
---> 873991a18378
Step 2/7 : LABEL maintainer "kazusato"
---> Running in 381771f34c9d
---> 3bbf65e78e0a
Removing intermediate container 381771f34c9d
Step 3/7 : COPY files/jaxrs-kotlin-example.war /opt/payara/deployments
---> 2342affa227c
Step 4/7 : USER root
---> Running in d65be685fb5b
---> fea8e6825437
Removing intermediate container d65be685fb5b
Step 5/7 : RUN mkdir /var/log/payara && chown payara:payara /var/log/payara
---> Running in 9640a01d51cb
---> 0442c264dde0
Removing intermediate container 9640a01d51cb
Step 6/7 : USER payara
---> Running in c56c0addfd97
---> e3d719b76980
Removing intermediate container c56c0addfd97
Step 7/7 : ENTRYPOINT
---> Running in f95c7642c7d9
---> 4b905f318386
Removing intermediate container f95c7642c7d9
Successfully built 4b905f318386
Successfully tagged kazusato/jaxrs-kotlin-example:0.1
APIを動かしてみる
docker run用スクリプト
docker run用のスクリプトはこちらです。アプリにアクセスするためのポート8080を公開しています。また、Payara Microのログ出力先の指定もしています。
# !/bin/sh
docker run \
-d \
--name jaxrs-kotlin-example \
-p 8080:8080 \
kazusato/jaxrs-kotlin-example:0.1 \
java \
-jar /opt/payara/payara-micro.jar \
--deploymentDir /opt/payara/deployments \
--logToFile /var/log/payara/payara-micro.log
docker runしてからAPIを呼ぶまで
Dockerイメージ作成時と同様、dockerディレクトリにcdした状態で
bin/run.sh
を実行し、正しくコンテナが起動されると、
$ bin/run.sh
fa0c05d5715eb4055952a86992b6a6273c76e4083be2eb829ab9558a9616a0d2
のようにIDが出力されます(IDの値は異なるでしょうが)。
この状態では、まだアプリのデプロイまで終わっていません。
bin/tail_log.sh
を使うとコンテナ内に出力されているPayara Microのログを参照(tail -F)することができます。ログに以下のように出力されたら、デプロイ完了です。
(略)
http://fa0c05d5715e:8080/jaxrs-kotlin-example
'jaxrs-kotlin-example' REST Endpoints
POST /jaxrs-kotlin-example/api/product
GET /jaxrs-kotlin-example/api/product/{id}
この通り出力されたら、Ctrl+Cで抜けてください。
curlでのAPI呼出
work/call_get.sh
work/call_post.sh
を使うと、curlコマンドを使ってAPIを呼び出すことができます。
GETの呼び出しは、
curl http://localhost:8080/jaxrs-kotlin-example/api/product/1
のような感じです。
一方、POSTは、
curl -X POST -H "Content-type: application/json"
-d '{"category": "tablet"}'
http://localhost:8080/jaxrs-kotlin-example/api/product
となります。
いずれも、呼び出しに成功すると、API仕様の項に記載したようなJSONが返却されます。
Dockerコンテナの停止
動作確認が終わったら、dockerディレクトリにcdした状態で、
bin/stop.sh
としてDockerコンテナを止めてください。また、
bin/rm.sh
とすることでコンテナを削除することもできます。
まとめ
以上が、JAX-RSによるAPIをKotlinで作成し、Docker上のPayara Microで動かす手順となります。
実は、ここまで来る間に、何箇所かはまりどころがあったのですが、それはまた別記事で書きます。