Kotlin で Spring Boot Web アプリケーションの作成

このチュートリアルでは、Spring BootKotlin (英語) の機能を組み合わせて、サンプルのブログアプリケーションを効率的に構築する方法を示します。

Kotlin を使い始めたばかりの方は、リファレンスドキュメント (英語) を読んだり、オンラインの Kotlin Koans チュートリアル (英語) を実行したり、Kotlin のコードサンプルを提供する Spring Framework リファレンスドキュメントを使用したりすることで言語を学ぶことができます。

Spring Kotlin のサポートは、Spring Framework および Spring Boot のリファレンスドキュメントに記載されています。ヘルプが必要な場合は、 StackOverflow の spring および kotlin タグ (英語) で検索または質問するか、Kotlin Slack (英語) の #spring チャンネルで議論してください。

新しいプロジェクトを作成する

まず、Spring Boot アプリケーションを作成する必要がありますが、これはさまざまな方法で実行できます。

Initializr Web サイトの使用

https://start.spring.io にアクセスして、Kotlin 言語を選択してください。Gradle は、Kotlin で最も一般的に使用されるビルドツールであり、Kotlin プロジェクトの生成時にデフォルトで使用される Kotlin DSL を提供するため、これが推奨される選択肢です。ただし、Maven に慣れている場合は、Maven を使用することもできます。https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin を使用して、デフォルトで Kotlin および Gradle を選択できることに注意してください。

  1. "Gradle" を選択 - 使用するビルドツールに応じて、Kotlin" または "Maven"

  2. 次のアーティファクト座標を入力します: blog

  3. 次の依存関係を追加します。

    • Spring Web

    • Mustache

    • Spring Data JPA

    • H2 Database

    • Spring Boot DevTools

  4. プロジェクトの生成をクリックします。

.zip ファイルには、ルートディレクトリに標準プロジェクトが含まれているため、展開する前に空のディレクトリを作成することをお勧めします。

コマンドラインを使用する

コマンドラインから (英語) Initializr HTTP API を、たとえば UN*X のようなシステム上の curl で使用できます。

$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip

Gradle を使用する場合は、-d type=gradle-project を追加します。

IntelliJ IDEA の使用

Spring Initializr は IntelliJ IDEA Ultimate エディションにも統合されており、コマンドラインまたは Web UI の IDE を離れることなく、新しいプロジェクトを作成およびインポートできます。

ウィザードにアクセスするには、ファイル | 新規 | プロジェクト、および Spring Initializr を選択します。

ウィザードの手順に従って、次のパラメーターを使用します。

  • 成果物: "blog"

  • タイプ: "Gradle - Kotlin」または "Maven"

  • 言語: Kotlin

  • 名前: "Blog"

  • 依存関係: "Spring Web Starter"、"Mustache"、"Spring Data JPA"、"H2 Database" および "Spring Boot DevTools"

Gradle ビルドを理解する

Maven ビルドを使用している場合は、専用セクションにスキップできます。

プラグイン

明らかな Kotlin Gradle プラグイン (英語) に加えて、デフォルト構成は、Spring アノテーション付きまたはメタアノテーション付きのクラスとメソッド(Java とは異なり、デフォルトの修飾子は Kotlin の final)を自動的に開く kotlin-spring プラグインを宣言 (英語) します。これは、たとえば CGLIB プロキシで必要な open 修飾子を追加しなくても、@Configuration または @Transactional Bean を作成できるようにする場合に便利です。

JPA で Kotlin の Null 不可プロパティを使用できるようにするために、Kotlin JPA プラグイン (英語) も有効になっています。@Entity@MappedSuperclass@Embeddable のアノテーションが付けられたクラスの引数なしのコンストラクターを生成します。

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
  id("org.springframework.boot") version "3.2.2"
  id("io.spring.dependency-management") version "1.1.4"
  kotlin("jvm") version "1.9.22"
  kotlin("plugin.spring") version "1.9.22"
  kotlin("plugin.jpa") version "1.9.22"
}

コンパイラーオプション

