黒べこ本(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つに分割することで再利用性や柔軟性を高めている。
例えば、簡単なショッピングリストアプリを作成すると考えてみる。今週買う必要のある商品の名前・数量・価格のみを表示してくれる。
Model
Modelはリストの商品データと既に存在するリストを実装する。
View
Viewはリストをユーザに対してどのように見せるかを定義する。モデルから表示するデータを受け取る。
Controller
アプリからの入力を受け取って、それに応じてModelやViewを更新するロジック。 例えば、ショッピングリストアプリには入力フォームや追加・削除ボタンが有ります。これらのアクションにより、Modelのアップデートが必要となります。入力情報は一度、Controllerに送られます。その後、Modelの操作が行われ、更新されたデータがViewへ送られます。 また、Viewを更新し別のデータ形式(アルファベットを昇順→降順 / 価格順に並び替える)にすることもあるでしょう。この場合は、modelへのupdateを必要とせず、直接Controllerがハンドルします。
※ 内容書いてから気づきましたが、MDNを参照したので若干Webフロントエンドよりの解釈となっています。
実際のJavaアプリーションだと以下のような構成になると思われます。
これから始めるSpringのwebアプリケーションの7スライド目を引用
Controller関係のアノテーション
Javaが標準で提供しているWEBアプリ(ex: Servlet, JDBC)はプログラムや設定ファイルの記述が面倒。確かに新人研修の時にやったJava EEはServletやJSPをxmlに記述したり、冗長な記述でDispatchしていたような記憶がかすかに有ります。
SpringはJava標準APIをラップして簡単にしてくれる。Configurationの方法はアノテーションとXML、Java 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
Tutorial: Using Thymeleaf (ja)