home

KotlinでSpring BootのWebアプリケーションを作成してみる(3)

2022-06-26T12:41:26+09:00

前提

前回に引き続きチュートリアルを実施してみます。

コーディングルールがわからないときはこちらを参照。(間違っていたら指摘いただけると幸いです。)

環境

  • macOS: 11.6.4
  • IntelliJ IDEA: 2021.2.3 (Community Edition)
  • Kotlin: 212-1.5.10-release-IJ5457.46
  • Gradle: 7.4.1
  • Java: temurin 17.0.3

手順

ブログエンジンを実装

"blog" Mustache テンプレートを更新

src/main/resources/templates/blog.mustache

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

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}}

Controllerを更新

このコントローラーは単一のコンストラクター(暗黙的な@Autowired)があるので、ArticleRepositoryMarkdownConverterのコンストラクターのパラメーターは自動的にAutrowiredされるそうです。
MarkdownConverterってどこのことでしょう…)

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

HtmlController.kt
package com.example.blog

import org.springframework.http.HttpStatus
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.server.ResponseStatusException

@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: ArticleUser,
            val addedAt: String)

}

初期データを登録

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

BlogConfiguration.kt
package com.example.blog

import org.springframework.boot.ApplicationRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class BlogConfiguration {

    @Bean
    fun databaseInitializer(articleUserRepository: ArticleUserRepository,
                            articleRepository: ArticleRepository) = ApplicationRunner {

        val smaldini = articleUserRepository.save(ArticleUser("smaldini", "Stéphane", "Maldini"))
        articleRepository.save(Article(
                title = "Reactor Bismuth is out",
                headline = "Lorem ipsum",
                content = "dolor sit amet",
                author = smaldini
        ))
        articleRepository.save(Article(
                title = "Reactor Aluminium has landed",
                headline = "Lorem ipsum",
                content = "dolor sit amet",
                author = smaldini
        ))
    }
}

統合テストの更新

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

package com.example.blog

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.client.getForEntity
import org.springframework.http.HttpStatus

@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>", "Reactor")
    }

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

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

}

アプリケーションを起動して、http://localhost:8080にアクセスします。初期化データが投入されています。

リンクになっているところをクリックします。記事が参照できるようになっています。

構成プロパティ

引用:Kotlin では、アプリケーションプロパティを管理するための推奨される方法は、@ConfigurationProperties を @ConstructorBinding と組み合わせて、読み取り専用プロパティを使用できるようにすること

だそうです。

プロパティの用意

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

BlogProperties.kt
package com.example.blog

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding

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

アプリケーションレベルでプロパティを有効にする

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

BlogApplication.kt
package com.example.blog

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication

@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication

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

build.gradle.ktsの更新

カスタムプロパティをIDEに認識させるために、spring-boot-configuration-processor依存関係でkapt を構成する必要があります。
build.gradle.kts

build.gradle.kts
plugins {
  ...
  kotlin("kapt") version "1.4.32"
}

dependencies {
  ...
  kapt("org.springframework.boot:spring-boot-configuration-processor")
}

application.propertiesの更新

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

せっかくなのでyml形式にしてみます。

application.yml
blog:
  title: Blog
  banner:
    title: Warning
    content: The blog will be down tomorrow.

テンプレートの更新

src/main/resources/templates/blog.mustache

blog.mustache
{{> header}}

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

{{> 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}}

  {{#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}}

コントローラーの更新

クラスの引数にBlogPropertiesの追加、blog関数の処理を更新します。

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

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"
    }

  ・・・

アプリケーションをリフレッシュし直して再度http://localhost:8080にアクセスします。application.ymlに記載した内容が反省されています。

さいごに

時間かかってしまいましたが、kotlinでspring bootを使用したアプリケーションを構築できました。実際にはもっとエラーに遭遇していますが、たいてい起動する時のJavaのバージョン違いやチュートリアルの読み飛ばしだったように思います。

個人的にはkotlinの利点として、null-safetyである点とJUnitテストの関数名が文章で書けるという点でした。ただnull-safetyはまだまだイメージが掴めていないので、積極的に使いこなして理解していきたいところです。

自己紹介

サムネイル

y5347M

バックエンドエンジニアをしています。