Kotlin の重要な機能の 1 つは null-safety (英語) です。これは、実行時に有名な NullPointerException にぶつかるのではなく、コンパイル時に null 値をきれいに処理します。これにより、Optional のようなラッパーのコストを支払うことなく、null 可能性の宣言と「値または値なし」のセマンティクスを表現することにより、アプリケーションがより安全になります。Kotlin では、null 許容値を持つ関数構成体を使用できます。 Kotlin null-safety の包括的なガイド (英語) を参照してください。

Java では、その型システムで null 安全を表現することはできませんが、Spring Framework は、org.springframework.lang パッケージで宣言されたツールフレンドリーなアノテーションを介して、Spring Framework API 全体の null 安全を提供します。デフォルトでは、Kotlin で使用される Java API の型 (英語) は、null チェックが緩和されるプラットフォーム型 (英語) として認識されます。JSR 305 アノテーションの Kotlin サポート (英語) + Spring nullability アノテーションは、Kotlin 開発者に Spring Framework API 全体に対して null の安全性を提供し、コンパイル時に null 関連の課題を処理するという利点があります。

この機能を有効にするには、-Xjsr305 コンパイラーフラグを strict オプションに追加します。

build.gradle.kts

tasks.withType<KotlinCompile> {
  kotlinOptions {
    freeCompilerArgs += "-Xjsr305=strict"
  }
}

依存関係

このような Spring Boot Web アプリケーションには 2 つの Kotlin 固有のライブラリが必要であり (標準ライブラリは Gradle で自動的に追加されます)、デフォルトで構成されます。

  • kotlin-reflect は Kotlin 反射ライブラリです

  • jackson-module-kotlin は、Kotlin クラスとデータクラスのシリアライゼーション / デシリアライゼーションのサポートを追加します (単一のコンストラクタークラスを自動的に使用できます。また、セカンダリコンストラクターまたは静的ファクトリを持つクラスもサポートされます。)

build.gradle.kts

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-data-jpa")
  implementation("org.springframework.boot:spring-boot-starter-mustache")
  implementation("org.springframework.boot:spring-boot-starter-web")
  implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
  implementation("org.jetbrains.kotlin:kotlin-reflect")
  runtimeOnly("com.h2database:h2")
  runtimeOnly("org.springframework.boot:spring-boot-devtools")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
}

H2 の最近のバージョンでは、user などの予約済みキーワードを適切にエスケープするための特別な構成が必要です。

src/main/resources/application.properties

spring.jpa.properties.hibernate.globally_quoted_identifiers=true
spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions=true

Spring Boot Gradle プラグインは、Kotlin Gradle プラグインを介して宣言された Kotlin バージョンを自動的に使用します。

Maven ビルドを理解する

プラグイン

明らかな Kotlin Maven プラグイン (英語) に加えて、デフォルト構成は、Spring アノテーション付きまたはメタアノテーション付きのクラスとメソッド(Java とは異なり、デフォルトの修飾子は Kotlin の final)を自動的に開く kotlin-spring プラグインを宣言 (英語) します。これは、たとえば CGLIB プロキシで必要な open 修飾子を追加しなくても、@Configuration または @Transactional Bean を作成できるようにする場合に便利です。

JPA で Kotlin の Null 不可プロパティを使用できるようにするために、Kotlin JPA プラグイン (英語) も有効になっています。@Entity@MappedSuperclass@Embeddable のアノテーションが付けられたクラスの引数なしのコンストラクターを生成します。

pom.xml

<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-plugin</artifactId>
        <configuration>
          <compilerPlugins>
            <plugin>jpa</plugin>
            <plugin>spring</plugin>
          </compilerPlugins>
          <args>
            <arg>-Xjsr305=strict</arg>
          </args>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
          </dependency>
          <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlin.version}</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>

Kotlin の重要な機能の 1 つは null-safety (英語) です。これは、実行時に有名な NullPointerException にぶつかるのではなく、コンパイル時に null 値をきれいに処理します。これにより、Optional のようなラッパーのコストを支払うことなく、null 可能性の宣言と「値または値なし」のセマンティクスを表現することにより、アプリケーションがより安全になります。Kotlin では、null 許容値を持つ関数構成体を使用できます。 Kotlin null-safety の包括的なガイド (英語) を参照してください。

