1. 記事一覧 >
  2. ブログ記事
Spring Boot
category logo

Kotlin + Spring Boot + Dockerビルド(Gradle, Fat JAR)まで全手順

(更新) (公開)

はじめに

Kotlin + SpringBoot で REST API 実装

Gradle / FatJar のビルド

Docker ビルド
の流れを最初から最後までの全手順になります。


どちらかと言うと、Dockerビルドが主題で、REST APIの実装は、主題ではありません。そのため、かなり簡素な実装で進めます。


この記事の Docker ビルドについては、公式ドキュメント
Spring Boot with Docker

Example 3
Kotlin 版という感じです。

【検証環境】

Ubuntu 20.04.2 LTS

 IntelliJ IDEA 2022.1.2(Community Edition)

 openjdk version "17.0.3"

 org.springframework.boot 2.6.8

 kotlin("jvm") version "1.6.21"

 kotlin("plugin.spring") version "1.6.21"

 gradle-7.4.1

 Docker version 20.10.17, build 100c701

  openjdk:17-jdk-alpine


プロジェクト作成

Spring initializr にて、プロジェクトのテンプレートをダウンロードします。


有料の IntelliJ IDEA Ultimate には、 Spring initializr が備わっていますが、今回は、無料の IntelliJ IDEA Community を使う前提です。

【余談】

initializr が initializer じゃないのが気になりましたが、

initializr.com にインスパイアされたという情報がありました。

その initializr.com は、なぜ initializr なのかは謎ですが...。


Project: Gradle Project
Language: Kotlin
Spring Boot: 2.6.8
Packing: Jar
Java: 17
Dependencies: Spring Web
を選択します。


プロジェクトの名前は、任意ですが、最初から表示されている demo で進めていきます。


選択したら、GENERATE をクリックします。

Spring initializrでGENERATE


API 実装

IntelliJ IDEA Community をインストールします。
インストール手順は、別記事「Kotless と LocalStack で疑似サーバーレス - AWS S3 ダウンロード」にありますので、省略して、Ubuntu に IntelliJ IDEA Community Linux をインストールしたものとして、進めていきます。


Spring initializr から入手した、demo.zip を展開します。


今回は、/home/admin/demo に展開したものとします。

/home/admin/demoにdemo.zipを展開


IntelliJ IDEA Community で読み込みます。

IntelliJ IDEA Community で読み込み


最初は、いろいろダウンロードが始まるので、終わるまで待ちます。(作業していても良いです。)

IntelliJ いろいろダウンロード中


src/main/kotlin/com/example/demo/DemoApplication.kt を編集します。


最初は、以下の状態でした。

DemoApplication.kt
package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
	runApplication<DemoApplication>(*args)
}

編集後、以下のようにします。
GET /api   →  Hello world! と返す。
POST /api   →   POST パラメーターの value の値をそのまま返す。
という単純な実装です。

DemoApplication.kt
package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.*

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
	runApplication<DemoApplication>(*args)
}

@RestController
@RequestMapping("/api")
class IndexController {
	@GetMapping
	fun get(): String? = "Hello world!"
	@PostMapping
	fun post(@RequestParam("value") value: String?): String? = value
}

テストプログラム
src/test/kotlin/com/example/demo/DemoApplicationTests.kt
を編集します。
最初は、以下の状態でした。

DemoApplicationTests.kt
package com.example.demo

import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class DemoApplicationTests {

	@Test
	fun contextLoads() {
	}

}

編集後、以下のようにします。
GET /api   →  Hello world! と返ってくるかどうか確認。
value=hello で POST /api   →  hello と返ってくるかどうか確認。
という単純な実装です。

DemoApplicationTests.kt
package com.example.demo

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

@SpringBootTest
@AutoConfigureMockMvc
class DemoApplicationTests {

	@Autowired
	private lateinit var mockMvc: MockMvc

	@Test
	fun get() {
		mockMvc.get("/api").andExpect {
			status { isOk() }
			content {
				contentTypeCompatibleWith("text/plain")
				string("Hello world!")
			}
		}
	}

	@Test
	fun post() {
		mockMvc.post("/api") {
			param("value", "hello")
		}.andExpect {
			status { isOk() }
			content {
				string("hello")
			}
		}.andDo {
			print()
			handle {
				println(it.response.characterEncoding)
			}
		}
	}

}

テストを実行します。

IntelliJ でテストを実行


あるいは、端末から、

$ cd /home/admin/demo
$ ./gradlew test

で実行します。


結果、問題無しでした。


アプリを実行します。

IntelliJ でアプリを実行


あるいは、端末から、

$ cd /home/admin/demo
$ ./gradlew bootRun

で実行します。


端末から API を呼び出してみます。

$ curl http://localhost:8080/api
Hello world!
$ curl -d "value=hello" http://localhost:8080/api
hello

OKです!


Kotlin ビルド

ビルドして、Fat JAR を生成します。
実行可能 jar ファイル(Fat JAR)の作成を担当するタスク bootJar タスク でビルドします。
bootJar タスク は、SpringBoot プラグインのタスクです。

# cd /home/admin/demo
# ./gradlew bootJar

【 Fat JAR 】

Fat JAR(または uber-jar とも呼ばれる)アーカイブは通常、すべての依存ライブラリを1つにまとめた単一のJARファイルで、Javaを使ってスタンドアロンのアプリケーションとして起動することができます。

