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
{{> 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}}
Controllerを更新
このコントローラーは単一のコンストラクター(暗黙的な@Autowired
)があるので、ArticleRepository
とMarkdownConverter
のコンストラクターのパラメーターは自動的にAutrowiredされるそうです。
(MarkdownConverter
ってどこのことでしょう…)
src/main/kotlin/com/example/blog/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
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
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
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
plugins {
...
kotlin("kapt") version "1.4.32"
}
dependencies {
...
kapt("org.springframework.boot:spring-boot-configuration-processor")
}
application.propertiesの更新
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
せっかくなのでyml形式にしてみます。
blog:
title: Blog
banner:
title: Warning
content: The blog will be down tomorrow.
テンプレートの更新
src/main/resources/templates/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
@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
はまだまだイメージが掴めていないので、積極的に使いこなして理解していきたいところです。