Java では、その型システムで null 安全を表現することはできませんが、Spring Framework は、org.springframework.lang パッケージで宣言されたツールフレンドリーなアノテーションを介して、Spring Framework API 全体の null 安全を提供します。デフォルトでは、Kotlin で使用される Java API の型 (英語) は、null チェックが緩和されるプラットフォーム型 (英語) として認識されます。JSR 305 アノテーションの Kotlin サポート (英語) + Spring nullability アノテーションは、Kotlin 開発者に Spring Framework API 全体に対して null の安全性を提供し、コンパイル時に null 関連の課題を処理するという利点があります。

この機能を有効にするには、-Xjsr305 コンパイラーフラグを strict オプションに追加します。

Kotlin コンパイラーは、Java 8 バイトコード(デフォルトでは Java 6)を生成するように構成されていることにも注意してください。

依存関係

このような Spring Boot Web アプリケーションには 3 つの Kotlin 固有のライブラリが必要であり、デフォルトで構成されています。

  • kotlin-stdlib は Kotlin 標準ライブラリです

  • kotlin-reflect は Kotlin 反射ライブラリです

  • jackson-module-kotlin は、Kotlin クラスとデータクラスのシリアライゼーション / デシリアライゼーションのサポートを追加します (単一のコンストラクタークラスを自動的に使用できます。また、セカンダリコンストラクターまたは静的ファクトリを持つクラスもサポートされます。)

pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mustache</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
  </dependency>
  <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
  </dependency>
  <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

生成されたアプリケーションを理解する

src/main/kotlin/com/example/blog/BlogApplication.kt

package com.example.blog

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

@SpringBootApplication
class BlogApplication

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

Java と比較すると、セミコロンがないこと、空のクラス (@Bean アノテーションを介して Bean を宣言する必要がある場合は、いくつか追加することができます) に括弧がないこと、runApplication トップレベル関数が使われていることがわかります。runApplication<BlogApplication>(*args) は SpringApplication.run(BlogApplication::class.java, *args) に代わる Kotlin イディオムであり、次の構文を使用してアプリケーションをカスタマイズできます。

src/main/kotlin/com/example/blog/BlogApplication.kt

fun main(args: Array<String>) {
  runApplication<BlogApplication>(*args) {
    setBannerMode(Banner.Mode.OFF)
  }
}

初めての Kotlin コントローラーの作成

シンプルなコントローラーを作成して、シンプルな Web ページを表示しましょう。

src/main/kotlin/com/example/blog/HtmlController.kt

package com.example.blog

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping

@Controller
class HtmlController {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = "Blog"
    return "blog"
  }

}

ここでは、Kotlin 拡張 (英語) を使用して、Kotlin 関数または演算子を既存の Spring 型に追加できることに注意してください。ここでは、model.addAttribute("title", "Blog") ではなく model["title"] = "Blog" を記述できるように、org.springframework.ui.set 拡張機能をインポートします。Spring Framework KDoc API (英語) は、Java API を強化するために提供されているすべての Kotlin 拡張をリストしています。

また、関連する Mustache テンプレートを作成する必要があります。

src/main/resources/templates/header.mustache

<html>
<head>
  <title>{{title}}</title>
</head>
<body>

src/main/resources/templates/footer.mustache

</body>
</html>

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

{{> footer}}

BlogApplication.kt の main 機能を実行して Web アプリケーションを起動し、http://localhost:8080/ に移動すると、"Blog" という見出しのある落ち着いた Web ページが表示されます。

JUnit 5 を使用したテスト

Spring Boot でデフォルトで使用されるようになった JUnit 5 は、NULL 非許容 val プロパティを使用できるようにするコンストラクター / メソッドパラメーターのオートワイヤーや、通常の非静的メソッドで @BeforeAll/@AfterAll を使用する可能性など、Kotlin で非常に便利なさまざまな機能を提供します。

Kotlin での JUnit 5 テストの作成

