Kotlin
spring
SpringBoot

開発準備

Spring Inirializrで以下のプロジェクトを作る。

Spring Initializr

  • ビルドツール: Gradle
  • 言語: Kotlin
  • Spring Bootのバージョン: 1.5.7
  • Group: com.example
  • Artifact: todolist
  • Dependencies: Web, Thymeleaf

タスクの一覧表示

タスクのデータクラスを作成

Task.kt
package com.example.todolist

data class Task(
        val id: Long,
        val content: String,
        val done: Boolean
)

コントローラーの作成

ダミータスクをプレーンテキストを返すコントローラー

TaskController.kt
package com.example.todolist

import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("tasks")
class TaskController {
    @GetMapping
    fun index(model: Model): String {
        val tasks = listOf(
                Task(1, "Test Task1", false),
                Task(2, "Test Task2", true),
                Task(3, "Test Task3", false)
        )
        return tasks.toString()
    }
}

@RequestMapping("tasks")の指定により、このクラスに定義するメソッドへのパスは"/tasks"から始まることを設定している。
@GetMapping("")の指定があるため、/tasksというパスへのGETリクエストに反応する。

動作確認

以下のコマンドでアプリケーションを起動。

./gradlew bootRun

以下のURLにブラウザにアクセス。

http://localhost:8080/tasks

ブラウザに以下の文字列が表示される。

[Task(id=1, content=Test Task1, done=false), Task(id=2, content=Test Task2, done=true), Task(id=3, content=Test Task3, done=false)]

タスク一覧をHTMLで返す

Controllerの修正

TaskController.kt
package com.example.todolist

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

@Controller
@RequestMapping("tasks")
class TaskController {
    @GetMapping
    fun index(model: Model): String {
        val tasks = listOf(
                Task(1, "Test Task1", false),
                Task(2, "Test Task2", true),
                Task(3, "Test Task3", false)
        )
        model.addAttribute("tasks", tasks)
        return "tasks/index"
    }
}

修正点

  • importとアノテーションをRestControllerからControllerに変更
  • model.addAttributeでテンプレートにデータを引き渡す処理を追加
  • returnはテンプレートの名称を返すように変更

テンプレートの追加

  • Thymeleafというテンプレートエンジンを使用
  • テンプレートファイルはsrc/main/resources/templates/に配置する
tasks/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>タスク一覧</title>
</head>
<body>
<ul th:unless="${tasks.isEmpty()}">
    <li th:each="task: ${tasks}">
        <span th:unless="${task.done}" th:text="${task.content}"></span>
        <s th:if="${task.done}" th:text="${task.content}"></s>
    </li>
</ul>
</body>
</html>

  • Thymeleafのテンプレート要素はth:から始まる属性をつける
  • th:if は条件が当てはまるときに表示する
  • th:unless は th:if の逆
  • th:each は 要素の繰り返し
  • th:text は要素の中身を出力する

動作確認

ブラウザで以下のURLにアクセスする。

http://localhost:8080/task

ブラウザに以下が表示される

  • Test Task1
  • Test Task2
  • Test Task3

保存されているタスクを表示するように

TaskRepositoryインターフェースの定義

TaskRepository.kt
package com.example.todolist

interface TaskRepository {
    fun create(content: String): Task
    fun update(task: Task)
    fun findAll(): List<Task>
    fun findById(id: Long): Task?
}

メモリ上(変数)にタスクを保存するTaskRepositoryの実装をする。

InMemoryTaskRepository.kt
package com.example.todolist

import org.springframework.stereotype.Repository

@Repository
class InMemoryTaskRepository : TaskRepository {
    private val tasks: MutableList<Task> = mutableListOf()

    private val maxId: Long
        get() = tasks.map(Task::id).max() ?: 0

    override fun create(content: String): Task {
        val id = maxId + 1
        val task = Task(id, content, false)
        tasks += task
        return task
    }

    override fun update(task: Task) {
        tasks.replaceAll{ t ->
            if (t.id == task.id) task
            else t
        }
    }

    override fun findAll(): List<Task> = tasks.toList()

    override fun findById(id: Long): Task? = tasks.find { it.id == id }
}

上記を使うためにControllerの修正を行う。

TaskController.kt
package com.example.todolist

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

@Controller
@RequestMapping("tasks")
class TaskController(private val taskRepository: TaskRepository) {
    @GetMapping
    fun index(model: Model): String {
        val tasks = taskRepository.findAll()
        model.addAttribute("tasks", tasks)
        return "tasks/index"
    }
}

テンプレートを修正し、タスクがない旨を表す。

tasks/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>タスク一覧</title>
</head>
<body>
<p th:if="${tasks.isEmpty()}">タスクがありません。</p>
<ul th:unless="${tasks.isEmpty()}">
    <li th:each="task: ${tasks}">
        <span th:unless="${task.done}" th:text="${task.content}"></span>
        <s th:if="${task.done}" th:text="${task.content}"></s>
    </li>
</ul>
</body>
</html>

※ pタグを追加

タスク新規作成

タスクを作成する機能を実装する。

フォームクラス作成

こういうクラスをフォームクラスと呼ぶらしい。
Symfonyのformhelperみたいなもんか?

TaskCreateFrom.kt
package com.example.todolist

import org.hibernate.validator.constraints.NotBlank
import javax.validation.constraints.Size

class TaskCreateForm {
    @NotBlank
    @Size(max = 20)
    var content: String? = null
}

コントローラの修正

importの追加
```kotlin:TaskController.kt
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.servlet.mvc.support.RedirectAttributes
import org.springframework.validation.BindingResult
import org.springframework.validation.annotation.Validated

tasks/newページの作成

```kotlin:TaskController.kt
// 以下のメソッドを追加
@GetMapping("new")
fun new(form: TaskCreateForm): String {
    return "tasks/new"
}

タスクを新規作成する処理を追加

TaskController.kt
// 以下のメソッドを追加
@PostMapping("")
fun create(@Validated form: TaskCreateForm, bindingResult: BindingResult): String {
    if (bindingResult.hasErrors()) {
        return "tasks/new"
    }
    val content = requireNotNull(form.content)
    taskRepository.create(content)
    return "redirect:/tasks"
}

テンプレート追加/修正

tasks/new.htmlの追加

tasks/new.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>タスク作成</title>
</head>
<body>
<form th:method="post" th:action="@{./}" th:object="${taskCreateForm}">
    <div>
        <label>
            内容: <input type="text" th:field="*{content}" />
        </label>
        <p th:if="${#fields.hasErrors('content')}" th:errors="*{content}">エラーメッセージ</p>
    </div>
    <div>
        <input type="submit" value="作成" />
    </div>
</form>
</body>
</html>

tasks/index.htmlの修正

tasks/index.html
<!-- 次の行を追加 -->
<a th:href="@{tasks/new}">作成</a>

リンク