【 bootJarタスク 】

./gradlew build あるいは、./gradlew assemble でも bootJar タスクが実行されますが、他のタスクに興味は無いため、./gradlew bootJar としています。

# find build/libs
build/libs
build/libs/demo-0.0.1-SNAPSHOT.jar

ビルドできました!


Docker インストール

docker コマンドを使ってビルドするため、docker をインストールします。

# apt update
# apt install -y apt-transport-https ca-certificates software-properties-common
# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
# apt update
# apt install -y docker-ce
# docker -v
Docker version 20.10.17, build 100c701

Dockerfile 作成

Dockerfile を作成します。


注意:下記「公式ドキュメント」 は、リンク切れになりました。


内容は、公式ドキュメント
Spring Boot with Docker
Example 3
に書いてある通りで、アプリ名の部分だけ以下のように変えました。
hello.Applicationcom.example.demo.DemoApplicationKt


MANIFEST.MF に載っている
Start-Class: com.example.demo.DemoApplicationKt
です。(MANIFEST.MFは、この後の jar 展開のところで出てきます。)

# cd /home/admin/demo
# vi Dockerfile
FROM openjdk:17-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplicationKt"]

jar 展開

Dockerfile の中で、BOOT-INF と META-INF をコピーしています。
この BOOT-INF と META-INF は、jar の中にあるため、展開しておきます。

# mkdir build/dependency
# cd build/dependency
# jar -xf ../libs/*.jar
# apt -y install tree
# cd ../../
# tree -L 3 build/dependency
build/dependency
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   ├── com
│   │   ├── META-INF
│   │   ├── static
│   │   └── templates
│   ├── classpath.idx
│   ├── layers.idx
│   └── lib
│       ├── annotations-13.0.jar
│       ├(略)
│       └── tomcat-embed-websocket-9.0.63.jar
├── META-INF
│   └── MANIFEST.MF
└── org
    └── springframework
        └── boot

11 directories, 39 files
# cat build/dependency/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplicationKt
Spring-Boot-Version: 2.6.8
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

準備完了です!


Docker ビルド

Docker ビルドし、コンテナを起動します。
イメージのタグは、公式ドキュメントと同じ springio/gs-spring-boot-docker にしています。 --build-arg DEPENDENCY=build/dependency により、Dockerfile 内の ${DEPENDENCY}build/dependency になるようにしています。

# docker build --build-arg DEPENDENCY=build/dependency -t springio/gs-spring-boot-docker .
・・・
Step 8/8 : ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplicationKt"]
 ---> Running in 5a37a2c857c1
Removing intermediate container 5a37a2c857c1
 ---> fbf91ab1c9ed
Successfully built fbf91ab1c9ed
Successfully tagged springio/gs-spring-boot-docker:latest
# docker run -p 8080:8080 springio/gs-spring-boot-docker

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.8)

2022-06-11 10:25:52.896  INFO 1 --- [           main] com.example.demo.DemoApplicationKt       : Starting DemoApplicationKt using Java 17-ea on 2183b7ba3a37 with PID 1 (/app started by spring in /)
・・・

できました!


動作確認

# docker images
REPOSITORY                       TAG             IMAGE ID       CREATED         SIZE
springio/gs-spring-boot-docker   latest          fbf91ab1c9ed   7 minutes ago   348MB
openjdk                          17-jdk-alpine   264c9bdce361   11 months ago   326MB
# docker ps
CONTAINER ID   IMAGE                            COMMAND                  CREATED         STATUS         PORTS                                       NAMES
2183b7ba3a37   springio/gs-spring-boot-docker   "java -cp app:app/li…"   3 minutes ago   Up 3 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   vigorous_lewin

コンテナ起動ヨシ!


端末から API を呼び出してみます。

$ curl http://localhost:8080/api
Hello world!
$ curl -d "value=hello" http://localhost:8080/api
hello

ヨシ!!


bootBuildImage

ここまで書いておいて、なんですが、
Kotlin ビルド ~ Docker ビルドまで 1行で済む別のビルド方法があります。

# ./gradlew bootBuildImage --imageName=springio/gs-spring-boot-docker

とすると、自動的に Docker ビルドされます。
Dockerfile 不要です。(docker のインストールは必要です。)


# docker images
REPOSITORY                       TAG        IMAGE ID       CREATED        SIZE
paketobuildpacks/run             base-cnb   d212cf6a84ff   2 days ago     88.6MB
springio/gs-spring-boot-docker   latest     4108c09ba48e   42 years ago   279MB
paketobuildpacks/builder         base       beb5e13e1cee   42 years ago   980MB

# docker run -it -d -p 8080:8080 springio/gs-spring-boot-docker
# curl http://localhost:8080/api
Hello world!
# curl -d "value=hello" http://localhost:8080/api
hello
# docker ps
CONTAINER ID   IMAGE                            COMMAND              CREATED          STATUS          PORTS                                       NAMES
f2433f94adc9   springio/gs-spring-boot-docker   "/cnb/process/web"   16 seconds ago   Up 15 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   nice_tereshkova
# docker exec -it nice_tereshkova head -n2 /etc/*-release
==> /etc/lsb-release <==
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04

==> /etc/os-release <==
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"

CREATED が 42 years ago になっているのが気になりましたが、動作は正常でした。


Ubuntu 18.04.6 LTS で起動しているようでした。

loading...