この例のために、さまざまな機能を実証するために統合テストを作成しましょう。

  • 表現力豊かなテスト関数名を提供するために、キャメルケースの代わりにバッククォートの間に実際の文を使用します

  • JUnit 5 では、コンストラクターとメソッドのパラメーターをインジェクトできます。これは、Kotlin の読み取り専用および null 不可のプロパティに適しています

  • このコードは、getForObject および getForEntity Kotlin 拡張を活用しています (それらをインポートする必要があります)

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @Test
  fun `Assert blog page title, content and status code`() {
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>")
  }

}

テストインスタンスのライフサイクル

特定のクラスのすべてのテストの前または後にメソッドを実行する必要がある場合があります。JUnit 4 と同様に、テストクラスはテストごとに 1 回インスタンス化されるため、JUnit 5 ではデフォルトでこれらのメソッドが静的である必要があります(Kotlin では companion object (英語) に変換されます。

ただし、Junit 5 では、このデフォルトの動作を変更し、テストクラスをクラスごとに 1 回インスタンス化できます。これはさまざまな方法 (英語) で実行できます。ここでは、プロパティファイルを使用してプロジェクト全体のデフォルトの動作を変更します。

src/test/resources/junit-platform.properties

junit.jupiter.testinstance.lifecycle.default = per_class

この構成により、上記の IntegrationTests の更新バージョンに示すような通常のメソッドで @BeforeAll および @AfterAll アノテーションを使用できるようになりました。

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @BeforeAll
  fun setup() {
    println(">> Setup")
  }

  @Test
  fun `Assert blog page title, content and status code`() {
    println(">> Assert blog page title, content and status code")
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>")
  }

  @Test
  fun `Assert article page title, content and status code`() {
    println(">> TODO")
  }

  @AfterAll
  fun teardown() {
    println(">> Tear down")
  }

}

独自の拡張機能を作成する

Java のように抽象メソッドで util クラスを使用する代わりに、Kotlin では通常、Kotlin 拡張機能を介してそのような機能を提供します。ここでは、英語の日付形式のテキストを生成するために、既存の LocalDateTime 型に format() 関数を追加します。

src/main/kotlin/com/example/blog/Extensions.kt

fun LocalDateTime.format(): String = this.format(englishDateFormatter)

private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }

private val englishDateFormatter = DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd")
    .appendLiteral(" ")
    .appendText(ChronoField.DAY_OF_MONTH, daysLookup)
    .appendLiteral(" ")
    .appendPattern("yyyy")
    .toFormatter(Locale.ENGLISH)

private fun getOrdinal(n: Int) = when {
  n in 11..13 -> "${n}th"
  n % 10 == 1 -> "${n}st"
  n % 10 == 2 -> "${n}nd"
  n % 10 == 3 -> "${n}rd"
  else -> "${n}th"
}

fun String.toSlug() = lowercase(Locale.getDefault())
    .replace("\n", " ")
    .replace("[^a-z\\d\\s]".toRegex(), " ")
    .split(" ")
    .joinToString("-")
    .replace("-+".toRegex(), "-")

これらの拡張機能を次のセクションで活用します。

JPA を使用した永続性

遅延フェッチが期待どおりに機能するためには、KT-28525 (英語) に従って、エンティティは open でなければなりません。そのために、Kotlin allopen プラグインを使用します。

Gradle の場合:

build.gradle.kts

plugins {
  ...
  kotlin("plugin.allopen") version "1.9.22"
}

allOpen {
  annotation("jakarta.persistence.Entity")
  annotation("jakarta.persistence.Embeddable")
  annotation("jakarta.persistence.MappedSuperclass")
}

または Maven の場合:

pom.xml

<plugin>
  <artifactId>kotlin-maven-plugin</artifactId>
  <groupId>org.jetbrains.kotlin</groupId>
  <configuration>
    ...
    <compilerPlugins>
      ...
      <plugin>all-open</plugin>
    </compilerPlugins>
    <pluginOptions>
      <option>all-open:annotation=jakarta.persistence.Entity</option>
      <option>all-open:annotation=jakarta.persistence.Embeddable</option>
      <option>all-open:annotation=jakarta.persistence.MappedSuperclass</option>
    </pluginOptions>
  </configuration>
</plugin>

次に、Kotlin プライマリコンストラクターの簡潔な構文 (英語) を使用してモデルを作成します。これにより、プロパティとコンストラクターパラメーターを同時に宣言できます。

