0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

前提

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

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

環境

  • 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はまだまだイメージが掴めていないので、積極的に使いこなして理解していきたいところです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?