HerokuにKotlinアプリをデプロイする
Javaは発売当初から、「一度書けば、どこでも動く」言語として自身をアピールしてきました。プログラマーがJavaでアプリを開発し、バイトコードにコンパイルした後、オペレーティングシステムやプラットフォームに関係なく、どのプラットフォームでも実行可能な実行ファイルにするという考え方です。それを可能にする要因の一部は、JVMとして知られるランタイムです。
Javaの実績を讃えるとすれば、JVMは非常に洗練されたランタイムで、コンピュータの基礎となるハードウェアを抽象化しています。Javaは今日もプログラミング言語として存在していますが、しばしば扱いにくく、古いアプローチの代表と見なされています。
この10年で、JVM上で動く多くの新しい言語が開発され、Javaとは見た目も感じも全く異なります。そのような言語の一つにKotlinがあります。JVMのおかげでパフォーマンスの面でJavaに実質的な優位性はありませんが、しかし、Javaとは異なり、可読性を重視している点がKotlinの強みです。例えば、Javaで部分文字列を出力することを考えてみましょう:
// Java
String input = "人生、宇宙、そして万物についての究極の質問への答えは何ですか?42";
String answer = input.substring(input.indexOf("?") + 1);
System.out.println(answer);
まず、部分文字列に含まれるべき文字のインデックスを得て、一つ足し(文字列はゼロインデックス)、System.out.println
を呼び出しstdoutに書き込みます。
対照的にKotlinではより短く書けます:
// Kotlin
val input = "人生、宇宙、そして万物についての究極の質問への答えは何ですか?42"
val answer = input.substringAfter("?")
println(answer)
Kotlinは非常に関心が集まっており、GoogleはAndroidアプリの開発にJavaよりも推奨しています。
この投稿では、Kotlinでのアプリ開発の簡単な見方をします。PostgreSQLデータベースを備えたシンプルなAPIを構築し、Herokuにデプロイしてライブで見てみましょう。
前提条件
始める前に、マシンに以下のソフトウェアがインストールされていることを確認してください:
- Herokuのアカウント。これは完全に無料で、支払い情報は必要ありません。
- Heroku CLI。アプリがHerokuにある一度に、これにより管理がずっと簡単になります。
- Kotlinがインストールされている必要があります(>= 1.4)。
- Gradleもインストールされている必要があります(>= 7.0)。
また、Gitに少しでも慣れている必要がありますし、マシンにインストールされている必要があります。
KotlinアプリにはIntelliJ IDEを使っていきます。そのドキュメントは、新しいプロジェクトを作成する方法に関するいくつかのガイダンスを提供します。以下のオプションを選択してください:
- Gradleを使ったKotlinアプリケーションを作成したい
- プロジェクトの名前をkotlin-apiに設定
- JDKのバージョンを16に設定します。このバージョンがインストールされていない場合は、ドロップダウンからDownload JDKを選択し、次にOracle Open JDKバージョン16を選択してください
IDEがすべて設定すると、ディレクトリ構造は大体以下のようになっているはずです:
kotlin-api
├── build.gradle.kts
└── src
├── main
│ ├── kotlin
Kotlinファイルはsrc/main/kotlin
に置かれ、ビルドロジックは_build.gradle.kts_にあります。
スタートガイド
Gradleは様々な言語のビルドツールです。また依存性管理ツールとしても機能しますが、Mavenのようなものです。_build.gradle.kts_ファイルには、すでにIDEが役立つために自動的に追加された何行かのコードがあります。それをすべて削除し、代わりにこれらの行を貼り付けます:
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.5.10"
id("org.springframework.boot") version "2.4.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
}
group "com.example"
version "0.0.1"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter")
developmentOnly("org.springframework.boot:spring-boot-devtools")
}
これらの行はプロジェクトの依存性とそれらを見つける場所を指定しています。例えば、[org.springframework.boot](https://plugins.gradle.org/plugin/org.springframework.boot)
をバージョン2.4.3で使いたいので、それがplugins
ブロック内で定義されています。パッケージを見つける場所—mavenCentral()
—を示し、使用したい露出したクラスを指定します(implementation("org.springframework.boot:spring-boot-starter-web")
)。
セットアップをテストするために二つの小さなファイルを作成しましょう。Application.kt
というファイルを_src/main/kotlin_フォルダに作成し、以下の内容を貼り付けます:
package com.example
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
open class Application
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
これでSpringフレームワークを使ったデフォルトアプリを開始します。本物の魔法は次のファイル、Controller.kt
で起きますが、これも_src/main/kotlin_にApplication.kt
の隣に作成してください:
package com.example
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class GreetingController {
@GetMapping("/{name}")
fun get(@PathVariable name: String) = "こんにちは、$name"
}
ここでは、@GetMapping("/{name}")
でルートを定義し、{name}
は動的な値です。このデコレーターをKotlinメソッド(fun get
、つまり「getという名前の関数」)の上に置くことで、ルートを任意の振る舞いにマッチさせることができます—この場合は、GET
リクエストでパス名を使った挨拶を返すことです。
アプリを起動する方法をIDEに知らせるためには、ランの構成を作成する必要があります。IDEメニューの上部にある**構成の追加...**ボタンをクリックします。新しいラン構成の追加を選択し、リストからGradleを選びます。Gradleプロジェクト名にはkotlin-apiを入力します。TasksフィールドにはbootRun
と入力してください。[bootRun](https://docs.spring.io/spring-boot/docs/2.5.0/gradle-plugin/reference/htmlsingle/#running-your-application)
はSpringフレームワークによって提供されるGradleタスクで、コードをコンパイルしサーバを起動します。Okをクリックすると、IDEメニューバーに緑色の再生ボタンが表示されます。これをクリックすると、IDEはgradle bootRun
を実行してこのKotlinアプリをビルドし、サーバを起動します。それが完了したら、http://localhost:8080/world
にアクセスしてください。素敵な挨拶が見えるはずです。
データベースとのやり取り
さて、(少し)真剣な話です。プロジェクトにデータベースを接続したいとしたらどうでしょうか。Maven/Java世界では、XMLファイルを更新し、JARファイルへの参照を追加する必要があります。Gradleでは、build.gradle.kts
ファイルにいくつかの行を追加するだけですむんです:
dependencies {
# ...
implementation("com.zaxxer:HikariCP:4.0.3")
runtimeOnly("org.postgresql:postgresql")
# ...
}
ここではHikariCPをプロジェクトに含めています。これは人気のデータベース接続ドライバーです。org.postgresql
ライブラリをランタイム中に「ロード」したいと示しています。たった2行で、PostgreSQLデータベースを利用したいことを設定に伝えました。すでにローカルでPostgreSQLデータベースが動いているなら、素晴らしいです。このガイドの残りをローカルで続行し、localhostを閲覧すると結果が見られます。PostgreSQLがない場合は心配しないでください—Herokuにこのアプリをデプロイするのがどれだけ簡単かをお見せします。インフラはHerokuが面倒を見てくれます。
Controller.kt
に戻って、以下の内容で置き換えてください。以前のものからいくつか取り、それに追加しています。その変更についてはまもなく説明します:
package com.example
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.http.MediaType
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import java.net.URI
import javax.sql.DataSource
@RestController
class GreetingController {
val dataSource = dataSource()
val connection = dataSource.connection
@GetMapping("/{name}")
fun get(@PathVariable name: String) = "こんにちは、$name"
@PostMapping(value = ["/add-name"], consumes = [MediaType.TEXT_PLAIN_VALUE])
fun post(@RequestBody requestBody: String): String {
initDb()
val stmt = connection.createStatement()
stmt.executeUpdate("INSERT INTO names values('$requestBody')")
return "追加されました: $requestBody"
}
@GetMapping("/everyone")
fun getAll(): String {
initDb()
val stmt = connection.createStatement()
val rs = stmt.executeQuery("SELECT name FROM names")
val output = ArrayList<String>()
while (rs.next()) {
output.add(rs.getString("name"))
}
val names = output.joinToString(", ")
return "こちらが名前です: $names"
}
internal fun initDb() {
val stmt = connection.createStatement()
stmt.executeUpdate("CREATE TABLE IF NOT EXISTS names (name text)")
}
internal fun dataSource(): HikariDataSource {
val config = HikariConfig()
var dbUri = URI(System.getenv("DATABASE_URL") ?: "postgresql://localhost:5432/")
var dbUserInfo = dbUri.getUserInfo()
var username: String?; var password: String?;
if (dbUserInfo != null) {
username = dbUserInfo.split(":").get(0)
password = dbUserInfo.split(":").get(1)
} else {
username = System.getenv("DATABASE_USERNAME") ?: null
password = System.getenv("DATABASE_PASSWORD") ?: null
}
if (username != null) {
config.setUsername(username)
}
if (password != null) {
config.setPassword(password)
}
val dbUrl = "jdbc:postgresql://${dbUri.getHost()}:${dbUri.getPort()}${dbUri.getPath()}"
config.setJdbcUrl(dbUrl)
return HikariDataSource(config)
}
}
なんだかんだでかなり多くのことが行われていますね!最初にdataSource
という関数を定義し、これがデータベースへの接続を提供します。12ファクターアプリを構築しているため、データベース認証情報はDATABASE_URL
という環境変数に保存されています。そのURLを取得し、もしあればユーザー名とパスワードを取り出します。なければ、その情報のための別の2つの環境変数—DATABASE_USERNAME
とDATABASE_PASSWORD
—をチェックします。それから、すべての情報をデータベースコネクタが必要とする形式にまとめます。initDb
関数はnames
というテーブルを作成し、そこにname
という単一のテキストカラムを作ります。/everyone
エンドポイントには、前と同じように@GetMapping
デコレーターがあります。これはGET /everyone
ルートを定義し、データベースからすべての名前を取得します。
最後に、まったく新しいものを追加しました:@PostMapping
デコレーターです。ここでは、このPOST
ルートが受け入れられるコンテンツの種類を定義する必要があります。この場合、TEXT_PLAIN_VALUE
メディアタイプ(つまり、"Content-Type: text/plain"
)をconsumes
します。リクエストボディに入れた情報の文字列はすべてデータベースに追加されます。わずか数行で、追加してクエリできる小さなAPIを構築しました。
このサーバを今スタートし、ロ
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/heroku/deploying-a-kotlin-app-to-heroku-5d0g