1クール続けるブログ

とりあえず1クール続けるエンジニアの備忘録

黒べこ本(kotlin)でSpring Boot入門メモ② (Controller・ざっくりThymeleaf)

黒べこ本では、"Hello World"でREST APIのController・"ToDoアプリ"でMVCのControllerを書くことになっています。 Controllerに着目して書いていきます。また、Viewを返す後者で使用されるテンプレートエンジンであるThymeleafも少しだけ知っておこうと思います。

Spring Boot での Controller

そもそもMVCってなんだっけ

ウェブアプリの設計においてポピュラーな選択肢であるModel View Controller (MVC) のこと。アプリケーションロジックを3つに分割することで再利用性や柔軟性を高めている。

例えば、簡単なショッピングリストアプリを作成すると考えてみる。今週買う必要のある商品の名前・数量・価格のみを表示してくれる。

f:id:jrywm121:20180313004328p:plain

Model

Modelはリストの商品データと既に存在するリストを実装する。

View

Viewはリストをユーザに対してどのように見せるかを定義する。モデルから表示するデータを受け取る。

Controller

アプリからの入力を受け取って、それに応じてModelやViewを更新するロジック。 例えば、ショッピングリストアプリには入力フォームや追加・削除ボタンが有ります。これらのアクションにより、Modelのアップデートが必要となります。入力情報は一度、Controllerに送られます。その後、Modelの操作が行われ、更新されたデータがViewへ送られます。 また、Viewを更新し別のデータ形式(アルファベットを昇順→降順 / 価格順に並び替える)にすることもあるでしょう。この場合は、modelへのupdateを必要とせず、直接Controllerがハンドルします。

※ 内容書いてから気づきましたが、MDNを参照したので若干Webフロントエンドよりの解釈となっています。

実際のJavaアプリーションだと以下のような構成になると思われます。 これから始めるSpringのwebアプリケーションの7スライド目を引用 f:id:jrywm121:20180313012920j:plain

Controller関係のアノテーション

Javaが標準で提供しているWEBアプリ(ex: Servlet, JDBC)はプログラムや設定ファイルの記述が面倒。確かに新人研修の時にやったJava EEServletJSPxmlに記述したり、冗長な記述でDispatchしていたような記憶がかすかに有ります。

SpringはJava標準APIをラップして簡単にしてくれる。Configurationの方法はアノテーションXMLJava Configが有りますが、Controller / Service / Dao はアノテーションという手段を取るのが見通し良くデメリットも小さいらしい。 @Controllerコンポーネントと@RestControllerコンポーネントでは、アノテーションを使用してリクエスマッピング、リクエスト入力、例外処理などを表現できる(引用元)。

黒べこ本ではToDoリストのアプリで以下のようにControllerを記述しました(一部分のみです)。

@Controller
@RequestMapping("tasks")
class TaskController(private val taskRepository: InMemoryTaskRepository) {

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