src/main/kotlin/com/example/blog/Entities.kt

@Entity
class Article(
    var title: String,
    var headline: String,
    var content: String,
    @ManyToOne var author: User,
    var slug: String = title.toSlug(),
    var addedAt: LocalDateTime = LocalDateTime.now(),
    @Id @GeneratedValue var id: Long? = null)

@Entity
class User(
    var login: String,
    var firstname: String,
    var lastname: String,
    var description: String? = null,
    @Id @GeneratedValue var id: Long? = null)

ここでは、String.toSlug() 拡張機能を使用して、Article コンストラクターの slug パラメーターにデフォルト引数を提供していることに注意してください。デフォルト値を持つオプションパラメーターは、位置引数を使用するときに省略できるようにするために、最後の位置で定義されます(Kotlin は名前付き引数 (英語) もサポートしています)。Kotlin では、同じファイル内で簡潔なクラス宣言をグループ化することは珍しくありません。

ここでは、val プロパティで data クラス (英語) を使用しません。これは、JPA が不変クラスまたは data クラスによって自動的に生成されたメソッドで機能するように設計されていないためです。他の Spring Data フレーバーを使用している場合、それらのほとんどはそのような構成をサポートするように設計されているため、Spring Data MongoDB、Spring Data JDBC などを使用する場合は data class User(val login: String, …​) などのクラスを使用する必要があります。
Spring Data JPA は Persistable 経由でナチュラル ID (User クラスの login プロパティであった可能性があります) を使用することを可能にしますが、KT-6653 (英語) のため Kotlin とはうまく適合しません。そのため、Kotlin で生成された ID を持つエンティティを常に使用することが推奨されます。

また、Spring Data JPA リポジトリを次のように宣言します。

src/main/kotlin/com/example/blog/Repositories.kt

interface ArticleRepository : CrudRepository<Article, Long> {
  fun findBySlug(slug: String): Article?
  fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}

interface UserRepository : CrudRepository<User, Long> {
  fun findByLogin(login: String): User?
}

また、JPA テストを作成して、基本的なユースケースが期待どおりに機能するかどうかを確認します。

src/test/kotlin/com/example/blog/RepositoriesTests.kt

@DataJpaTest
class RepositoriesTests @Autowired constructor(
    val entityManager: TestEntityManager,
    val userRepository: UserRepository,
    val articleRepository: ArticleRepository) {

  @Test
  fun `When findByIdOrNull then return Article`() {
    val johnDoe = User("johnDoe", "John", "Doe")
    entityManager.persist(johnDoe)
    val article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
    entityManager.persist(article)
    entityManager.flush()
    val found = articleRepository.findByIdOrNull(article.id!!)
    assertThat(found).isEqualTo(article)
  }

  @Test
  fun `When findByLogin then return User`() {
    val johnDoe = User("johnDoe", "John", "Doe")
    entityManager.persist(johnDoe)
    entityManager.flush()
    val user = userRepository.findByLogin(johnDoe.login)
    assertThat(user).isEqualTo(johnDoe)
  }
}
ここでは、Spring Data にデフォルトで提供される CrudRepository.findByIdOrNull Kotlin 拡張を使用します。これは、Optional ベースの CrudRepository.findById の null 可能バリアントです。詳細については、素晴らしい null はあなたの役に立つ、間違いではない (英語) ブログ投稿を参照してください。

ブログエンジンを実装する

"blog" Mustache テンプレートを更新します。

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

