KotlinでSpring BootのWebアプリケーションを作成してみる（３）

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

https://qiita.com/y5347M/items/203a2b2fc3532bf9699f

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

https://runebook.dev/ja/docs/kotlin/docs/reference/coding-conventions

# 環境
- 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`
```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`
```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`）があるので、`ArticleRepository`と`MarkdownConverter`のコンストラクターのパラメーターは自動的にAutrowiredされるそうです。
（`MarkdownConverter`ってどこのことでしょう…）

`src/main/kotlin/com/example/blog/HtmlController.kt`
```kotlin: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`
```kotlin: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](http://localhost:8080/)にアクセスします。初期化データが投入されています。
<img width="500" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2433961/d0ac4c91-b31c-8efb-d6ff-4eb26a5bbc3c.png">
リンクになっているところをクリックします。記事が参照できるようになっています。
<img width="500" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2433961/56fd3136-17f1-7cf8-6dab-07ab431bfdf9.png">


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

だそうです。
#### プロパティの用意
`src/main/kotlin/com/example/blog/BlogProperties.kt`
```kotlin: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`
```kotlin: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の更新
```properties:application.properties
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
```

せっかくなのでyml形式にしてみます。
```yml:application.yml
blog:
  title: Blog
  banner:
    title: Warning
    content: The blog will be down tomorrow.
```

#### テンプレートの更新
`src/main/resources/templates/blog.mustache`
```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`
```kotlin: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](http://localhost:8080/)にアクセスします。`application.yml`に記載した内容が反省されています。
<img width="500" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2433961/118e2519-baef-8a48-a95b-4dc17a01b9cd.png">

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

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