    @GetMapping("{id}/edit")
    fun edit(@PathVariable("id") id: Long,
             form: TaskUpdateForm): String {
        val task = taskRepository.findById(id) ?: throw NotFoundException()
        form.content = task.content
        form.done = task.done
        return "tasks/edit"
    }
@Controller

このアノテーションは、@Componentと同じようにコンテナで管理するように指定するもの。Beanとして扱われ、@AutowiredアノテーションがControllerの使用者側に付けられることによってInjectionされる。

@RequestMapping

RequestをController内のメソッドにマッピングするのに使用されます。URL、HTTPメソッド、要求パラメータ、ヘッダー、およびメディアタイプによって一致するさまざまな属性があります。これをクラスレベルで使用して共有マッピングを表現したり、メソッドレベルで特定のエンドポイントマッピングに絞り込むことができます。

@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping

@RequestMappingとHTTPメソッドの組み合わせ。()内で指定するURI正規表現でも書ける。引数を取らない場合は()内にパスの文字列のみを記述すれば良いが、引数を2つ以上取る場合には、パラメータとなる変数名を記述しなくてはいけない。
path: エンドポイント / consumes: Content-Type / produces: Accept / headers: request header / params: request parameter

@PostMapping(path = "/pets", consumes = "application/json", produces = "application/json;charset=UTF-8", headers = "myHeader=myValue",  params = "myParam=myValue")
@PathVariable

pathの中で{ }で定義した変数には、このアノテーションを使用することでアクセス出来ます。URI変数はString変数でない場合には、自動的に適切な型に変換されるか、 もし出来ない場合にはTypeMismatchExceptionが発生します。int, long, Dateはデフォルトでサポートされています。

@Validated

このアノテーションが付与された引数は、標準のBean検査が行われ、もし検査にひっかかってしまった場合にはデフォルトでは400(Bad Request)を返すようにしている。BindingResult型の引数を取ることで、ローカルで処理をすることも可能。

redirect:(Path)

指定されたパスにリダイレクトする。

ハンドルに関するアノテーション
  • @RequestParam : リクエストパラメータの値を取る
  • @RequestHeader : リクエストヘッダーの値を取る
  • @CookieValue : Cookieの値を取る
  • @ModelAttribute : モデル(インスタンス)の値を取る。既にModelメソッドで追加されているモデルから もしくは @SessionAttributes経由のHTTPセッションから もしくは デフォルトのコンストラクタの呼び出しから 等々
  • @SessionAttribute : 既存のセッション属性にアクセスする
  • @RequestAttribute : 作成された既存のリクエスト属性にアクセス
  • @RequestBody : リクエストボディにアクセス
  • @ResponseBody : 関数の戻り値をシリアライズします。@ResponseBody + @Controller = @RestController

戻り値がString型だけど・・・

ビューの論理名からビューの物理名を解決する。 "tasks/new" → "/resources/templates/tasks/new.html" Thymeleaf用のViewResolverクラスがあり、それが名前解決をしてくれる。

Viewに相当するThymeleaf

Thymeleafとは

ThymeleafはJavaのテンプレートエンジンライブラリ。XML/XHTML/HTML5で書かれたテンプレートを変換して、アプリケーションのデータやテキストを表示することができる。JSPの代替技術として近年注目されている

Thymeleafの基本

黒べこ本の中で書くコードは以下となります。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>タスク一覧</title>
    <link rel="stylesheet" href="webjars/bootstrap/4.0.0-2/css/bootstrap.min.css">
</head>
<body>
<a th:href="@{tasks/new}">作成</a>
<p th:if="${tasks.isEmpty()}">タスクがありません</p>
<ul th:unless="${tasks.isEmpty()}">
    <li th:each="task: ${tasks}">
        <a th:href="@{tasks/{id}/edit(id=${task.id})}">
            <span th:unless="${task.done}" th:text="${task.content}"></span>
            <s th:if="${task.done}" th:text="${task.content}"></s>
        </a>
    </li>
</ul>
</body>
</html>
@{...} : リンク式

/で始めると、レスポンスされるhtmlにはコンテキストパスが補完されます。コンテキストパスは絶対パスで記述する際に必要となるもので、Applicationサーバの資源の位置を表す。相対パスで指定する場合は環境によって上手く通らないケースが出てくるため危険。

${...} : 変数式

コンテキストのModelにaddされているインスタンスを指定して展開することが出来ます。事前にController側で以下のコードのように、してあげる必要があります。

    @GetMapping("")
    fun index(model: Model): String {
        val tasks = taskRepository.findAll()
        // 直下の部分。第一引数に取っている名前でView側は参照する。
        model.addAttribute("tasks", tasks)
        return "tasks/index"
    }
th:if=${condition} th:unless=${condition}

conditionが正であれば、if記述のブロックはActiveとなる。 conditionが負であれば、unless記述のブロックはActiveとなる。

th:text

サーバーで実行した時は、th:text属性を指定したタグに挟まれた部分が置き換えられます。htmlで直書きしている内容は上書かれるので、基本的にはモックとして扱われる際のデフォルト値

@{path1/{変数}/path2(変数=代入値}

パスに変数を埋め込む

黒べこ本では先ほど挙げたものの他にこのようなコードも書きます

<!DOCTYPE html>
<html lang="ja" 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>
*{...} : 選択変数式

上記のコードでは、*{content}は本来${taskCreateForm.content}と記述しなくてはいけないのですが、なんども同じような記述が生まれてしまう状況が起きやすいです。そのため簡略化して書けるよう選択変数式があります。親要素でth:object="${taskCreateForm}"と宣言されていれば、*{プロパティ名}で子要素はコンテキストにアクセスすることが可能です。

#fields

#を先頭に付けたものはユーティリティメソッドと呼ばれるもので他にはdateやcalender、sessionに保管されている値を参照するものなどがある。この#fieldsは、Validate後の各プロパティで発生したエラーメッセージを参照している。${#fields.errors('【プロパティ名】')}でアクセスできる。結果はリストになっているので、ループで回せば各エラーメッセージを取得できる。

次回はDIとValidateについてメモしていこうと思います。


参考にさせていただきましたサイト様

MVC architecture - App Center | MDN

これから始めるSpringのwebアプリケーション

Springを何となく使ってる人が抑えるべきポイント

Web on Servlet Stack

Tutorial: Using Thymeleaf (ja)

必要最小限のサンプルでThymeleafを完全マスター - Java EE 事始め!

Spring Boot で Thymeleaf 使い方メモ - Qiita