<div class="articles">

  {{#articles}}
    <section>
      <header class="article-header">
        <h2 class="article-title"><a href="/article/{{slug}}">{{title}}</a></h2>
        <div class="article-meta">By  <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
      </header>
      <div class="article-description">
        {{headline}}
      </div>
    </section>
  {{/articles}}
</div>

{{> footer}}

そして、新しい「記事」を作成します。

src/main/resources/templates/article.mustache

{{> header}}

<section class="article">
  <header class="article-header">
    <h1 class="article-title">{{article.title}}</h1>
    <p class="article-meta">By  <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
  </header>

  <div class="article-description">
    {{article.headline}}

    {{article.content}}
  </div>
</section>

{{> footer}}

フォーマットされた日付でブログと記事のページをレンダリングするために、HtmlController を更新します。HtmlController には単一のコンストラクター (暗黙の @Autowired) があるため、ArticleRepository コンストラクターパラメーターは自動的にオートワイヤーされます。

src/main/kotlin/com/example/blog/HtmlController.kt

@Controller
class HtmlController(private val repository: ArticleRepository) {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = "Blog"
    model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
    return "blog"
  }

  @GetMapping("/article/{slug}")
  fun article(@PathVariable slug: String, model: Model): String {
    val article = repository
        .findBySlug(slug)
        ?.render()
        ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
    model["title"] = article.title
    model["article"] = article
    return "article"
  }

  fun Article.render() = RenderedArticle(
      slug,
      title,
      headline,
      content,
      author,
      addedAt.format()
  )

  data class RenderedArticle(
      val slug: String,
      val title: String,
      val headline: String,
      val content: String,
      val author: User,
      val addedAt: String)

}

次に、データの初期化を新しい BlogConfiguration クラスに追加します。

src/main/kotlin/com/example/blog/BlogConfiguration.kt

@Configuration
class BlogConfiguration {

  @Bean
  fun databaseInitializer(userRepository: UserRepository,
              articleRepository: ArticleRepository) = ApplicationRunner {

    val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
    articleRepository.save(Article(
        title = "Lorem",
        headline = "Lorem",
        content = "dolor sit amet",
        author = johnDoe
    ))
    articleRepository.save(Article(
        title = "Ipsum",
        headline = "Ipsum",
        content = "dolor sit amet",
        author = johnDoe
    ))
  }
}
コードを読みやすくするために名前付きパラメーターを使用していることに注意してください。

また、それに応じて統合テストも更新します。

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @BeforeAll
  fun setup() {
    println(">> Setup")
  }

  @Test
  fun `Assert blog page title, content and status code`() {
    println(">> Assert blog page title, content and status code")
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>", "Lorem")
  }

  @Test
  fun `Assert article page title, content and status code`() {
    println(">> Assert article page title, content and status code")
    val title = "Lorem"
    val entity = restTemplate.getForEntity<String>("/article/${title.toSlug()}")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains(title, "Lorem", "dolor sit amet")
  }

  @AfterAll
  fun teardown() {
    println(">> Tear down")
  }

}

Web アプリケーションを起動(または再起動)して http://localhost:8080/ に移動すると、クリック可能なリンクを含む記事のリストが表示され、特定の記事が表示されます。

HTTP API を公開する

@RestController アノテーション付きコントローラーを介して HTTP API を実装します。

src/main/kotlin/com/example/blog/HttpControllers.kt

@RestController
@RequestMapping("/api/article")
class ArticleController(private val repository: ArticleRepository) {

  @GetMapping("/")
  fun findAll() = repository.findAllByOrderByAddedAtDesc()

  @GetMapping("/{slug}")
  fun findOne(@PathVariable slug: String) =
      repository.findBySlug(slug) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")

}

@RestController
@RequestMapping("/api/user")
class UserController(private val repository: UserRepository) {

  @GetMapping("/")
  fun findAll() = repository.findAll()

  @GetMapping("/{login}")
  fun findOne(@PathVariable login: String) =
      repository.findByLogin(login) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This user does not exist")
}

テストでは、統合テストの代わりに、Mockito (英語) に似ていますが Kotlin により適した @WebMvcTest および Mockk (英語) を活用します。

@MockBean および @SpyBean アノテーションは Mockito に固有であるため、Mockk に同様の @MockkBean および @SpykBean アノテーションを提供する SpringMockK [GitHub] (英語) を活用します。

Gradle の場合:

build.gradle.kts

testImplementation("org.springframework.boot:spring-boot-starter-test") {
  exclude(module = "mockito-core")
}
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("com.ninja-squad:springmockk:4.0.2")

または Maven の場合:

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>com.ninja-squad</groupId>
  <artifactId>springmockk</artifactId>
  <version>4.0.2</version>
  <scope>test</scope>
</dependency>

src/test/kotlin/com/example/blog/HttpControllersTests.kt

@WebMvcTest
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {

  @MockkBean
  lateinit var userRepository: UserRepository

  @MockkBean
  lateinit var articleRepository: ArticleRepository

  @Test
  fun `List articles`() {
    val johnDoe = User("johnDoe", "John", "Doe")
    val lorem5Article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
    val ipsumArticle = Article("Ipsum", "Ipsum", "dolor sit amet", johnDoe)
    every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(lorem5Article, ipsumArticle)
    mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk)
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("\$.[0].author.login").value(johnDoe.login))
        .andExpect(jsonPath("\$.[0].slug").value(lorem5Article.slug))
        .andExpect(jsonPath("\$.[1].author.login").value(johnDoe.login))
        .andExpect(jsonPath("\$.[1].slug").value(ipsumArticle.slug))
  }

  @Test
  fun `List users`() {
    val johnDoe = User("johnDoe", "John", "Doe")
    val janeDoe = User("janeDoe", "Jane", "Doe")
    every { userRepository.findAll() } returns listOf(johnDoe, janeDoe)
    mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk)
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("\$.[0].login").value(johnDoe.login))
        .andExpect(jsonPath("\$.[1].login").value(janeDoe.login))
  }
}
$ は文字列の補間に使用されるため、文字列でエスケープする必要があります。

構成プロパティ

Kotlin では、アプリケーションプロパティを管理するための推奨される方法は、読み取り専用プロパティを使用することです。

src/main/kotlin/com/example/blog/BlogProperties.kt

@ConfigurationProperties("blog")
data class BlogProperties(var title: String, val banner: Banner) {
  data class Banner(val title: String? = null, val content: String)
}

次に、BlogApplication レベルで有効にします。

src/main/kotlin/com/example/blog/BlogApplication.kt

@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
  // ...
}

これらのカスタムプロパティを IDE に認識させるために独自のメタデータを生成するには、次のように spring-boot-configuration-processor 依存関係で kapt を構成する必要があります (英語)

build.gradle.kts

plugins {
  ...
  kotlin("kapt") version "1.9.22"
}

dependencies {
  ...
  kapt("org.springframework.boot:spring-boot-configuration-processor")
}
kapt が提供するモデルの制限により、一部の機能(デフォルト値の検出や非推奨のアイテムなど)が機能しないことに注意してください。また、KT-18022 (英語) により、Maven ではアノテーション処理はまだサポートされていません。詳細については、initializr#438 [GitHub] (英語) を参照してください。

IntelliJ IDEA の場合:

  • メニューの ファイル | 設定 | プラグイン | Spring Boot で Spring Boot プラグインが有効になっていることを確認してください

  • メニューの ファイル | 設定 | ビルド、実行、デプロイ | コンパイラー | アノテーションプロセッサー | アノテーション処理を使用可能にする でアノテーション処理を有効にします

  • Kapt はまだ IDEA に統合されていない (英語) ため、コマンド ./gradlew kaptKotlin を手動で実行してメタデータを生成する必要があります

application.properties の編集時に、カスタムプロパティが認識されるようになります(自動補完、検証など)。

src/main/resources/application.properties

blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.

それに応じてテンプレートとコントローラーを編集します。

src/main/resources/templates/blog.mustache

{{> header}}

<div class="articles">

  {{#banner.title}}
  <section>
    <header class="banner">
      <h2 class="banner-title">{{banner.title}}</h2>
    </header>
    <div class="banner-content">
      {{banner.content}}
    </div>
  </section>
  {{/banner.title}}

  ...

</div>

{{> footer}}

src/main/kotlin/com/example/blog/HtmlController.kt

@Controller
class HtmlController(private val repository: ArticleRepository,
           private val properties: BlogProperties) {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = properties.title
    model["banner"] = properties.banner
    model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
    return "blog"
  }

  // ...

Web アプリケーションを再起動し、http://localhost:8080/ をリフレッシュすると、ブログのホームページにバナーが表示されます。

結論

これで、このサンプル Kotlin ブログアプリケーションの構築が完了しました。ソースコードは Github で入手できます (英語) 。特定の機能の詳細が必要な場合は、Spring Framework および Spring Boot のリファレンスドキュメントを参照してください。

コードを入手する

プロジェクト