3.4. アプリケーション層の実装¶
目次
- Controllerの実装
- フォームオブジェクトの実装
- Viewの実装
- ThymeleafのテンプレートHTMLの実装
- Thymeleafのネームスペースを設定する
- モデルに格納されている値を表示する
- モデルに格納されている数値を表示する
- モデルに格納されている日時を表示する
- リクエストURLを生成する
- メッセージを表示する
- 文字列を組み立てる
- 条件を判定する
- 条件によって表示を切り替える
- コレクションの要素に対して表示処理を繰り返す
- オブジェクトのプロパティを省略して指定する
- ローカル変数を定義する
- プリプロセッシング
- フォームオブジェクトのプロパティをバインドする
- 入力チェックエラーを表示する
- 処理結果のメッセージを表示する
- コードリストを表示する
- ページネーション用のリンクを表示する
- 権限によって表示を切り替える
- JavaScriptの実装
- スタイルシートの実装
- ThymeleafのテンプレートHTMLの実装
- 共通処理の実装
- 二重送信防止について
- セッションの使用について
本節では、HTML formを使った画面遷移型のアプリケーションにおけるアプリケーション層の実装について説明する。
アプリケーション層の実装は、以下の3つにわかれる。
- Controllerは、リクエストの受付、業務処理の呼び出し、モデルの更新、Viewの決定といった処理を行い、リクエストを受けてからの一連の処理フローを制御する。アプリケーション層の実装において、もっとも重要な実装となる。
- フォームオブジェクトは、HTML formとアプリケーションの間での値の受け渡しを行う。
- View(Thymeleaf)は、モデル(フォームオブジェクトやドメインオブジェクトなど)からデータを取得し、画面(HTML)を生成する。
3.4.1. Controllerの実装¶
- リクエストを受け取るためのメソッドを提供する。
@RequestMapping
アノテーションもしくは@RequestMapping
合成アノテーションが付与されたメソッドを実装することで、リクエストを受け取ることができる。 - リクエストパラメータの入力チェックを行う。入力チェックが必要なリクエストを受け取るメソッドでは、
@Validated
アノテーションをフォームオブジェクトの引数に指定することで、リクエストパラメータの入力チェックを行うことができる。単項目チェックはBean Validation、相関チェックはSpring Validator又はBean Validationでチェックを行う。 - 業務処理の呼び出しを行う。Controllerでは業務処理の実装は行わず、Serviceのメソッドに処理を委譲する。
- 業務処理の処理結果をModelに反映する。Serviceのメソッドから返却されたドメインオブジェクトを
Model
に反映することで、Viewから処理結果を参照できるようにする。 - 処理結果に対応するView名を返却する。Controllerでは処理結果に対する描画処理を実装せず、描画処理はThymeleaf等のViewで実装する。Controllerでは描画処理が実装されているViewのView名の返却のみ行う。View名に対応するViewの解決は、Spring Frameworkより提供されている
ViewResolver
によって行われ、処理結果に対応するView(Thymeleaf等)が呼び出される仕組みになっている。
Note
Controllerでは、業務処理の呼び出し、処理結果のModel
への反映、遷移先(View名)の決定などのルーティング処理の実装に徹することを推奨する。
Controllerの実装について、以下4つの点に着目して説明する。
3.4.1.1. Controllerクラスの作成方法¶
org.springframework.web.servlet.mvc.Controller
インタフェースを実装する方法 (Interface-based Controller)もあるが、Spring3以降はDeprecatedになっているため、原則使用しない。@Controller public class SampleController { // omitted }
3.4.1.2. リクエストとハンドラメソッドのマッピング方法¶
@RequestMapping
アノテーションを付与する。@RequestMapping(value = "hello", RequestMethod.GET) public String hello() { // omitted }
@RequestMapping
の合成アノテーションである@GetMapping
や@PostMapping
が追加された。@GetMapping(value = "hello") public String hello() { // omitted }
@GetMapping
や@PostMapping
を使用すると、シンプルにマッピングを定義することができ、意図しないHTTPメソッドのマッピング防止とソースコードの可読性向上が期待できる。リクエストとハンドラメソッドをマッピングするためのルールは、@RequestMapping
アノテーション、@RequestMapping
合成アノテーションの属性に指定する。
項番 属性名 説明
value マッピング対象にするリクエストパスを指定する(複数可)。
method マッピング対象にするHTTPメソッド(RequestMethod
型)を指定する(複数可)。GET/POSTについてはHTML form向けのリクエストをマッピングする際にも使用するが、それ以外のHTTPメソッド(PUT/DELETEなど)はREST API向けのリクエストをマッピングする際に使用する。本ガイドラインではHTTPメソッドの指定はこの属性を使用せず、@GetMapping
/@PostMapping
/@PutMapping
/@DeleteMapping
などの@RequestMapping
合成アノテーションを使用することを推奨する。
params マッピング対象にするリクエストパラメータを指定する(複数可)。主にHTML form向けのリクエストをマッピングする際に使用する。このマッピング方法を使用すると、HTML form上に複数のボタンが存在する場合のマッピングを簡単に実現する事ができる。
headers マッピング対象とするリクエストヘッダを指定する(複数可)。主にREST APIやAjax向けのリクエストをマッピングする際に使用する。
consumes リクエストのContent-Typeヘッダを使ってマッピングすることが出来る。マッピング対象とするメディアタイプを指定する(複数可)。主にREST APIやAjax向けのリクエストをマッピングする際に使用する。
produces リクエストのAcceptヘッダを使ってマッピングすることが出来る。マッピング対象とするメディアタイプを指定する(複数可)。主にREST APIやAjax向けのリクエストをマッピングする際に使用する。Note
マッピングの組み合わせについて
複数の属性を組み合わせることで複雑なマッピングを行うことも可能だが、保守性を考慮し、可能な限りシンプルな定義になるようにマッピングの設計を行うこと。2つの属性の組み合わせ(value属性と別の属性1つ)を目安にすることを推奨する。
@Controller // (1) @RequestMapping("sample") // (2) public class SampleController { // omitted }
項番 説明 (1)@Controller
アノテーションを付加することでAnnotation-basedなコントローラークラスとして認識され、component scanの対象となる。 (2)クラスレベルで
@RequestMapping("sample")
アノテーションを付けることでこのクラス内のハンドラメソッドがsample配下のURLにマッピングされる。Note
@RequestMapping
の値(value属性)を省略した場合、サーブレットルート(”/
” )のURLにマッピングされる。
3.4.1.2.1. HTTPメソッドでマッピング¶
下記の定義の場合、sample
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。
@GetMapping public String hello() {
sample
というURLにPOSTメソッドでアクセスすると、helloメソッドが実行される。@PostMapping public String hello() {
Note
1つのハンドラメソッドに対して複数のHTTPメソッドを指定したい場合
1つのハンドラメソッドに対して@GetMapping
や@PostMapping
を同時に使用することはできない。この場合は@RequestMapping
を使用し、method属性に複数の値を指定することで実現できる。
下記の定義の場合、sample/hello
というURLにGET又はPOSTメソッドでアクセスすると、helloメソッドが実行される。
@RequestMapping(value = "hello", method = {RequestMethod.GET, RequestMethod.POST}) public String hello() {
ただし、HTTPメソッドを複数指定することにより機能障害やセキュリティホールに繋がる可能性がある。
ハンドラメソッドの目的に応じて使用するHTTPリクエストメソッドを1つに絞り、@GetMapping
や@PostMapping
を使用することを推奨する。
3.4.1.2.2. リクエストパスでマッピング¶
下記の定義の場合、sample/hello
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。
@GetMapping(value = "hello") public String hello() {
sample/hello
又はsample/bonjour
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。@GetMapping(value = {"hello", "bonjour"}) public String hello() {
指定するリクエストパスは、具体的な値ではなくパターンを指定することも可能である。パターン指定の詳細は、Spring Framework Documentation -URI patterns-を参照されたい。
3.4.1.2.3. リクエストパラメータでマッピング¶
sample/hello?form
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。@GetMapping(value = "hello", params = "form") public String hello() {
sample/hello?form&formType=foo
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。@GetMapping(value = "hello", params = {"form", "formType=foo"}) public String hello(@RequestParam("formType") String formType) {
サポートされている指定形式は以下の通り。
項番 形式 説明
paramName 指定したparameNameのリクエストパラメータが存在する場合にマッピングされる。
!paramName 指定したparameNameのリクエストパラメータが存在しない場合にマッピングされる。
paramName=paramValue 指定したparameNameの値がparamValueの場合にマッピングされる。
paramName!=paramValue 指定したparameNameの値がparamValueでない場合にマッピングされる。
3.4.1.3. リクエストとハンドラメソッドのマッピング方針¶
以下の方針でマッピングを行うことを推奨する。
- 業務や機能といった意味のある単位で、リクエストのURLをグループ化する。URLのグループ化とは、
@RequestMapping(value = "xxx")
をクラスレベルのアノテーションとして定義することを意味する。 - 処理内の画面フローで使用するリクエストのURLは、同じURLにする。同じURLとは
@RequestMapping(value = "xxx")
のvalue属性の値を同じ値にすることを意味する。処理内の画面フローで使用するハンドラメソッドの切り替えは、HTTPメソッドとHTTPパラメータによって行う。 - ハンドラメソッドには@RequestMappingではなく、@GetMappingや@PostMappingなどの@RequestMapping合成アノテーションを使用する意図しないHTTPメソッドのマッピング防止と可読性の向上のために
@RequestMapping
合成アノテーションの使用を推奨する。
Warning
Spring MVCでは @RequestMapping(value = "xxx")
のvalue属性によってリクエストがマッピングされる際、サーブレットパスとパス情報は区別されず、パス情報が存在する場合はパス情報、存在しない場合はサーブレットパスがマッピングに利用される。
そのため、サーブレットパスとパス情報に同一のパスを設定した場合、意図せぬパス(URL)がマッピングされる可能性がある。
具体的には、リクエストパスでマッピングのようにハンドラメソッドにマッピングするパスを「/sample/hello
」と定義した場合、web.xmlでサーブレットパスを同じ「/sample/hello/*
」と定義すると、本来マッピングしたい”/sample/hello/sample/hello”だけでなく、意図しない”/sample/hello”もマッピングされてしまう。
業務上、意図せぬパス(URL)でハンドラメソッドにアクセスできてしまう可能性があり、また、Spring MVCのリクエストマッピング(@RequestMapping
)ではサーブレット内のパスを指定するのに対し、Spring Security(Servlet Filter)の認可(<sec:intercept-url>
)ではWebアプリケーション内のパスを指定する。このため、意図しないパス(上記の場合、”/sample/hello”)への認可設定が漏れ、認可をバイパスされる脆弱性を作りこんでしまう恐れがある。
サーブレットパスとパス情報には異なる値を設定するようにされたい。
以下にベーシックな画面フローを行うサンプルアプリケーションを例にして、リクエストとハンドラメソッドの具体的なマッピング例を示す。
3.4.1.3.1. サンプルアプリケーションの概要¶
サンプルアプリケーションの機能概要は以下の通り。
- EntityのCRUD処理を行う機能を提供する。
- 以下の5つの処理を提供する。
項番 処理名 処理概要 Entity一覧取得 作成済みのEntityを全て取得し、一覧画面に表示する。 Entity新規作成 指定した内容で新たにEntityを作成する。処理内には、画面フロー(フォーム画面、確認画面、完了画面)が存在する。 Entity参照 指定されたIDのEntityを取得し、詳細画面に表示する。 Entity更新 指定されたIDのEntityを更新する。処理内には、画面フロー(フォーム画面、確認画面、完了画面)が存在する。 Entity削除 指定されたIDのEntityを削除する。 - 機能全体の画面フローは以下の通り。画面フロー図には記載していないが、入力チェックエラーが発生した場合はフォーム画面を再描画するものとする。
3.4.1.3.2. リクエストURL¶
必要となるリクエストのURLの設計を行う。
- 機能内で必要となるリクエストのリクエストURLをグループ化する。ここではAbcというEntityのCRUD操作を行う機能となるので、
/abc/
から始まるURLとする。 処理毎にリクエストURLを設ける。
項番 処理名 処理毎のURL(パターン) Entity一覧取得 /abc/list Entity新規作成 /abc/create Entity参照 /abc/{id} Entity更新 /abc/{id}/update Entity削除 /abc/{id}/delete Note
Entity参照、Entity更新、Entity削除処理のURL内に指定している
{id}
は、URI patternsと呼ばれ、任意の値を指定する事ができる。サンプルアプリケーションでは、操作するEntityのIDを指定する。
画面フロー図に各処理に割り振られたURLをマッピングすると以下のようになる。
3.4.1.3.3. リクエストとハンドラメソッドのマッピング¶
項番 処理名 URL リクエスト名 HTTPメソッド HTTPパラメータ ハンドラメソッド
Entity一覧取得 /abc/list 一覧表示 GET - list
Entity新規作成 /abc/create フォーム表示 GET form createForm
入力内容確認表示 POST confirm createConfirm
フォーム再表示 POST redo createRedo
新規作成 POST - create
新規作成完了表示 GET complete createComplete
Entity参照 /abc/{id} 詳細表示 GET - read
Entity更新 /abc/{id}/update フォーム表示 GET form updateForm
入力内容確認表示 POST confirm updateConfirm
フォーム再表示 POST redo updateRedo
更新 POST - update
更新完了表示 GET complete updateComplete
Entity削除 /abc/{id}/delete 削除 POST - delete
削除完了表示 GET complete deleteComplete
/abc/create
で、HTTPメソッドとHTTPパラメータの組み合わせでハンドラメソッドを切り替えている点に注目すること。@RequestMapping
、@GetMapping
、@PostMapping
の書き方に注目すること。3.4.1.3.4. フォーム表示の実装¶
フォーム表示する場合は、HTTPパラメータとしてform
を指定させる。
@GetMapping(value = "create", params = "form") // (1) public String createForm(AbcForm form, Model model) { // omitted return "abc/createForm"; // (2) }
項番 説明 (1)@GetMapping
を使用し、params属性にform
を指定する。 (2)フォーム画面を描画するためのThymeleafによって生成されるHTMLのView名を返却する。
以下に、ハンドラメソッド以外の部分の実装例についても説明しておく。
フォーム表示を行う場合、ハンドラメソッドの実装以外に、
- フォームオブジェクトの生成処理の実装。
- フォーム画面のViewの実装。
以下のフォームオブジェクトを使用する。
public class AbcForm implements Serializable { private static final long serialVersionUID = 1L; @NotEmpty private String input1; @NotNull @Min(1) @Max(10) private Integer input2; // omitted setter&getter }
フォームオブジェクトを生成する。
@ModelAttribute public AbcForm setUpAbcForm() { return new AbcForm(); }
フォーム画面のView(テンプレートHTML)を作成する。
<h1>Abc Create Form</h1> <form th:action="@{/abc/create}" th:object="${abcForm}" method="post"> <label for="input1">Input1</label> <input th:field="*{input1}"> <span th:errors="*{input1}"></span> <br> <label for="input2">Input2</label> <input th:field="*{input2}"> <span th:errors="*{input2}"></span> <br> <input type="submit" name="confirm" value="Confirm"> <!-- (1) --> </form>
項番 説明 (1)確認画面へ遷移するためのsubmitボタンには name="confirm"
というパラメータを指定しておく。
以下に、フォーム表示の動作について説明する。
abc/create?form
というURIにアクセスする。form
というHTTPパラメータの指定があるため、ControllerのcreateFormメソッドが呼び出されフォーム画面が表示される。3.4.1.3.5. 入力内容確認表示の実装¶
フォームの入力内容を確認する場合は、POSTメソッドでデータを送信し、HTTPパラメータに confirm
を指定させる。
@PostMapping(value = "create", params = "confirm") // (1) public String createConfirm(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); // return "abc/createForm"; (2) } // omitted return "abc/createConfirm"; // (3) }
項番 説明 (1)@PostMapping
を使用し、params属性にconfirm
を指定する。 (2)入力チェックエラーが発生した場合の処理は、フォーム再表示用のハンドラメソッドを呼び出すことを推奨する。フォーム画面を再表示するための処理の共通化を行うことができる。 (3)入力内容確認画面を描画するためのThymeleafによって生成されるHTMLのView名を返却する。 Note
POSTメソッドを指定させる理由は、個人情報やパスワードなどの秘密情報がブラウザのアドレスバーに現れ、他人に容易に閲覧されることを防ぐためである。(もちろんセキュリティ対策としては十分ではなく、SSLなどのセキュアなサイトにする必要がある)。
以下に、ハンドラメソッド以外の部分の実装例についても説明しておく。
入力内容確認表示を行う場合、ハンドラメソッドの実装以外に、
- 入力内容確認画面のViewの実装。
入力内容確認画面のView(テンプレートHTML)を作成する。
<h1>Abc Create Form</h1> <form th:action="@{/abc/create}" th:object="${abcForm}" method="post"> <label for="input1">Input1</label> <span th:text="*{input1}"></span> <input th:field="*{input1}" type="hidden"> <!-- (1) --> <br> <label for="input2">Input2</label> <span th:text="*{input2}"></span> <input th:field="*{input2}" type="hidden"> <!-- (1) --> <br> <input type="submit" name="redo" value="Back"> <!-- (2) --> <input type="submit" value="Create"> <!-- (3) --> </form>
項番 説明 (1)フォーム画面で入力された値は、Createボタン及びBackボタンが押下された際に再度サーバに送る必要があるため、HTML formのhidden項目とする。 (2)フォーム画面に戻るためのsubmitボタンには name="redo"
というパラメータを指定しておく。 (3)新規作成を行うためのsubmitボタンにはパラメータ名の指定は不要。 Note
th:text
属性を使用すると、値をHTMLエスケープして表示することができる。XSS対策のため、HTMLエスケープは必ず行うこと。詳細についてはOutput Escapingを参照されたい。
以下に、入力内容確認の動作について説明する。
aa
を、Input2に”5
”を入力し、Confirmボタンを押下する。abc/create?confirm
というURIにPOSTメソッドでアクセスする。confirm
というHTTPパラメータがあるため、ControllerのcreateConfirmメソッドが呼び出され、入力内容確認画面が表示される。Confirmボタンを押下するとPOSTメソッドでHTTPパラメータが送信されるため、URIには現れていないが、HTTPパラメータとしてconfirm
が含まれている。
3.4.1.3.6. フォーム再表示の実装¶
フォームを再表示する場合は、HTTPパラメータにredoを指定させる。
@PostMapping(value = "create", params = "redo") // (1) public String createRedo(AbcForm form, Model model) { // omitted return "abc/createForm"; // (2) }
項番 説明 (1)@PostMapping
を使用し、params属性にredo
を指定する。 (2)入力内容確認画面を描画するためのThymeleafによって生成されるHTMLのView名を返却する。
以下に、フォーム再表示の動作について説明する。
abc/create?redo
というURIにPOSTメソッドでアクセスする。redo
というHTTPパラメータがあるため、ControllerのcreateRedoメソッドが呼び出され、フォーム画面が再表示される。Backボタンを押下するとPOSTメソッドでHTTPパラメータが送信されるため、URIには現れていないが、HTTPパラメータとしてredo
が含まれている。また、フォームの入力値をhidden項目として送信されるため、フォーム画面で入力値を復元することが出来る。
Note
戻るボタンの実現方法には、ボタンの属性に onclick="javascript:history.back()"
を設定する方法もある。両者では以下が異なり、要件に応じて選択する必要がある。
- ブラウザの戻るボタンを押した場合の挙動
- 戻るボタンがあるページに直接アクセスして戻るボタンを押した場合の挙動
- ブラウザの履歴
3.4.1.3.7. 新規作成の実装¶
@PostMapping(value = "create") // (1) public String create(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); // return "abc/createForm"; } // omitted return "redirect:/abc/create?complete"; // (2) }
項番 説明 (1)@PostMapping
を使用し、params属性は指定しない。 (2)PRGパターンとするため、新規作成完了表示リクエストにリダイレクトするためのURLをView名として返却する。 Note
“redirect:/xxx”を返却すると”/xxx”へリダイレクトさせることができる。
Warning
PRGパターンとすることで、ブラウザのF5ボタン押下時のリロードによる二重送信を防ぐ事はできるが、二重送信の対策としては十分ではない。二重送信の対策としては、共通部品として提供しているTransactionTokenCheckを行う必要がある。
TransactionTokenCheckの詳細については二重送信防止を参照されたい。
以下に、「新規作成」の動作について説明する。
abc/create
というURIにPOSTメソッドでアクセスする。/abc/create?complete
)へリダイレクトしているため、HTTPステータスが302になっている。3.4.1.3.8. 新規作成完了表示の実装¶
新規作成処理が完了した事を通知する場合は、HTTPパラメータにcomplete
を指定させる。
@GetMapping(value = "create", params = "complete") // (1) public String createComplete() { // omitted return "abc/createComplete"; // (2) }
項番 説明 (1)@GetMapping
を使用し、params属性にcomplete
を指定する。 (2)新規作成完了画面を描画するため、Thymeleafによって生成されるHTMLのView名を返却する。
以下に、「新規作成完了表示」の動作について説明する。
/abc/create?complete
)にアクセスする。complete
というHTTPパラメータがあるため、ControllerのcreateCompleteメソッドが呼び出され、新規作成完了画面が表示される。3.4.1.3.9. HTML form上に複数のボタンを配置する場合の実装¶
下図のように、入力内容確認画面のフォームには、新規作成を行うCreateボタンと新規作成フォーム画面を再表示するBackボタンが存在する。
Backボタンを押下した場合、新規作成フォーム画面を再表示するためのリクエスト( /abc/create?redo
)を送信する必要があるため、
HTML form内に以下のコードが必要となる。
<input type="submit" name="redo" value="Back"> <!-- (1) --> <input type="submit" value="Create">
項番 説明 (1)上記のように、入力内容確認画面( abc/createConfirm.html
)のBackボタンにname="redo"
というパラメータを指定する。
Backボタン押下時の動作については、フォーム再表示の実装を参照されたい。
3.4.1.3.10. サンプルアプリケーションのControllerのソースコード¶
@Controller @RequestMapping("abc") public class AbcController { @ModelAttribute public AbcForm setUpAbcForm() { return new AbcForm(); } // Handling request of "GET /abc/create?form" @GetMapping(value = "create", params = "form") public String createForm(AbcForm form, Model model) { // omitted return "abc/createForm"; } // Handling request of "POST /abc/create?confirm" @PostMapping(value = "create", params = "confirm") public String createConfirm(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); } // omitted return "abc/createConfirm"; } // Handling request of "POST /abc/create?redo" @PostMapping(value = "create", params = "redo") public String createRedo(AbcForm form, Model model) { // omitted return "abc/createForm"; } // Handling request of "POST /abc/create" @PostMapping(value = "create") public String create(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); } // omitted return "redirect:/abc/create?complete"; } // Handling request of "GET /abc/create?complete" @GetMapping(value = "create", params = "complete") public String createComplete() { // omitted return "abc/createComplete"; } }
3.4.1.4. ハンドラメソッドの引数について¶
ハンドラメソッドの引数は様々な値をとることができるが、基本的には次に挙げるものは原則として使用しないこと。
- ServletRequest
- HttpServletRequest
- org.springframework.web.context.request.WebRequest
- org.springframework.web.context.request.NativeWebRequest
- java.io.InputStream
- java.io.Reader
- java.io.OutputStream
- java.io.Writer
- java.util.Map
- org.springframework.ui.ModelMap
Note
HttpServletRequest
のgetAttribute/setAttributeやMap
のget/putのような汎用的なメソッドの利用を許可すると自由な値の受け渡しができてしまい、プロジェクトの規模が大きくなると保守性を著しく低下させる可能性がある。
同様の理由で、他で代替できる場合はHttpSession
を極力使用しないことを推奨する。
共通的なパラメータ(リクエストパラメータ)をJavaBeanに格納してControllerの引数に渡したい場合は、後述のHandlerMethodArgumentResolverの実装を使用することで実現できる。
以下に、引数の使用方法について、目的別に13例示す。
- 画面(View)にデータを渡す
- URLのパスから値を取得する
- リクエストパラメータを個別に取得する
- リクエストパラメータをまとめて取得する
- 入力チェックを行う
- リダイレクト先にデータを渡す
- リダイレクト先へリクエストパラメータを渡す
- リダイレクト先URLのパスに値を埋め込む
- Cookieから値を取得する
- Cookieに値を書き込む
- ページネーション情報を取得する
- アップロードファイルを取得する
- 画面に結果メッセージを表示する
3.4.1.4.1. 画面(View)にデータを渡す¶
画面(View)に表示するデータを渡したい場合は、org.springframework.ui.Model
(以降 Model
と呼ぶ) をハンドラメソッドの引数として受け取り、Model
オブジェクトに渡したいデータ(オブジェクト)を追加する。
SampleController.java
@GetMapping("hello") public String hello(Model model) { // (1) model.addAttribute("hello", "Hello World!"); // (2) model.addAttribute(new HelloBean("Bean Hello World!")); // (3) return "sample/hello"; // returns view name }
hello.html
<span th:text="${hello}"></span><br> <!--/* (4) */--> <span th:text="${helloBean.message}"></span><br> <!--/* (5) */-->
HTML of created by View(hello.html)
<span>Hello World!</span><br> <!-- (6) --> <span>Bean Hello World!</span><br> <!-- (6) -->
項番 説明 (1)Model
オブジェクトを引数として受け取る。(2)引数で受け取ったModel
オブジェクトのaddAttribute
メソッドを呼び出し、渡したいデータをModel
オブジェクトに追加する。例では、hello
という属性名でHello World!
という文字列のデータを追加している。(3)addAttribute
メソッドの第一引数を省略するとConventions#getVariableNameの仕様に基づき、値のクラス名から属性名を決定する。例では、model.addAttribute("helloBean", new HelloBean());
を行ったのと同じ結果となる。(4)テンプレートHTML側では、th:text
などの属性において${属性名}のような式を記述することできる。${}
は変数式で、Model
オブジェクトに追加したデータを取得することができる。例では、取得したデータをHTMLエスケープして出力するためにth:text
属性を利用し、「th:text=”${hello}”」としている。HTMLエスケープの詳細については、Output Escapingを参照されたい。(5)「${属性名.JavaBeanのプロパティ名}」と記述することでModel
に格納されているJavaBeanから値を取得することができる。(6)Thymeleafによって出力されるHTML。Note
Model
は使用しない場合でも引数に指定しておいてもよい。実装初期段階では必要なくても後で使う場合がある(後々メソッドのシグニチャを変更する必要がなくなる)。Note
Model
にaddAttribute
することで、HttpServletRequest
にsetAttribute
されるため、Spring MVCの管理下にないモジュール(例えばServletFilterなど)からも値を参照することが出来る。
3.4.1.4.2. URLのパスから値を取得する¶
@PathVariable
アノテーションを付与する。@PathVariable
アノテーションを使用してパスから値を取得する場合、 @GetMapping
アノテーションのvalue属性に取得したい部分を変数化しておく必要がある。@GetMapping("hello/{id}/{version}") // (1) public String hello( @PathVariable("id") String id, // (2) @PathVariable Integer version, // (3) Model model) { // do something return "sample/hello"; // returns view name }
項番 説明 (1)@GetMapping
アノテーションのvalue属性に、抜き出したい箇所をパス変数として指定する。パス変数は、「{変数名}」の形式で指定する。上記例では、id
とversion
という二つのパス変数を指定している。 (2)@PathVariable
アノテーションのvalue属性には、パス変数の変数名を指定する。上記例では、sample/hello/aaaa/1
というURLにアクセスした場合、引数idに文字列aaaa
が渡る。 (3)@PathVariable
アノテーションのvalue属性は省略可能で、省略した場合は引数名がリクエストパラメータ名となる。上記例では、sample/hello/aaaa/1
というURLにアクセスした場合、引数versionに数値 “1
” が渡る。ただしこの方法は、
-g
オプション(デバッグ情報を出力するモード)- Java8から追加された
-parameters
オプション(メソッド・パラメータにリフレクション用のメタデータを生成するモード)のどちらかを指定してコンパイルする必要がある。
Note
バインドする引数の型はString以外でも良い。型が合わない場合は
org.springframework.beans.TypeMismatchException
がスローされ、デフォルトの動作は400(Bad Request)が応答される。例えば、上記例で
sample/hello/aaaa/v1
というURLでアクセスした場合、v1
をIntegerに変換できないため、例外がスローされる。Warning
@PathVariable
アノテーションのvalue属性を省略する場合、デプロイするアプリケーションは-g
オプション又はJava8から追加された-parameters
オプションを指定してコンパイルする必要がある。これらのオプションを指定した場合、コンパイル後のクラスにはデバッグ時に必要となる情報や処理などが挿入されるため、メモリや処理性能に影響を与えることがあるので注意が必要である。
基本的には、value属性を明示的に指定する方法を推奨する。
Warning
Spring Framework 5.3.0より、パスの最後をパス変数にする場合、バインドされる値に拡張子が含まれるように変更された。
これはSpring MVCにおいてリクエストパスの拡張子によるパターンマッチングが非推奨となったことによる影響で、従来は拡張子がパス変数と別に扱われていたが、パス変数の一部として扱われるようになったためである。
これを回避するには以下の2種類の方法がある。
mvc:annotation-driven
の設定でsuffix-pattern
を有効にする(全体)<mvc:annotation-driven> <!-- ommitted --> <mvc:path-matching suffix-pattern="true" /> </mvc:annotation-driven>
@GetMapping
で拡張子無しと有りの両方のパスにマッピングする(個別)@GetMapping({ "hello/{id}/{version}", "hello/{id}/{version}.*" }) public String hello( @PathVariable("id") String id, @PathVariable Integer version, Model model) { // do something return "sample/hello"; // returns view name }なお、リクエストパスの拡張子によるパターンマッチングはブラウザから送信されるAcceptヘッダーを一貫して解釈することが困難だった古い時代の手法であり、拡張子ではなくAcceptヘッダーやURLのクエリパラメータでマッピングを切り分けることが、Springでは推奨されている。
詳細は Spring Framework Documentation -Suffix Match-を参照されたい。
3.4.1.4.3. リクエストパラメータを個別に取得する¶
リクエストパラメータを1つずつ取得したい場合は、引数に@RequestParam
アノテーションを付与する。
@GetMapping("bindRequestParams") public String bindRequestParams( @RequestParam("id") String id, // (1) @RequestParam String name, // (2) @RequestParam(value = "age", required = false) Integer age, // (3) @RequestParam(value = "genderCode", required = false, defaultValue = "unknown") String genderCode, // (4) Model model) { // do something return "sample/hello"; // returns view name }
項番 説明 (1)@RequestParam
アノテーションのvalue属性には、リクエストパラメータ名を指定する。上記例では、sample/hello?id=aaaa
というURLにアクセスした場合、引数idに文字列aaaa
が渡る。 (2)@RequestParam
アノテーションのvalue属性は省略可能で、省略した場合は引数名がリクエストパラメータ名となる。上記例では、sample/hello?name=bbbb&....
というURLにアクセスした場合、引数nameに文字列bbbb
が渡る。ただしこの方法は、
-g
オプション(デバッグ情報を出力するモード)- Java8から追加された
-parameters
オプション(メソッド・パラメータにリフレクション用のメタデータを生成するモード)のどちらかを指定してコンパイルする必要がある。
(3) デフォルトの動作では、指定したリクエストパラメータが存在しないとエラーとなる。リクエストパラメータが存在しないケースを許容する場合は、required属性をfalse
に指定する。上記例では、age
というリクエストパラメータがない状態でアクセスした場合、引数ageにnull
が渡る。 (4) 指定したリクエストパラメータが存在しない場合にデフォルト値を使用したい場合は、defaultValue属性にデフォルト値を指定する。上記例では、genderCode
というリクエストパラメータがない状態でアクセスした場合、引数genderCodeにunknown
が渡る。Note
必須パラメータを指定しないでアクセスした場合は、
org.springframework.web.bind.MissingServletRequestParameterException
がスローされ、デフォルトの動作は400(Bad Request)が応答される。ただし、defaultValue属性を指定している場合、例外はスローされず、defaultValue属性で指定した値が渡る。
Note
バインドする引数の型はString以外でも良い。型が合わない場合は
org.springframework.beans.TypeMismatchException
がスローされ、デフォルトの動作は400(Bad Request)が応答される。例えば、上記例で
sample/hello?age=aaaa&...
というURLでアクセスした場合、aaaa
をIntegerに変換できないため、例外がスローされる。
以下の条件に当てはまる場合は、次に説明するフォームオブジェクトにバインドすること。
- リクエストパラメータがHTML form内の項目である。
- リクエストパラメータはHTML form内の項目ではないが、リクエストパラメータに必須チェック以外の入力チェックを行う必要がある。
- リクエストパラメータの入力チェックエラーのエラー詳細をパラメータ毎に出力する必要がある。
- 3つ以上のリクエストパラメータをバインドする。(保守性、可読性の観点)
3.4.1.4.4. リクエストパラメータをまとめて取得する¶
以下は、@RequestParam
で個別にリクエストパラメータを受け取っていたハンドラメソッドを、フォームオブジェクトで受け取るように変更した場合の実装例である。
@RequestParam
を使って個別にリクエストパラメータを受け取っているハンドラメソッドは以下の通り。
@GetMapping("bindRequestParams") public String bindRequestParams( @RequestParam("id") String id, @RequestParam String name, @RequestParam(value = "age", required = false) Integer age, @RequestParam(value = "genderCode", required = false, defaultValue = "unknown") String genderCode, Model model) { // do something return "sample/hello"; // returns view name }
public class SampleForm implements Serializable{ private static final long serialVersionUID = 1477614498217715937L; private String id; private String name; private Integer age; private String genderCode; // omit setters and getters }Note
リクエストパラメータ名とフォームオブジェクトのプロパティ名は一致させる必要がある。
上記のフォームオブジェクトに対して id=aaa&name=bbbb&age=19&genderCode=men?tel=01234567
というパラメータが送信された場合、id
,name
,age
,genderCode
は名前が一致するプロパティに値が格納されるが、tel
は名前が一致するプロパティがないため、フォームオブジェクトに取り込まれない。
@RequestParam
を使って個別に受け取っていたリクエストパラメータをフォームオブジェクトとして受け取るようにする。
@GetMapping("bindRequestParams") public String bindRequestParams(@Validated SampleForm form, // (1) BindingResult result, Model model) { // do something return "sample/hello"; // returns view name }
項番 説明 (1)SampleForm
オブジェクトを引数として受け取る。Note
フォームオブジェクトを引数に用いた場合、
@RequestParam
の場合とは異なり、必須チェックは行われない。フォームオブジェクトを使用する場合は、次に説明する入力チェックを行うを行うこと。Warning
EntityなどDomainオブジェクトをそのままフォームオブジェクトとして使うこともできるが、実際には、WEBの画面上にしか存在しないパラメータ(確認用パスワードや、規約確認チェックボックス等)が存在する。
Domainオブジェクトにそのような画面項目に依存する項目を入れるべきではないので、Domainオブジェクトとは別にフォームオブジェクト用のクラスを作成することを推奨する。
リクエストパラメータからDomainオブジェクトを作成する場合は、一旦フォームオブジェクトにバインドしてからプロパティ値をDomainオブジェクトにコピーすること。
3.4.1.4.5. 入力チェックを行う¶
リクエストパラメータがバインドされているフォームオブジェクトに対して入力チェックを行う場合は、フォームオブジェクト引数に@Validated
アノテーションを付け、フォームオブジェクト引数の直後にorg.springframework.validation.BindingResult
(以降BindingResult
と呼ぶ) を引数に指定する。
入力チェックの詳細については、入力チェックを参照されたい。
フォームオブジェクトクラスのフィールドに入力チェックで必要となるアノテーションを付加する。
public class SampleForm implements Serializable { private static final long serialVersionUID = 1477614498217715937L; @NotNull @Size(min = 10, max = 10) private String id; @NotNull @Size(min = 1, max = 10) private String name; @Min(1) @Max(100) private Integer age; @Size(min = 1, max = 10) private Integer genderCode; // omit setters and getters }
@Validated
アノテーションを付与する。@Validated
アノテーションを付けた引数は、ハンドラメソッド実行前に入力チェックが行われ、チェック結果が直後のBindingResult
引数に格納される。BindingResult
に格納されている。@GetMapping("bindRequestParams") public String bindRequestParams(@Validated SampleForm form, // (1) BindingResult result, // (2) Model model) { if (result.hasErrors()) { // (3) return "sample/input"; // back to the input view } // do something return "sample/hello"; // returns view name }
項番 説明 (1)SampleForm
オブジェクトに@Validated
アノテーションを付与し、入力チェック対象のオブジェクトにする。 (2)入力チェック結果が格納される BindingResult
を引数に指定する。 (3)入力チェックエラーが存在するか判定する。エラーがある場合は、 true
が返却される。
3.4.1.4.6. リダイレクト先にデータを渡す¶
ハンドラメソッドを実行した後にリダイレクトする場合に、リダイレクト先で表示するデータを渡したい場合は、org.springframework.web.servlet.mvc.support.RedirectAttributes
(以降RedirectAttributes
と呼ぶ) をハンドラメソッドの引数として受け取り、
RedirectAttributes
オブジェクトに渡したいデータを追加する。
SampleController.java
@GetMapping("hello") public String hello(RedirectAttributes redirectAttrs) { // (1) redirectAttrs.addFlashAttribute("hello", "Hello World!"); // (2) redirectAttrs.addFlashAttribute(new HelloBean("Bean Hello World!")); // (3) return "redirect:/sample/hello?complete"; // (4) } @GetMapping(value = "hello", params = "complete") public String helloComplete() { return "sample/complete"; // (5) }
complete.html
<span th:text="${hello}"></span><br> <!--/* (6) */--> <span th:text="${helloBean.message}"></span><br> <!--/* (7) */-->
HTML of created by View(complete.html)
<span>Hello World!</span><br> <!-- (8) --> <span>Bean Hello World!</span><br> <!-- (8) -->
項番 説明 (1)RedirectAttributes
オブジェクトを引数として受け取る。 (2)RedirectAttributes
オブジェクトのaddFlashAttribute
メソッドを呼び出し、渡したいデータをRedirectAttributes
オブジェクトに追加する。例では、hello
という属性名でHello World!
という文字列のデータを追加している。 (3)addFlashAttribute
メソッドの第一引数を省略するとConventions#getVariableNameの仕様に基づき、値のクラス名から属性名を決定する。例では、model.addFlashAttribute("helloBean", new HelloBean());
を行ったのと同じ結果となる。 (4) 画面(View)を直接表示せず、次の画面を表示するためのリクエストにリダイレクトする。 (5) リダイレクト後のハンドラメソッドでは、(2)(3)で追加したデータを表示する画面のView名を返却する。 (6) View(テンプレートHTML)側では、th:text
などの属性において${属性名}のような式を記述することできる。${}
は変数式で、Model
オブジェクトだけでなくRedirectAttributes
を通じてflash scopeに追加したデータも取得することができる。例では、取得したデータをHTMLエスケープして出力するためにth:text
属性を利用し、「th:text=”${hello}”」としている。HTMLエスケープの詳細については、Output Escapingを参照されたい。 (7) 「${属性名.JavaBeanのプロパティ名}」と記述することでRedirectAttributes
に格納されているJavaBeanから値を取得することができる。 (8) Thymeleafによって出力されるHTML。
Warning
Model
に追加してもリダイレクト先にデータを渡すことはできない。
Note
Model
のaddAttribute
メソッドに非常によく似ているが、データの生存期間が異なる。
RedirectAttributes
のaddFlashAttribute
ではflash scopeというスコープにデータが格納され、リダイレクト後の1リクエスト(PRGパターンのG)でのみ追加したデータを参照することができる。2回目以降のリクエストの時にはデータは消えている。
3.4.1.4.7. リダイレクト先へリクエストパラメータを渡す¶
リダイレクト先へ動的にリクエストパラメータを設定したい場合は、引数のRedirectAttributes
オブジェクトに渡したい値を追加する。
@GetMapping("hello") public String hello(RedirectAttributes redirectAttrs) { String id = "aaaa"; redirectAttrs.addAttribute("id", id); // (1) // must not return "redirect:/sample/hello?complete&id=" + id; return "redirect:/sample/hello?complete"; }
項番 説明 (1) 属性名にリクエストパラメータ名、属性値にリクエストパラメータの値を指定して、RedirectAttributes
オブジェクトのaddAttribute
メソッドを呼び出す。上記例では、/sample/hello?complete&id=aaaa
にリダイレクトされる。
Warning
上記例ではコメント化しているが、return "redirect:/sample/hello?complete&id=" + id;
と結果は同じになる。ただし、 RedirectAttributes
オブジェクトのaddAttribute
メソッドを用いるとURIエンコーディングも行われるので、動的に埋め込むリクエストパラメータについては、返り値のリダイレクトURLとして組み立てるのではなく、必ずaddAttributeメソッドを使用してリクエストパラメータに設定すること。
動的に埋め込まないリクエストパラメータ(上記例だと”complete”)については、返り値のリダイレクトURLに直接指定してよい。
3.4.1.4.8. リダイレクト先URLのパスに値を埋め込む¶
リダイレクト先URLのパスに動的に値を埋め込みたい場合は、リクエストパラメータの設定と同様引数のRedirectAttributes
オブジェクトに埋め込みたい値を追加する。
@GetMapping("hello") public String hello(RedirectAttributes redirectAttrs) { String id = "aaaa"; redirectAttrs.addAttribute("id", id); // (1) // must not return "redirect:/sample/hello/" + id + "?complete"; return "redirect:/sample/hello/{id}?complete"; // (2) }
項番 説明 (1) 属性名とパスに埋め込みたい値を指定して、RedirectAttributes
オブジェクトのaddAttribute
メソッドを呼び出す。 (2) リダイレクトURLの埋め込みたい箇所に「{属性名}」のパス変数を指定する。上記例では、/sample/hello/aaaa?complete
にリダイレクトされる。
Warning
上記例ではコメント化しているが、"redirect:/sample/hello/" + id + "?complete";
と結果は同じになる。ただし、 RedirectAttributes
オブジェクトのaddAttribute
メソッドを用いるとURLエンコーディングも行われるので、動的に埋め込むパス値については、返り値のリダイレクトURLとして記述せずに、必ずaddAttributeメソッドを使用し、パス変数を使って埋め込むこと。
3.4.1.4.9. Cookieから値を取得する¶
Cookieから取得したい場合は、引数に@CookieValue
アノテーションを付与する。
@GetMapping("readCookie") public String readCookie(@CookieValue("JSESSIONID") String sessionId, Model model) { // (1) // do something return "sample/readCookie"; // returns view name }
項番 説明 (1)@CookieValue
アノテーションのvalue属性には、Cookie名を指定する。上記例では、Cookieから”JSESSIONID”というCookie名の値が引数sessionIdに渡る。
Note
@RequestParam
同様、required属性、defaultValue属性があり、引数の型にはString型以外の指定も可能である。
詳細は、リクエストパラメータを個別に取得するを参照されたい。
3.4.1.4.10. Cookieに値を書き込む¶
HttpServletResponse
オブジェクトのaddCookie
メソッドを直接呼び出してCookieに追加する。@GetMapping("writeCookie") public String writeCookie(Model model, HttpServletResponse response) { // (1) Cookie cookie = new Cookie("foo", "HelloWorld!"); response.addCookie(cookie); // (2) // do something return "sample/writeCookie"; }
項番 説明 (1)Cookieを書き込むために、 HttpServletResponse
オブジェクトを引数に指定する。 (2)Cookie
オブジェクトを生成し、HttpServletResponse
オブジェクトに追加する。上記例では、foo
というCookie名でHelloWorld!
という値を設定している。
Tip
HttpServletResponse
を引数として受け取ることに変わりはないが、Cookieに値を書き込むためのクラスとして、Spring Frameworkからorg.springframework.web.util.CookieGenerator
というクラスが提供されている。必要に応じて使用すること。
Note
HTTP Cookieの処理を規定するRFC 6265では、Cookieの名前や値に一部使用できない文字があることに注意されたい。
RFC 6265(HTTP State Management Mechanism)の4.1 SetCookieのSyntaxを参照されたい。
3.4.1.4.11. ページネーション情報を取得する¶
org.springframework.data.domain.Pageable
(以降Pageable
と呼ぶ) オブジェクトをハンドラメソッドの引数に取ることで、ページネーション情報(ページ数、取得件数)を容易に扱うことができる。詳細についてはページネーションを参照されたい。
3.4.1.4.12. アップロードファイルを取得する¶
アップロードされたファイルを取得する方法は大きく2つある。
- フォームオブジェクトに
MultipartFile
のプロパティを用意する。 @RequestParam
アノテーションを付与してorg.springframework.web.multipart.MultipartFile
をハンドラメソッドの引数とする。
詳細については ファイルアップロード を参照されたい。
3.4.1.4.13. 画面に結果メッセージを表示する¶
Model
オブジェクト又はRedirectAttributes
オブジェクトをハンドラメソッドの引数として受け取り、ResultMessages
オブジェクトを追加することで処理の結果メッセージを表示できる。
詳細については メッセージ管理 を参照されたい。
3.4.1.5. ハンドラメソッドの返り値について¶
ハンドラメソッドの返り値についても様々な値をとることができるが、基本的には次に挙げるもののみを使用すること。
- String(View名)
以下に、目的別に返り値の使用方法について説明する。
3.4.1.5.1. HTMLを応答する¶
ViewResolver
には、ThymeleafViewResolver
を用いる。ThymeleafViewResolver
の設定例については、ブランクプロジェクトの設定を参照されたい。SampleController.java
@GetMapping("hello") public String hello() { // omitted return "sample/hello"; // (1) }
項番 説明 (1)ハンドラメソッドの返り値として sample/hello
というView名を返却した場合、ThymeleafViewResolver
の設定によりテンプレートHTMLとして/WEB-INF/views/sample/hello.html
を利用して生成したHTMLが返される。
Note
JSPやFreeMarkerなど他のテンプレートエンジンを使用してHTMLを生成する場合でも、ハンドラメソッドの返り値はsample/hello
のままでよい。使用するテンプレートエンジンでの差分はViewResolver
によって解決される。
Note
単純にview 名を返すだけのメソッドを実装する場合は、<mvc:view-controller>
を使用してControllerクラスの実装を代用することも可能である。
<mvc:view-controller>
を使用したControllerの定義例。<mvc:view-controller path="/hello" view-name="sample/hello" />
Warning
<mvc:view-controller>使用に関する留意点
Spring Framework 4.3以降では、<mvc:view-controller>
が許可するHTTPメソッドはGETとHEADのみに限定される様になったため(SPR-13130)、HTTPメソッドがGETとHEAD以外(POSTなど)でアクセスするページの場合、<mvc:view-controller>
は使用できない。
GETとHEAD以外(POSTなど)からフォワードされた場合も同様となるため、エラーページへの遷移などフォワード元のHTTPメソッドが限定できない場合には<mvc:view-controller>
を使用しないよう注意されたい。
3.4.1.5.2. ダウンロードデータを応答する¶
application/octet-stream
等 )として応答する場合、レスポンスデータの生成(ダウンロード処理)を行うViewを作成し、処理を委譲することを推奨する。Model
に追加し、ダウンロード処理を行うViewのView名を返却する。BeanNameViewResolver
を使用する。spring-mvc.xml
<mvc:view-resolvers> <mvc:bean-name /> <!-- (1) --> <bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <property name="characterEncoding" value="UTF-8" /> <property name="forceContentType" value="true" /> <property name="contentType" value="text/html;charset=UTF-8" /> </bean> </mvc:view-resolvers>
SampleController.java
@GetMapping("report") public String report() { // omitted return "sample/report"; // (2) }
XxxExcelView.java
@Component("sample/report") // (3) public class XxxExcelView extends AbstractXlsxView { // (4) @Override protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception { Sheet sheet; Cell cell; sheet = workbook.createSheet("Spring"); sheet.setDefaultColumnWidth(12); // write a text at A1 cell = getCell(sheet, 0, 0); setText(cell, "Spring-Excel test"); cell = getCell(sheet, 2, 0); setText(cell, ((Date) model.get("serverTime")).toString()); } }
項番 説明 (1)<mvc:bean-name>
要素を使用して、BeanNameViewResolver
を定義する。<mvc:view-resolvers>
要素を使用してViewResolver
を定義する場合は、子要素に指定するViewResolver
の定義順が優先順位となる。(2)ハンドラメソッドの返り値として sample/report
というView名を返却した場合、 (3)でBean登録されたViewインスタンスによって生成されたデータがダウンロードデータとして応答される。(3)コンポーネントの名前にView名を指定して、ViewオブジェクトをBeanとして登録する。
上記例では、
sample/report
というbean名(View名)でx.y.z.app.views.XxxExcelView
のインスタンスがBean登録される。(4)Viewの実装例。
上記例では、
org.springframework.web.servlet.view.document.AbstractXlsxView
を継承し、Excelデータを生成するViewクラスの実装となる。
3.4.1.6. 処理の実装¶
Note
Controllerは、基本的には画面遷移の決定などの処理のルーティングとModel
の設定のみ実装することに徹し、可能な限りシンプルな状態に保つこと。
この方針で統一することにより、Controllerで実装すべき処理が明確になり、開発規模が大きくなった場合でもControllerのメンテナンス性を保つことができる。
Controllerで実装すべき処理を以下に4つ示す。
3.4.1.6.1. 入力値の相関チェック¶
org.springframework.validation.Validator
インタフェースを実装したValidationクラス、もしくは、Bean Validationで検証を行う。Validator
をorg.springframework.web.bind.WebDataBinder
に追加する必要がある。@Inject PasswordEqualsValidator passwordEqualsValidator; // (1) @InitBinder protected void initBinder(WebDataBinder binder){ binder.addValidators(passwordEqualsValidator); // (2) }
項番 説明 (1)相関チェックを行う Validator
をInjectする。 (2) InjectしたValidator
をWebDataBinder
に追加する。WebDataBinder
に追加しておくことで、ハンドラメソッド呼び出し前に行われる入力チェック処理にて、(1)で追加したValidator
が実行され、相関チェックを行うことが出来る。
3.4.1.6.2. 業務処理の呼び出し¶
業務処理が実装されているServiceをInjectし、InjectしたServiceのメソッドを呼び出すことで業務処理を実行する。
@Inject SampleService sampleService; // (1) @GetMapping("hello") public String hello(Model model){ String message = sampleService.hello(); // (2) model.addAttribute("message", message); return "sample/hello"; }
項番 説明 (1) 業務処理が実装されているService
をInjectする。 (2)Injectした Service
のメソッドを呼び出し、業務処理を実行する。
3.4.1.6.3. ドメインオブジェクトへの値反映¶
@GetMapping("hello") public String hello(@Validated SampleForm form, BindingResult result, Model model){ // omitted Sample sample = new Sample(); // (1) sample.setField1(form.getField1()); sample.setField2(form.getField2()); sample.setField3(form.getField3()); // ... // omitted // ... String message = sampleService.hello(sample); // (2) model.addAttribute("message", message); // (3) return "sample/hello"; }
項番 説明 (1) Serviceの引数となるドメインオブジェクトを生成し、フォームオブジェクトにバインドされている値を反映する。 (2)Serviceのメソッドを呼び出し、業務処理を実行する。 (3)業務処理から返却されたデータを Model
に追加する。
SampleController.java
@Inject SampleHelper sampleHelper; // (1) @GetMapping("hello") public String hello(@Validated SampleForm form, BindingResult result){ // omitted String message = sampleHelper.hello(form); // (2) model.addAttribute("message", message); return "sample/hello"; }
SampleHelper.java
public class SampleHelper { @Inject SampleService sampleService; public String hello(SampleForm form){ // (3) Sample sample = new Sample(); sample.setField1(form.getField1()); sample.setField2(form.getField2()); sample.setField3(form.getField3()); // ... // and more ... // ... String message = sampleService.hello(sample); return message; } }
項番 説明 (1)ControllerにHelperクラスのオブジェクトをInjectする。 (2)InjectしたHelperクラスのメソッドを呼び出すことで、ドメインオブジェクトへの値の反映を行っている。 Helperクラスに処理を委譲することで、Controllerの実装をシンプルな状態に保つことができる。 (3)ドメインオブジェクトを生成した後にServiceクラスのメソッド呼び出し、業務処理を実行している。
3.4.1.6.4. フォームオブジェクトへの値反映¶
@GetMapping("hello") public String hello(SampleForm form, BindingResult result, Model model){ // omitted Sample sample = sampleService.getSample(form.getId()); // (1) form.setField1(sample.getField1()); // (2) form.setField2(sample.getField2()); form.setField3(sample.getField3()); // ... // and more ... // ... model.addAttribute(sample); // (3) return "sample/hello"; }
項番 説明 (1)業務処理が実装されているServiceのメソッドを呼び出し、ドメインオブジェクトを取得する。 (2)取得したドメインオブジェクトの値をフォームオブジェクトに反映する。 (3)表示のみ行う項目がある場合は、データを参照できるようにするために、 Model
にドメインオブジェクトを追加する。Note
画面に表示のみ行う項目については、フォームオブジェクトに項目をもつのではなく、Entityなどのドメインオブジェクトから直接値を参照することを推奨する。
フォームオブジェクトへの値反映処理は、Controllerのハンドラメソッド内で実装してもよいが、コード量が多くなる場合はハンドラメソッドの可読性を考慮してHelperクラスのメソッドに委譲することを推奨する。
SampleController.java
@GetMapping("hello") public String hello(@Validated SampleForm form, BindingResult result){ // omitted Sample sample = sampleService.getSample(form.getId()); sampleHelper.applyToForm(sample, form); // (1) model.addAttribute(sample); return "sample/hello"; }
SampleHelper.java
public void applyToForm(SampleForm destForm, Sample srcSample){ destForm.setField1(srcSample.getField1()); // (2) destForm.setField2(srcSample.getField2()); destForm.setField3(srcSample.getField3()); // ... // and more ... // ... }
項番 説明 (1)ドメインオブジェクトの値をフォームオブジェクトに反映するためのメソッドを呼び出す。 (2)ドメインオブジェクトの値をフォームオブジェクトに反映するためのメソッドにて、ドメインオブジェクトの値をフォームオブジェクトに反映する。
3.4.2. フォームオブジェクトの実装¶
フォームオブジェクトはHTML上のformを表現するオブジェクト(JavaBean)であり、以下の役割を担う。
- データベース等で保持している業務データを保持し、HTML formから参照できるようにする。
- HTML formから送信されたリクエストパラメータを保持し、ハンドラメソッドで参照できるようにする。
フォームオブジェクトの実装について、以下4点に着目して説明する。
3.4.2.1. フォームオブジェクトの作成方法¶
java.lang.String
だけではなく、任意の型で定義することができる。public class SampleForm implements Serializable { private String id; private String name; private Integer age; private String genderCode; private Date birthDate; // ommitted getter/setter }Warning
フォームオブジェクトには画面に表示のみ行う項目は保持せず、HTML formの項目のみ保持することを推奨する。
フォームオブジェクトに画面表示のみ行う項目の値を設定した場合、フォームオブジェクトをHTTPセッションオブジェクトに格納する際にメモリを多く消費する事になり、メモリ枯渇の原因になる可能性がある。
画面表示のみの項目は、Entityなどのドメイン層のオブジェクトをリクエストスコープに追加(
Model.addAttribute
)することでテンプレートHTMLにデータを渡すことを推奨する。
3.4.2.1.1. フィールド単位の数値型変換¶
@NumberFormat
アノテーションを使用することでフィールド毎に数値の形式を指定することが出来る。
public class SampleForm implements Serializable { @NumberFormat(pattern = "#,#") // (1) private Integer price; // ommitted getter/setter }
項番 説明 (1)HTML formから送信されるリクエストパラメータの数値形式を指定する。例では、patternとして #,#
形式を指定しているので、「,」でフォーマットされた値をバインドすることができる。 リクエストパラメータの値が1,050
の場合、フォームオブジェクトのpriceには1050
のIntegerオブジェクトがバインドされる。
@NumberFormat
アノテーションで指定できる属性は以下の通り。
項番 属性名 説明
style 数値のスタイルを指定する。詳細は、NumberFormat.StyleのJavadocを参照されたい。
pattern Javaの数値形式を指定する。詳細は、DecimalFormatのJavadocを参照されたい。
3.4.2.1.2. フィールド単位の日時型変換¶
@DateTimeFormat
アノテーションを使用することでフィールド毎に日時の形式を指定することが出来る。
public class SampleForm implements Serializable { @DateTimeFormat(pattern = "yyyyMMdd") // (1) private Date birthDate; // ommitted getter/setter }
項番 説明 (1)HTML formから送信されるリクエストパラメータの日時形式を指定する。例では、patternとして yyyyMMdd
形式を指定している。 リクエストパラメータの値が20131001
の場合、フォームオブジェクトのbirthDateには 2013年10月1日のDateオブジェクトがバインドされる。
@DateTimeFormat
アノテーションで指定できる属性は以下の通り。
項番 属性名 説明
iso ISOの日時形式を指定する。詳細は、DateTimeFormat.ISOのJavadocを参照。
pattern Javaの日時形式を指定する。詳細は、SimpleDateFormatのJavadocを参照されたい。
style 日付と時刻のスタイルを2桁の文字列として指定する。1桁目が日付のスタイル、2桁目が時刻のスタイルとなる。スタイルとして指定できる値は以下の値となる。S :java.text.DateFormat.SHORT
と同じ形式となる。M :java.text.DateFormat.MEDIUM
と同じ形式となる。L :java.text.DateFormat.LONG
と同じ形式となる。F :java.text.DateFormat.FULL
と同じ形式となる。- : 省略を意味するスタイル。指定例及び変換例)MM : Dec 9, 2013 3:37:47 AMM- : Dec 9, 2013-M : 3:41:45 AMNote
DateTimeFormat
では、java.util.Date
、java.util.Calendar
、java.lang.Long
(タイムスタンプとしてのLong
) およびjava.tim.*
(JSR-310 Date And Time API) を型として指定できる。Macchinetta Server Framework (1.x)ではJSR-310 Date And Time APIの使用を推奨する。
詳しくは、日付操作(JSR-310 Date and Time API)、 システム時刻を参照されたい。
3.4.2.1.3. Controller単位の型変換¶
@InitBinder
アノテーションを使用することでController毎に型変換の定義を指定する事も出来る。
@InitBinder // (1) public void initWebDataBinder(WebDataBinder binder) { binder.registerCustomEditor( Long.class, new CustomNumberEditor(Long.class, new DecimalFormat("#,#"), true)); // (2) }@InitBinder("sampleForm") // (3) public void initSampleFormWebDataBinder(WebDataBinder binder) { // ... }
項番 説明 (1)@InitBinder
アノテーション を付与したメソッド用意すると、バインド処理が行われる前にこのメソッドが呼び出され、デフォルトの動作をカスタマイズすることができる。 (2)例では、Long型のフィールドの数値形式を #,#
に指定しているので、「,」でフォーマットされた値をバインドすることができる。 (3)@InitBinder
アノテーションのvalue属性にフォームオブジェクトの属性名を指定することで、フォームオブジェクト毎にデフォルトの動作をカスタマイズすることもできる。 例では、sampleForm
という属性名のフォームオブジェクトに対するバインド処理が行われる前にメソッドが呼び出される。
3.4.2.1.4. 入力チェック用のアノテーションの指定¶
3.4.2.2. フォームオブジェクトの初期化方法¶
@ModelAttribute
アノテーションを使うことで結びつけることができる。@ModelAttribute
アノテーションを付与したメソッドで行う。setUpXxxForm
というメソッド名で定義することを推奨する。@ModelAttribute // (1) public SampleForm setUpSampleForm() { SampleForm form = new SampleForm(); // populate form return form; }@ModelAttribute("xxx") // (2) public SampleForm setUpSampleForm() { SampleForm form = new SampleForm(); // populate form return form; }@ModelAttribute public SampleForm setUpSampleForm( @CookieValue(value = "name", required = false) String name, // (3) @CookieValue(value = "age", required = false) Integer age, @CookieValue(value = "birthDate", required = false) Date birthDate) { SampleForm form = new SampleForm(); form.setName(name); form.setAge(age); form.setBirthDate(birthDate); return form; }
項番 説明 (1)Model
に追加するための属性名は、クラス名の先頭を小文字にした値(デフォルト値)が設定される。この例ではsampleForm
が属性名になる。返却したオブジェクトは、model.addAttribute(form)
相当の処理が実行されModel
に追加される。 (2)Model
に追加するための属性名を指定したい場合は、@ModelAttribute
アノテーションのvalue属性に指定する。この例では /xxx
が属性名になる。返却したオブジェクトは、model.addAttribute("xxx", form)
相当の処理が実行されModel
に追加される。デフォルト値以外の属性名を指定した場合、ハンドラメソッドの引数としてフォームオブジェクトを受け取る時に@ModelAttribute("xxx")
の指定が必要になる。 (3) ModelAttributeメソッドは、ハンドラメソッドと同様に初期化に必要なパラメータを渡すこともできる。例では、@CookieValue
アノテーションを使用してCookieの値をフォームオブジェクトに設定している。
Note
フォームオブジェクトにデフォルト値を設定したい場合はModelAttributeメソッドで値を設定すること。
例の(3)ではCookieから値を取得しているが、定数クラスなどに定義されている固定値を直接設定してもよい。
Note
ModelAttributeメソッドはController内に複数定義することができる。各メソッドはControllerのハンドラメソッドが呼び出される前に毎回実行される。
Warning
ModelAttributeメソッドはリクエストごとにメソッドが実行されるため、特定のリクエストの時のみに必要なオブジェクトについてModelAttributeメソッドを使って生成すると、無駄なオブジェクトの生成及び初期化処理が行われる点に注意すること。
特定のリクエストのみで必要なオブジェクトについては、ハンドラメソッド内で生成しModel
に追加する方法にすること。
3.4.2.3. HTMLへのバインディング方法¶
Model
に追加されたフォームオブジェクトの各プロパティは、Thymeleaf + Springで提供されるth:field
属性で指定することで、HTMLのinput要素にバインドすることができる。<form:xxx>
タグを利用してフォームオブジェクトをHTML formにバインドする機能があるが、Thymeleafではth:field
属性にth:object
属性を併用することで同様の機能を実現することができる。<html xmlns:th="http://www.thymeleaf.org"> <!-- (1) --><form th:action="@{/sample/hello}" th:object="${sampleForm}" method="post"> <!-- (2) --> Id : <input th:field="*{id}"><span th:errors="*{id}"></span><br> <!-- (3) --> Name : <input th:field="*{name}"><span th:errors="*{name}"></span><br> Age : <input th:field="*{age}"><span th:errors="*{age}"></span><br> Gender : <input th:field="*{genderCode}"><span th:errors="*{genderCode}"></span><br> Birth Date : <input th:field="*{birthDate}"><span th:errors="*{birthDate}"></span><br> </form>
項番 説明 (1)スタンダードダイアレクトが提供する属性を使用したとき、EclipseなどのIDEでの警告を抑止するため、ネームスペースを付与する。 (2)<form>
タグのth:object
属性には、Model
に格納されているフォームオブジェクトの属性名を指定する。th:object
属性については、オブジェクトのプロパティを省略して指定するも参照されたい。 (3)<input>
タグのth:field
属性には、フォームオブジェクトのプロパティ名を指定する。
3.4.2.4. リクエストパラメータのバインディング方法¶
HTML formから送信されたリクエストパラメータは、フォームオブジェクトにバインドし、Controllerのハンドラメソッドの引数に渡すことができる。
@GetMapping("hello") public String hello( @Validated SampleForm form, // (1) BindingResult result, Model model) { if (result.hasErrors()) { return "sample/input"; } // process form... return "sample/hello"; }@ModelAttribute("xxx") public SampleForm setUpSampleForm() { SampleForm form = new SampleForm(); // populate form return form; } @GetMapping("hello") public String hello( @ModelAttribute("xxx") @Validated SampleForm form, // (2) BindingResult result, Model model) { // omitted }
項番 説明 (1)フォームオブジェクトにリクエストパラメータが反映された状態で、Controllerのハンドラメソッドの引数に渡される。 (2)ModelAttributeメソッドにて属性名を指定した場合、 @ModelAttribute("xxx")
といった感じで、フォームオブジェクトの属性名を明示的に指定する必要がある。
Warning
ModelAttributeメソッドで指定した属性名とメソッドの引数で指定した属性名が異なる場合、ModelAttributeメソッドで生成したインスタンスとは別のインスタンスが生成されるので注意が必要。
ハンドラメソッドで属性名の指定を省略した場合、クラス名の先頭を小文字にした値が属性名として扱われる。
3.4.2.4.1. バインディング結果の判定¶
HTML formから送信されたリクエストパラメータをフォームオブジェクトにバインドする際に発生したエラー(入力チェックエラーも含む)は、 org.springframework.validation.BindingResult
に格納される。
@GetMapping("hello") public String hello( @Validated SampleForm form, BindingResult result, // (1) Model model) { if (result.hasErrors()) { // (2) return "sample/input"; } // omitted }
項番 説明 (1)フォームオブジェクトの直後に BindingResult
を宣言すると、フォームオブジェクトへのバインド時のエラーと入力チェックエラーを参照することができる。 (2)BindingResult.hasErrors()
を呼び出すことで、フォームオブジェクトの入力値のエラー有無を判定することができる。
フィールドエラーの有無、グローバルエラー(相関チェックエラーなどのクラスレベルのエラー)の有無を個別に判定することもできるので、要件に応じて使い分けること。
項番 メソッド 説明
hasGlobalErrors()
グローバルエラーの有無を判定するメソッド
hasFieldErrors()
フィールドエラーの有無を判定するメソッド
hasFieldErrors(String field)
指定したフィールドのエラー有無を判定するメソッド
3.4.3. Viewの実装¶
Viewは以下の役割を担う。
- クライアントに応答するレスポンスデータ(HTML)を生成する。Viewはモデル(フォームオブジェクトやドメインオブジェクトなど)から必要なデータを取得し、クライアントが描画するために必要な形式でレスポンスデータを生成する。
3.4.3.1. ThymeleafのテンプレートHTMLの実装¶
ViewResolver
は、Thymeleaf + Springより提供されているThymeleafViewResolver
を使用する。ViewResolver
の設定方法については、ブランクプロジェクトの設定を参照されたい。以下に、基本的なテンプレートHTMLの実装方法について説明する。
- Thymeleafのネームスペースを設定する
- モデルに格納されている値を表示する
- モデルに格納されている数値を表示する
- モデルに格納されている日時を表示する
- リクエストURLを生成する
- メッセージを表示する
- 文字列を組み立てる
- 条件を判定する
- 条件によって表示を切り替える
- コレクションの要素に対して表示処理を繰り返す
- オブジェクトのプロパティを省略して指定する
- ローカル変数を定義する
- プリプロセッシング
- フォームオブジェクトのプロパティをバインドする
- 入力チェックエラーを表示する
- 処理結果のメッセージを表示する
- コードリストを表示する
- ページネーション用のリンクを表示する
- 権限によって表示を切り替える
本章では、ThymeleafおよびThymeleaf + Springのダイアレクト、ならびにThymeleafのSpring Security連携用ダイアレクトで提供されている代表的な属性やオブジェクトの使い方を説明しているが、全てについて説明はしていないので、詳細な使い方については、それぞれのドキュメントを参照すること。
項番 説明 ドキュメント
Thymeleafのダイアレクト
Thymeleaf + Springのダイアレクト
ThymeleafのSpring Security連携用ダイアレクト
3.4.3.1.1. Thymeleafのネームスペースを設定する¶
th:text
のようなThymeleaf独自の属性を使用する必要があるため、Thymeleafのネームスペースを付与する。<html>
要素にネームスペースを付与することを推奨する。<html xmlns:th="http://www.thymeleaf.org">
Note
ネームスペースはXHTMLの標準で定義された以外の要素・属性を使用する場合に付与するものであり、HTML5では本来不要なものである。(事実、ネームスペースを付与しなくとも、テンプレートの解釈に問題は生じない。)
ただし、HTML5であっても標準で定義された以外の要素・属性を使用すると、EclipseなどのIDEで警告が出力されるため、これを抑止するためにネームスペースを付与すべきである。
なお、テンプレートを解釈して出力されるHTMLからは、ネームスペース(
xmlns:th
)は削除される。Note
本ガイドラインでは解説しないが、HTML5に準拠する形でThymeleafを使用することも可能である。
具体的には、HTML5で独自の属性を使用する場合は属性名に
data-
をつけるが、Thymeleafでもこれを使用してdata-th-text
のように属性を記述することができる。詳細については、Tutorial: Using Thymeleaf -A multi-language welcome-を参照されたい。
3.4.3.1.2. モデルに格納されている値を表示する¶
th:text
属性を使用する。th:text
属性に変数式${}
を使用すれば良い。SampleController.java
@GetMapping("hello") public String hello(Model model) { model.addAttribute(new HelloBean("Bean Hello World!")); // (1) return "sample/hello"; // returns view name }
hello.html
<span th:text="${helloBean.message}"></span> <!--/* (2) */-->
HTML created by View(hello.html)
<span>Bean Hello World!</span>
項番 説明 (1)Model
オブジェクトにHelloBean
オブジェクトを追加する。(2)View(テンプレートHTML)側では、th:text
などの属性において${属性名}のような式を記述することできる。${}
は変数式で、Model
オブジェクトに追加したデータを取得することができる。例では、取得したデータをHTMLエスケープして出力するためにth:text
属性を利用し、「th:text=”${helloBean.message}”」としている。XSS対策のため必ずHTMLエスケープを行うことを推奨する。詳細については、Output Escapingを参照されたい。
3.4.3.1.3. モデルに格納されている数値を表示する¶
数値型の値をフォーマットして出力する場合、Thymeleafの#numbers
を使用する。
#numbers
は、数値のフォーマットを行う以下のようなメソッドをもつ。
項番 メソッド名 説明 使用例
formatInteger 整数値にフォーマットする。
引数として、以下のパターンをとる。
- 整数型、最小桁数
- 整数型、最小桁数、千の位の区切り文字
${#numbers.formatInteger(num, 1, ‘COMMA’)}
formatDecimal 小数値にフォーマットする。
引数として、以下のパターンをとる。
- 浮動小数点型、最小桁数、小数桁数
- 浮動小数点型、最小桁数、小数桁数、小数点の文字
- 浮動小数点型、最小桁数、千の位の区切り文字、小数桁数、小数点の文字
${#numbers.formatDecimal(num, 1, ‘COMMA’, 2, ‘POINT’)}
formatPercent パーセント表示にフォーマットする。
引数として、以下のパターンをとる。
- 浮動小数点型
- 浮動小数点型、最小桁数、小数桁数
${#numbers.formatPercent(num, 1, 2)}
Note
千の位の区切り文字、小数点の文字としては、以下のものが指定できる。
- ‘POINT’ - ピリオド “
.
”- ‘COMMA’ - カンマ “
,
”- ‘WHITESPACE’ - 半角スペース
- ‘NONE’ - 区切り文字なし
- ‘DEFAULT’ - ロケールに依存
指定しなかった場合、千の位の区切り文字には’NONE’が、小数点には’POINT’が設定される。
例えば小数を表示する際には、
#numbers.formatDecimal
メソッドを使用してフォーマットする。<span th:text="${#numbers.formatDecimal(helloBean.numberItem,1,2,'POINT')}"></span> <!--/* (1) */-->
項番 説明 (1) 取得した値を#numbers.formatDecimal
メソッドでフォーマットし、変数式${}
に指定する。例では、最小桁数に1を、小数桁数に2を、小数点にPOINT
(ピリオド)を、表示するフォーマットに設定している。仮にhelloBean.numberItem
の値が1.2
の場合、画面には1.20
が出力される。
3.4.3.1.4. モデルに格納されている日時を表示する¶
日時型の値をフォーマットして出力する場合、Thymeleafの#dates
、あるいは#calendars
を使用する。
java.util.Date
オブジェクトのフォーマットを行う場合は、#dates.format
メソッドを利用する。
<span th:text="${#dates.format(helloBean.dateItem,'yyyy-MM-dd')}"></span> <!--/* (1) */-->
¶ 項番 説明 (1) 取得した値を#dates.format
メソッドでフォーマットし、変数式${}
に指定する。例では、日付をyyyy-MM-dd
形式でフォーマットしている。仮にhelloBean.dateItem
の値が2013年3月2日の場合、画面には2013-03-02
が出力される。
java.util.Calendar
オブジェクトのフォーマットを行う場合は、#calendars.format
メソッドを利用する。
<span th:text="${#calendars.format(helloBean.calendarItem,'yyyy-MM-dd')}"></span> <!--/* (1) */-->
項番 説明 (1) 取得した値を#calendars.format
メソッドでフォーマットし、変数式${}
に指定する。例では、日付をyyyy-MM-dd
形式でフォーマットしている。仮にhelloBean.calendarItem
の値が2013年3月2日の場合、画面には2013-03-02
が出力される。
3.4.3.1.5. リクエストURLを生成する¶
HTMLの<form>
要素のaction
属性や<a>
要素のhref
属性、<img>
要素のsrc
属性などに対してURLを出力する場合は、Thymeleafのth:action
属性、th:href
属性、th:src
属性を使用する。
これらの属性では、以下のいずれかの方法によって生成されたリクエストURL(Controllerのメソッドを呼び出すためのURL)を設定する。
ThymeleafのリンクURL式
@{}
を使用してリクエストURLを組み立てるThymeleaf + Springの
#mvc.url
メソッドを使用してリクエストURLを組み立てるNote
どちらの方法を使用してもよいが、一つのアプリケーションの中で混在して使用することは、保守性を低下させる可能性があるので避けた方がよい。
package com.example.app.hello; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("hello") @Controller public class HelloController { // (1) @GetMapping public String hello() { return "hello/home"; } }
項番 説明 (1) このメソッドに割り当てられるリクエストURLは、{コンテキストパス}/hello
となる。
ThymeleafのリンクURL式を使用してリクエストURLを組み立てる
まず、ThymeleafのリンクURL式@{}
を使用してリクエストURLを組み立てる方法について説明する。
コンテキストルートからの相対パスを指定する
<form th:action="@{/hello}" method="post"> <!-- (2) --> <!-- omitted --> </form>
項番 説明 (2)リンクURL式@{}
に”/
”から始まるパスを記述すると、記述したパスをコンテキストルートに付与したURLが生成される。現在のパスからの相対パスを指定する
<a th:href="@{user/top}"></a> <!-- (3) -->
項番 説明 (3)リンクURL式@{}
にパスを記述すると、現在のパスから見た相対的なURLが生成される。Note
リンクURL式
@{}
は、コンテキストルートからの相対パスやページ現在のパスからの相対パスを生成するほか、サーバールートからの相対パス、プロトコルからの相対パスも生成できる。詳細については、Tutorial: Using Thymeleaf -Link URLs-を参照されたい。
リンクURL式には、パスの一部またはパラメータとして変数を埋め込むことが可能である。
パスの一部に変数を埋め込む
<form th:action="@{/user/{userId}/details(userId=${user.id})}" method="post"> <!-- (4) --> <!-- omitted --> </form>
項番 説明 (4)リンクURL式@{}
のパス内で変数式を使うこともできる。例では、パスの{userId}
の部分に変数${user.id}
の値が代入され、/user/3/details
といったパスが生成される。Note
パスの一部に変数を埋め込む際の注意点について
上記コード例のようにパスの一部に変数を埋め込む場合、変数
${user.id}
の値がnull
の場合は、URLが/コンテキストルート/user//details
のようにスラッシュが重複した状態で生成される。スラッシュの重複を無視された場合には、予期せぬコンテンツにアクセスされる恐れがある為、パスの一部に埋め込む変数は
null
とならない事が保証された値を用いるか、後述するデフォルト式?:
を使用して、テンプレートHTML側でnull
の代替文字列を指定することで回避すること。パラメータとして変数を埋め込む
<form th:action="@{/user/details(userId=${user.id})}" method="post"> <!-- (4) --> <!-- omitted --> </form>
項番 説明 (4)リンクURL式@{}
のパス内にパラメータを指定することができる。例では、変数${user.id}
の値が代入され、/user/details?userId=3
といったパスが生成される。
Thymeleaf + Springの#mvc.urlメソッドを使用してリクエストURLを組み立てる
つぎに、Thymeleaf + Springの#mvc.url
メソッドを使用してリクエストURLを組み立てる方法について説明する。
#mvc.url
メソッドを使用すると、Controllerのメソッドのメタ情報(メソッドシグネチャやアノテーションなど)と連携して、リクエストURLを組み立てる事ができる。
<form th:action="${(#mvc.url('HC#hello')).build()}" method="post"> <!-- (3) --> <!-- omitted --> </form>
項番 説明 (4)#mvc.url
メソッドの引数には、呼び出すControllerのメソッドに割り振られているリクエストマッピング名を指定する。#mvc.url
メソッドからは、リクエストURLを組み立てるクラス(Mvc.MethodArgumentBuilderWrapper
)のオブジェクトが返却される。Mvc.MethodArgumentBuilderWrapper
クラスは、ラップしているMvcUriComponentsBuilder.MethodArgumentBuilder
オブジェクトと同等の機能をもつ。MvcUriComponentsBuilder.MethodArgumentBuilder
クラスには、
arg
メソッドbuild
メソッドbuildAndExpand
メソッドが用意されており、それぞれ、以下の役割を持つ。
arg
メソッドは、Controllerのメソッドの引数に渡す値を指定するためのメソッドである。build
メソッドは、リクエストURLを生成するためのメソッドである。buildAndExpand
メソッドは、Controllerのメソッドの引数として宣言されていない動的な部分(パス変数など)に埋め込む値を指定した上で、リクエストURLを生成するためのメソッドである。上記例では、リクエストURLが静的なURLであるため、build
メソッドのみを呼び出してリクエストURLを生成している。リクエストURLが動的なURL(パス変数やクエリ文字列が存在するURL)の場合は、arg
メソッドやbuildAndExpand
メソッドを呼び出す必要がある。arg
メソッドとbuildAndExpand
メソッドの具体的な使用例については、Note
リクエストマッピング名について
リクエストマッピング名は、デフォルト実装(
org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMethodMappingNamingStrategy
の実装)では、「クラス名の大文字部分(クラスの短縮名) + “#
” + メソッド名」となる。リクエストマッピング名は重複しないようにする必要がある。名前が重複してしまった場合は、
@RequestMapping
合成アノテーションのname
属性に一意となる名前を指定する必要がある。
3.4.3.1.6. メッセージを表示する¶
#{}
または変数式で #messages
を使用する。- properties
# (1) label.orderStatus=注文ステータス
項番 説明 (1) プロパティファイルにラベルの値を定義する。
テンプレートHTML
- メッセージ式
#{}
を使う場合
<span th:text="#{label.orderStatus}"></span> <!--/* (2) */-->
項番 説明 (2)メッセージ式にプロパティファイルのキー名を指定するとキー名に一致するプロパティ値が表示される。- 変数式で
#messages
を使う場合
<span th:text="${#messages.msg(label.orderStatus)"></span> <!--/* (3) */-->
項番 説明 (3)変数式で#messages.msg
メソッドにプロパティファイルのキー名を指定するとキー名に一致するプロパティ値が表示される。Note
メッセージ式で指定したキーに該当するメッセージが存在しない場合は、
??label.orderStatus??
のようにメッセージキーが返却される。しかし、例えばメッセージが存在しない場合にはデフォルトメッセージを表示したい場合など、メッセージキーが返却されると判定が複雑になってしまう。このような場合は、
#messages.msgOrNull
メソッドと、後述するデフォルト式を利用することで、簡潔に記述することができる。以下にコード例を示す。
<span th:text="${#messages.msgOrNull(label.orderStatus) ?: '不明なステータス'}"></span>
デフォルト式については、条件を判定するも参照されたい。
- メッセージ式
3.4.3.1.7. 文字列を組み立てる¶
+
” 演算子やパイプ( “|
” )を使用する。+
” 演算子を使用すると良い。hello.html
<span th:text="|Message : ${helloBean.message}|"></span> <!--/* (1) */--> <span th:text="'Message : ' + ${helloBean.message}"></span> <!--/* (2) */-->
HTML created by View(hello.html)
<span>Message : Bean Hello World!</span> <!-- (3) --> <span>Message : Bean Hello World!</span> <!-- (3) -->
¶ 項番 説明 (1)パイプ( “|
” )で囲むことにより、結合された文字列を生成できる。(2)“+
” 演算子を利用すると、変数式やシングルクォート''
で囲んだテキストを結合できる。(3)HTMLの出力例。出力されるHTMLは2行とも同じになる。テキストの結合方法の混在は可読性を低下させるため、ここでは、パイプによる結合を推奨する。Note
文字列を結合する際の注意点について
文字列を結合する場合、結合対象の変数値が
null
の場合には画面に”null”が表示されてしまう。文字列を結合する際には、結合対象の変数値が
null
とならない事を確認するか、後述するデフォルト式?:
を使用して、テンプレートHTML側でnull
の代替文字列を指定することで回避すること。例えば以下の実装例において、
helloBean.message
の値がnull
の場合は、以下のようなHTMLが生成される。実装例
<span th:text="|Message : ${helloBean.message}|"></span> <span th:text="'Message : ' + ${helloBean.message}"></span> Message : <span th:text="${helloBean.message}"></span> <!-- 文字列を結合しない例 -->
生成されたHTML
<span>Message : null</span> <span>Message : null</span> Message : <span></span> <!-- 文字列を結合しない例 -->
3.4.3.1.8. 条件を判定する¶
true
またはfalse
で返却される。演算子を用いた条件の判定の結果によって、2つの式のどちらかを選択する式を条件式という。
<span th:text="${user.age} != null ? ${user.age} : 'no age specified'"></span> <!--/* (1) */-->
項番 説明 (1)${user.age} != null
という条件の判定がtrue
であった場合、${user.age}
を表示する。false
であった場合は、”no age specified”という文字列を表示する。このように、条件式は「条件 ? 式 : 式」と表されるため、3項演算子とも呼ばれる。
条件式には、等価演算子のほかに、算術演算子や比較演算子も利用できる。
<span th:text="${user.age} >= 12 ? 'adult' : 'child'"></span> <!--/* (1) */-->
項番 説明 (1)${user.age}
が12以上であった場合’adult’を、12未満であった場合’child’を表示する。Note
数値の配列やリストに対して、合計値や平均値を取得したい場合、
#aggregates
を利用できる。詳細については、Tutorial: Using Thymeleaf -Aggregates-を参照されたい。
条件の判定の結果によって処理が変わる式としては、条件式のほかに、デフォルト式と呼ばれるものもある。
<span th:text="${user.age} ?: 'no age specified'"></span> <!--/* (2) */-->
項番 説明 (2)?:
演算子は、左式 (例では${user.age}
)がnull
であったときに限って右式(例では”no age specified”)を選択し、それ以外の場合は左式を選択する。これは上の3項演算子と同じ機能をもっており、このようにデフォルト式は簡単にnullチェックを行うことができる。Note
No Operation Token( “
_
” )は、記述した属性で何もしないことを指示するものである。以下のコード例において、
user.age
がnull
だった場合、th:text
属性は処理されず、’no age specified’が表示される。<span th:text="${user.age} ?: _">no age specified</span>
3.4.3.1.9. 条件によって表示を切り替える¶
モデルが保持する値によって表示を切り替えたい場合は、Thymeleafのth:if
属性またはth:switch
属性を使用する。
Note
th:if
属性の逆の機能としてth:unless
属性があるが、th:if
とth:unless
の混在は可読性を低下させる場合があるため、いずれかへの統一を推奨する。本ガイドラインにおいては、
th:if
に統一している。
th:if
属性を使用して表示を切り替える。<div th:if="${orderForm.orderStatus} != 'complete'"> <!--/* (1) */--> <!--/* ... */--> </div>
¶ 項番 説明 (1)th:if
属性に条件を指定する。条件式がtrue
の場合はth:if
を記述した要素の表示処理が実行され、false
の場合は要素ごと削除され、表示処理は実行されない。例では、注文ステータスが'complete'
ではない場合に<div>
要素の表示処理が実行され、注文ステータスが'complete'
であった場合には<div>
要素が削除され、表示処理は実行されない。また、th:if
属性の逆の機能としてth:unless
属性があるが、th:if
属性とth:unless
属性の混在は可読性を低下させる場合があるため、いずれかへの統一を推奨する。本ガイドラインにおいては、th:if
属性に統一している。th:switch
属性を使用して表示を切り替える。<div th:switch="${customer.type}"> <!--/* (1) */--> <div th:case="premium"> <!--/* (2) */--> <!--/* ... */--> </div> <div th:case="general"> <!--/* ... */--> </div> <div th:case="*"> <!--/* (3) */--> <!--/* ... */--> </div> </div>
¶ 項番 説明 (1)th:switch
属性に変数値を指定する。(2)th:case
属性に条件を指定する。th:case
属性で指定した値がth:switch
属性で指定した変数値と等しかった場合、その要素の表示処理が実行される。th:case
属性は上から順に評価され、最初に合致した条件における表示処理が実行される。(3)いずれの条件にも合致しなかった場合に実行したい表示処理は、th:case="*"
を最後に指定して記述する。
3.4.3.1.10. コレクションの要素に対して表示処理を繰り返す¶
モデルが保持するコレクションや配列、Map等に対して表示処理を繰り返したい場合は、Thymeleafのth:each
属性を使用する。
<table> <tr> <th>Name</th> <th>Address</th> </tr> <tr th:each="customer : ${customers}"> <!--/* (1) */--> <td th:text="${customer.name}"></td> <!--/* (2) */--> <td th:text="${customer.address}"></td> <!--/* (2) */--> </tr> </table>
項番 説明 (1)th:each
属性の右項にコレクションを指定し、左項にはコレクション内の各オブジェクトを格納する変数名を指定する。<tr>
要素は、コレクション内のオブジェクトごとに繰り返し処理される。 (2)th:each
属性の左項に指定した変数に格納されているオブジェクトから変数値を取得している。Note
コレクション内のオブジェクトに対してインデックスなどを取りたい場合は、次のようにテンプレートHTMLを実装する。
<table> <tr> <th>No</th> <th>Name</th> <th>Address</th> </tr> <tr th:each="customer, status : ${customers}"> <!--/* (1) */--> <td th:text="${status.count}"></td> <!--/* (2) */--> <td th:text="${customer.name}"></td> <td th:text="${customer.address}"></td> </tr> </table>
項番 説明 (1)th:each
属性の左項に、要素の番号を格納する変数を2つ目に指定する。 (2)th:each
属性の左項で2つ目に指定した変数から、現在処理を行っている要素の位置を取得している。countは、要素の位置を1始まりで取得している。count以外の属性については、Tutorial: Using Thymeleaf -Keeping iteration status-を参照されたい。Note
th:each
属性の詳細は、Tutorial: Using Thymeleaf -Iteration-を参照されたい。
3.4.3.1.11. オブジェクトのプロパティを省略して指定する¶
Thymeleafのth:object
属性を用いると、オブジェクト名を省略してプロパティを指定することができる。
<div th:object="${helloBean}"> <!--/* (1) */--> <span th:text="*{message}"></span> <!--/* (2) */--> </div>
項番 説明 (1)th:object
属性にオブジェクトを変数式${}
で指定する。 (2) オブジェクトのプロパティを選択変数式*{}
で指定する。これは、変数式を用いてth:text="${helloBean.message}
と指定するのと同じ結果になる。
Warning
テンプレートレイアウトにより部品化する際に、後述するプリプロセッシングを利用すると、フラグメント(部品)に引数として式を与えることが可能になるが、
この場合、引数に渡す式は部品を呼び出す側に記述され、式の評価は部品側で行われることになる。
選択変数式*{}
は、記述された位置で有効なth:object
ではなく、評価時に有効なth:object
(部品でth:object
が使用され、その有効範囲内で引数に渡された式が評価される場合は、部品側に記載されたth:object
)が使用されるため、
フラグメント(部品)に引数として式を与える場合には、式をth:object
と選択変数式*{}
ではなく、変数式${}
で組み立てなければならない。
これは、部品内でth:object
と選択変数式*{}
を使用していても、部品を呼び出す側が与えた式の意味(参照するもの)が変わらないようにするための(裏を返せば、部品内でのth:object
と選択変数式*{}
の使用を制限せずに済むための)ルールである。
テンプレートHTMLの実装についてはこちらのNoteも参考にされたい。
3.4.3.1.12. ローカル変数を定義する¶
th:with
属性を使用する。<div th:with="localvar=|Hello, ${user.name}|"> <!--/* (1) */--> <span th:text="${localvar}"></span> <!--/* (2) */--> </div>
項番 説明 (1)th:with
属性に”変数名=値”の形式で設定すると、指定した値をもつローカル変数を定義できる。例では、localvar
という名の変数を定義し、Hello, (ユーザー名)
という文字列を代入している。このローカル変数は、th:with
属性を指定した<div>
要素とその要素配下でのみ有効となる。 (2) 変数式${}
にローカル変数localvar
を指定する。
3.4.3.1.13. プリプロセッシング¶
__
で囲み__${val}__
のように記述する。<form method="post" th:action="@{/sample/app/sampleModel}" th:object="${userManagementForm}" class="form-horizontal"> <!-- ... --> <div class="form-group" th:each="userForm, status : *{userFormList}"> <div class="col col-md-2" th:text="|氏名${status.count}|"></div> <div class="col col-md-3"> <!--/* (1) */--> <input type="text" th:field="*{userFormList[__${status.index}__].userName}" class="form-control input-sm" /> </div> <!--/* (2) */--> <div class="col col-md-4" th:text="${userForm.userAge}"></div> </div> <!-- ... --> </form>
項番 説明 (1)th:field
属性には選択変数式でフォームオブジェクト直下からプロパティを指定する必要があり、ここではth:field="*{userFormList[1].userName}"
のように指定する。配列インデックスの指定にth:each
のインデックスを利用したいが、th:field
属性やth:errors
属性ではSpELは配列インデックスに指定した式を評価しないため、userFormList[status.index]
のように指定するとインデックスが文字列”status.index”となってしまう。このため、式”status.index”をプリプロセッシングで先に評価する必要がある。 (2)th:text
属性ではフォームオブジェクト直下からプロパティを指定する必要はなく、th:each
属性で定義した変数userForm
を利用し、${userForm.userAge}
のように指定できる。また、th:text
属性ではSpELは配列インデックスに指定した式を評価するため、フォームオブジェクト直下からプロパティを取得する場合でもth:text="${userManagementForm.userFormList[status.index].userAge}"
またはth:text="*{userFormList[#ctx.status.index].userAge}"
と書くことができ、プリプロセッシングは不要である。(2つ目の例でインデックスに#ctx
を使用しているのは、#ctx
を使用しない場合th:obejct
で指定したもののstatus
プロパティを参照してしまうためである。)
Warning
プリプロセッシングは式を動的に構築するためのものであるため、ユーザの入力値(リクエストから入力するものであれば、画面上に入力欄が存在しないものも含む)をプリプロセッシングした場合、サーバーで任意のコードを実行されてしまう可能性がある。アプリケーションの実装次第では入力チェック後にもTOCTOUで不正な値が紛れ込む余地があり、開発者が問題を認識することが難しく、問題を見逃した場合の影響が甚大であるため、入力チェック対象の項目である場合でも、入力値をプリプロセッシングしないことを推奨する。
TOCTOU(Time of check to time of use)とは、ある値がチェックされてから使用されるまでの間に変更されることにより発生する問題である。
例えば、半角英数字のみという条件で入力チェックを行ったのち、何らかの原因によって入力値が変更され、記号などの不正な値が混入した状態で使用されてしまうなどである。
TOCTOUが発生する典型的なケースとしては以下のようなものがある。
- 入力画面で入力チェックを行った後、確認画面を表示し画面の操作(確定ボタン押下等)によって処理を実行するアプリにて、確定ボタン押下時に再チェックを行っていないことで実質任意の値が入力可能になっている。(入力チェックの実装漏れ)
- セッションスコープにフォームオブジェクトを格納している場合、入力チェックを通過したフォーム内の値が、ハンドラメソッド実行中に別スレッドから書き換えられ、不正な値で処理を実行してしまう。Springの機能(
RequestMappingHandlerAdapter
のsynchronizeOnSession
)で対策可能であり、 同一セッション内のリクエストの同期化 でも案内している。
上記の典型的なケースの対策が行われていても、Springの機能( RequestMappingHandlerAdapter
の synchronizeOnSession
)により同期化が行われるのはControllerの範囲であるため、ViewであるThymeleafのテンプレートHTMLでセッションスコープのフォームオブジェクトから値を取得している場合は、別スレッドから書き換えられた不正な値が得られる可能性がある。
当ガイドラインのプリプロセッシングに対する推奨事項としては以下の通りになる。
- プリプロセッシングを利用せずとも実装できないか検討を行う。
- プリプロセッシングを利用しなければならない場合は、サーバ上の安全なデータを用いる。
- どうしてもユーザの入力値などの安全でないデータをプリプロセッシングする必要がある場合は、入力値を利用する直前に入力チェックを行っていること、及びその値が状態により変更されないことを十分に確認した上で利用する。(ただし、上述の通り、開発者が問題を認識することが難しい上、問題を見逃した場合の影響が甚大であるため、安易に採用しないこと。)
3.4.3.1.14. フォームオブジェクトのプロパティをバインドする¶
th:object
属性とth:field
属性を使用すると、フォームオブジェクトのプロパティをバインドすることができる。<form th:action="@{/sample/hello}" th:object="${sampleForm}" method="post"> <!--/* (1) */--> Id : <input th:field="*{id}"> <!--/* (2) */--> </form>
項番 説明 (1)
<form>
タグのth:object
属性に、Model
に格納されているフォームオブジェクトを指定する。<form>
タグのth:object
では以下の点に注意されたい。
- 変数式に
${sample.sampleForm}
のようにネストしたプロパティを指定することはできない。<form>
タグ内でさらにth:object
を使用することはできない。 (2)<input>
タグのth:field
属性に、バインドするプロパティ名を指定する。th:field
では以下の点に注意されたい。
- 変数式
${}
は使用できず、必ず選択変数式*{}
を使用する必要がある。- 選択変数式に
*{receiverAddress.postcode}
のようにネストしたプロパティを指定することはできる。
3.4.3.1.15. 入力チェックエラーを表示する¶
入力チェックエラーの内容を表示する場合、Thymeleaf + Springのth:errors
属性を使用する。詳細は、入力チェックを参照されたい。
Note
入力チェックそのものは
th:errors
属性を使用することで可能となるが、th:object
属性と併用することで簡潔な記述となり、可読性が高まるため、これを推奨する。<form th:action="@{/sample/hello}" th:object="${sampleForm}" method="post"> Id : <input th:field="*{id}"><span th:errors="*{id}"></span><!--/* (1) */--> </form>
項番 説明 (1)th:errors
属性に、エラー表示したいプロパティのプロパティ名を指定する。なお、指定したプロパティに入力チェックエラーがなかった場合、出力されるHTMLにおいて、th:errors
属性を含む要素(ここでは<span>
)は削除される。
3.4.3.1.16. 処理結果のメッセージを表示する¶
ResultMessages
オブジェクトから結果メッセージを取り出して表示する必要がある。以下では、TERASOLUNAのJSPタグである<t:messagesPanel>
のデフォルト設定で出力するHTMLを生成する例として、以下のようにソースコードを記述している。
<div class="messages"> <h2>Message pattern</h2> <div th:if="${resultMessages} != null" th:class="|alert alert-${resultMessages.type}|"> <!--/* (1) */--> <ul> <li th:each="message : ${resultMessages}" th:text="${message.code} != null ? ${#messages.msgWithParams(message.code, message.args)} : ${message.text}"></li> <!--/* (2) */--> </ul> </div> </div>
項番 説明 (1)resultMessages
オブジェクトがnull
でないとき、<div>
とその配下の要素が実行される。 (2)resultMessages
オブジェクトに格納されたmessage
プロパティを、Thymeleafの#messages
を使用して繰り返し取得し、出力する。
3.4.3.1.17. コードリストを表示する¶
java.util.Map
型として取得することができ、Map
インタフェースと同じ方法で参照することができる。コードリストをセレクトボックスに表示する。
<select th:field="${orderForm.orderStatus}"> <option value="">--Select--</option> <option th:each="var : ${CL_ORDERSTATUS}" th:value="${var.key}" th:text="${var.value}" /> <!--/* (1) */--> </select>
項番 説明 (1) コードリスト名(CL_ORDERSTATUS
)を属性名として、コードリスト(java.util.Map
インタフェース)が格納されている。コードリストから、セレクトボックスに各コードのキー値を表示し、選択されたコード名をth:field
属性で指定されたオブジェクトに代入している。th:each
属性については、コレクションの要素に対して表示処理を繰り返すも参照されたい。
セレクトボックスで選択した値のコード名を表示する。
<span th:text="${orderForm.orderStatus} != null ? |Order Status : ${CL_ORDERSTATUS.get(orderForm.orderStatus)}|"></span> <!-- (1) -->
項番 説明 (1) セレクトボックス作成時と同様に、コードリスト名(CL_ORDERSTATUS
) を属性名として格納されたコードリスト(java.util.Map
インタフェース)を取得する。取得したコードリストのキー値として、セレクトボックスで選択した値を指定することで、コード名を表示することができる。
Warning
Map.get(key)
はMap[key]
と書くこともできる。ただし、Map[key]
でキーに式を指定する場合、式に「.」を含む場合は式として評価されるが、含まない場合はキーに指定した式を式ではなく文字列として評価してしまい、「意図したキーを指定できないバグ」の影響を受ける。
バグの影響を回避するために、キーに指定する式に「.」が含まれていない場合は、以下のいずれかの実装を行う必要がある。
Map.get(key)
:EL式のブラケットを利用せず、getメソッドを利用する。Map[#ctx.key]
:コンテキストオブジェクトから変数を指定することで、強制的に「.」を付与する。Map['__${key}__']
:プリプロセッシングで先にキー値を解決する。(非推奨)
「.」を含むか含まないかで実装を変えるとバグを引き起こしやすいため、いずれかの方法で統一することを推奨する。本ガイドラインではMap.get(key)
で実装を統一している。
プリプロセッシングはセキュリティ上の問題を引き起こす危険性が高いため推奨しない。詳細はプリプロセッシングのWarningを参照されたい。
3.4.3.1.18. ページネーション用のリンクを表示する¶
Page
インタフェースを取得し、ページネーション用のリンクを生成する。3.4.3.1.19. 権限によって表示を切り替える¶
sec:authorize
属性や #authorization
を使用する。3.4.3.3. スタイルシートの実装¶
3.4.4. 共通処理の実装¶
3.4.4.1. Controllerの呼び出し前後で行う共通処理の実装¶
本項でいう共通処理とは、Controllerを呼び出し前後に行う必要がある共通的な処理のことを指す。
3.4.4.1.1. Servlet Filterの実装¶
MDC
に値を格納している。java
public class ClientInfoPutFilter extends OncePerRequestFilter { // (1) private static final String ATTRIBUTE_NAME = "X-Forwarded-For"; protected final void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String remoteIp = request.getHeader(ATTRIBUTE_NAME); if (remoteIp == null) { remoteIp = request.getRemoteAddr(); } MDC.put(ATTRIBUTE_NAME, remoteIp); try { filterChain.doFilter(request, response); } finally { MDC.remove(ATTRIBUTE_NAME); } } }
web.xml
<filter> <!-- (2) --> <filter-name>clientInfoPutFilter</filter-name> <filter-class>x.y.z.ClientInfoPutFilter</filter-class> </filter> <filter-mapping> <!-- (3) --> <filter-name>clientInfoPutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
項番 説明 (1)サンプルではSpring Frameworkから提供されている org.springframework.web.filter.OncePerRequestFilter
の子クラスとしてServlet Filterを作成することで、同一リクエスト内で1回だけ実行されることを保証している。(2)作成したServlet Filterを web.xml
に登録する。(3)登録したServlet Filterを適用するURLのパターンを指定する。
Servlet FilterをSpring FrameworkのBeanとして定義することもできる。
web.xml
<filter> <filter-name>clientInfoPutFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <!-- (1) --> </filter> <filter-mapping> <filter-name>clientInfoPutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
applicationContext.xml
<bean id="clientInfoPutFilter" class="x.y.z.ClientInfoPutFilter" /> <!-- (2) -->
項番 説明 (1)サンプルではSpring Frameworkから提供されているorg.springframework.web.filter.DelegatingFilterProxy
をServlet Filterのクラスに指定することで、(2)で定義したServlet Filterに処理が委譲される。(2)作成したServlet FilterのクラスをBean定義ファイル(applicationContext.xml
)に追加する。その際に、id属性にはweb.xml
で指定したフィルター名(<filter-name>
タグで指定した値 )にすること。
3.4.4.1.2. HandlerInterceptorの実装¶
HandlerInterceptorでは以下の3つのポイントで処理を実行することが出来る。
- Controllerのハンドラメソッドを実行する前
HandlerInterceptor#preHandle
メソッドとして実装する。 - Controllerのハンドラメソッドが正常終了した後
HandlerInterceptor#postHandle
メソッドとして実装する。 - Controllerのハンドラメソッドの処理が完了した後(正常/異常に関係なく実行される)
HandlerInterceptor#afterCompletion
メソッドとして実装する。
public class SuccessLoggingInterceptor implements HandlerInterceptor { // (1) private static final Logger logger = LoggerFactory .getLogger(SuccessLoggingInterceptor.class); @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method m = handlerMethod.getMethod(); logger.info("[SUCCESS CONTROLLER] {}.{}", new Object[] { m.getDeclaringClass().getSimpleName(), m.getName()}); } }
spring-mvc.xml
<mvc:interceptors> <!-- omitted --> <mvc:interceptor> <mvc:mapping path="/**" /> <!-- (2) --> <mvc:exclude-mapping path="/resources/**" /> <!-- (3) --> <bean class="x.y.z.SuccessLoggingInterceptor" /> <!-- (4) --> </mvc:interceptor> <!-- omitted --> </mvc:interceptors>
項番 説明 (1)サンプルではSpring Frameworkから提供されている org.springframework.web.servlet.HandlerInterceptor
の実装クラスとしてHandlerInterceptorを作成している。(2)作成したHandlerInterceptorを適用するパスのパターンを指定する。 (3)作成したHandlerInterceptorを適用しないパスのパターンを指定する。 (4)作成したHandlerInterceptorを spring-mvc.xml
の<mvc:interceptors>
タグ内に追加する。
Note
非同期リクエストを処理するorg.springframework.web.servlet.AsyncHandlerInterceptor
も提供されている。
Note
HandlerInterceptorのパス指定においてはワイルドカード(*
や**
)を使用することができる。このうち**
はSpring Framework 5.3.0よりパスの最後にしか使用できなくなった。最後以外に使用した場合は起動時やアクセス時にエラーとなる。
3.4.4.2. Controllerの共通処理の実装¶
ここでいう共通処理とは、すべてのControllerで共通的に実装する必要がある処理のことを指す。
3.4.4.2.1. HandlerMethodArgumentResolverの実装¶
Spring FrameworkのデフォルトでサポートされていないオブジェクトをControllerの引数として渡したい場合は、HandlerMethodArgumentResolverを実装してControllerの引数として受け取れるようにする。
JavaBean
public class CommonParameters implements Serializable { // (1) private String param1; private String param2; private String param3; // omitted }
HandlerMethodArgumentResolver
public class CommonParametersMethodArgumentResolver implements HandlerMethodArgumentResolver { // (2) @Override public boolean supportsParameter(MethodParameter parameter) { return CommonParameters.class.equals(parameter.getParameterType()); // (3) } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { CommonParameters params = new CommonParameters(); // (4) params.setParam1(webRequest.getParameter("param1")); params.setParam2(webRequest.getParameter("param2")); params.setParam3(webRequest.getParameter("param3")); return params; }
Controller
@GetMapping(value = "home") public String home(CommonParameters commonParams) { // (5) logger.debug("param1 : {}",commonParams.getParam1()); logger.debug("param2 : {}",commonParams.getParam2()); logger.debug("param3 : {}",commonParams.getParam3()); // omitted return "sample/home"; }
spring-mvc.xml
<mvc:annotation-driven> <mvc:argument-resolvers> <!-- omitted --> <bean class="x.y.z.CommonParametersMethodArgumentResolver" /> <!-- (6) --> <!-- omitted --> </mvc:argument-resolvers> </mvc:annotation-driven>
項番 説明 (1)共通パラメータを保持するJavaBean。(2)org.springframework.web.method.support.HandlerMethodArgumentResolver
インタフェースを実装する。(3)処理対象とする型を判定する。例では、共通パラメータを保持するJavaBeanの型がControllerの引数として指定されていた場合に、このクラスのresolveArgumentメソッドが呼び出される。(4)リクエストパラメータから値を取得し、共通パラメータを保持するJavaBeanに設定し返却する。(5)Controllerのハンドラメソッドの引数に共通パラメータを保持するJavaBeanを指定する。(4)で返却されるオブジェクトが渡される。(6)作成したHandlerMethodArgumentResolverを spring-mvc.xml
の<mvc:argument-resolvers>
タグ内に追加する。
Note
全てのControllerのハンドラメソッドで共通的に渡すパラメータがある場合は、HandlerMethodArgumentResolverを使ってJavaBeanに変換してから渡す方法が有効的である。ここでいうパラメータとは、リクエストパラメータに限らない。
3.4.4.2.2. @ControllerAdvice
の実装¶
@ControllerAdvice
アノテーションを付与したクラスでは、複数のControllerで実行したい共通的な処理を実装する。
@ControllerAdvice
アノテーションを付与したクラスを作成すると、
@InitBinder
を付与したメソッド@ExceptionHandler
を付与したメソッド@ModelAttribute
を付与したメソッド
で実装した処理を、複数のControllerに適用する事ができる。
Tip
@ControllerAdvice
アノテーションは、Spring Framework 3.2 から追加された仕組みだが、全てのControllerに処理が適用される仕組みになっていたため、アプリケーション全体の共通処理しか実装できなかった。
Spring Framework 4.0 からは、共通処理を適用するControllerを柔軟に指定する事ができるように改善されている。
この改善により、様々な粒度で共通処理を実装する事ができるようになった。
以下に、共通処理を適用するControllerを指定する方法(属性の指定方法)について説明する。
項番 | 属性 | 説明と指定例 |
---|---|---|
(1)
|
annotations |
アノテーションを指定する。 指定したアノテーションが付与されたControllerに対して共通処理が適用される。
以下に指定例を示す。
@ControllerAdvice(annotations = LoginFormModelAttributeSetter.LoginFormModelAttribute.class)
public class LoginFormModelAttributeSetter {
@Target(TYPE)
@Retention(RUNTIME)
public static @interface LoginFormModelAttribute {}
// omitted
}
@LoginFormModelAttribute
@Controller
public class WelcomeController {
// omitted
}
@LoginFormModelAttribute
@Controller
public class LoginController {
// omitted
}
上記例では、 |
(2)
|
assignableTypes |
クラス又はインタフェースを指定する。
指定したクラス又はインタフェースに割り当て可能(キャスト可能)なControllerに対して共通処理が適用される。
本属性を使用する場合は、共通処理を適用するControllerであることを示すためのマーカーインタフェースを属性値に指定するスタイルを採用することを推奨する。
このスタイルを採用した場合、Controller側では、適用したい共通処理用のマーカーインタフェースを実装するだけでよい。
以下の指定例を示す。
@ControllerAdvice(assignableTypes = ISODateInitBinder.ISODateApplicable.class)
public class ISODateInitBinder {
public static interface ISODateApplicable {}
// omitted
}
@Controller
public class SampleController implements ISODateApplicable {
// omitted
}
上記例では、 |
(3)
|
basePackageClasses |
クラス又はインタフェースを指定する。 指定したクラス又はインタフェースのパッケージ配下のControllerに対して共通処理が適用される。 本属性を使用する場合は、
を属性値に指定するスタイルを採用することを推奨する。
以下に指定例を示す。
package com.example.app
@ControllerAdvice(basePackageClasses = AppGlobalExceptionHandler.class)
public class AppGlobalExceptionHandler {
// omitted
}
package com.example.app.sample
@Controller
public class SampleController {
// omitted
}
上記例では、 package com.example.app.common
@ControllerAdvice(basePackageClasses = AppPackage.class)
public class AppGlobalExceptionHandler {
// omitted
}
package com.example.app
public interface AppPackage {
}
|
(4)
|
basePackages |
パッケージ名を指定する。 指定したパッケージ配下のControllerに対して共通処理が適用される。
以下に指定例を示す。
@ControllerAdvice(basePackages = "com.example.app")
public class AppGlobalExceptionHandler {
// omitted
}
|
(5)
|
value |
@ControllerAdvice("com.example.app")
public class AppGlobalExceptionHandler {
// omitted
}
|
Tip
basePackageClasses
属性 / basePackages
属性 / value
属性は、共通処理を適用したいControllerが格納されているベースパッケージを指定するための属性であるが、basePackageClasses
属性を使用した場合、
- 存在しないパッケージを指定してしまう事を防ぐことが出来る
- IDE上で行ったパッケージ名変更と連動することが出来る
ため、タイプセーフな指定方法と言える。
@InitBinder
メソッドの実装サンプルを示す。yyyy/MM/dd
に設定している。@ControllerAdvice // (1) @Order(0) // (2) public class SampleControllerAdvice { // (3) @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); } }
項番 説明 (1)@ControllerAdvice
アノテーションを付与することで、ControllerAdviceのBeanであることを示している。 (2)@Order
アノテーションを付与することで、共通処理が適用される優先度を指定する。複数のControllerAdviceに依存関係があるなど、ControllerAdviceに順序性を持たせたい場合は必ず指定すること。順序性を持たせる必要がなければ指定しなくてもよい。 (3)@InitBinder
メソッドを実装する。全てのControllerに対して@InitBinder
メソッドが適用される。
@ExceptionHandler
メソッドの実装サンプルを示す。org.springframework.dao.PessimisticLockingFailureException
をハンドリングしてロックエラー画面のViewを返却している。// (1) @ExceptionHandler(PessimisticLockingFailureException.class) public String handlePessimisticLockingFailureException( PessimisticLockingFailureException e) { return "error/lockError"; }
項番 説明 (1)@ExceptionHandler
メソッドを実装する。全てのControllerに対して@ExceptionHandler
メソッドが適用される。
@ModelAttribute
メソッドの実装サンプルを示す。Model
に格納している。ControllerAdvice
// (1) @ModelAttribute public CommonParameters setUpCommonParameters( @RequestParam(value = "param1", defaultValue="def1") String param1, @RequestParam(value = "param2", defaultValue="def2") String param2, @RequestParam(value = "param3", defaultValue="def3") String param3) { CommonParameters params = new CommonParameters(); params.setParam1(param1); params.setParam2(param2); params.setParam3(param3); return params; }
Controller
@GetMapping("home") public String home(@ModelAttribute CommonParameters commonParams) { // (2) logger.debug("param1 : {}",commonParams.getParam1()); logger.debug("param2 : {}",commonParams.getParam2()); logger.debug("param3 : {}",commonParams.getParam3()); // omitted return "sample/home"; }
項番 説明 (1)@ModelAttribute
メソッドを実装する。全てのControllerに対して@ModelAttribute
メソッドが適用される。(2)@ModelAttribute
メソッドで生成されたオブジェクトが渡る。
3.4.5. 二重送信防止について¶
送信ボタンの複数回押下や完了画面の再読み込み(F5ボタンによる再読み込み)などで、 同じ処理が複数回実行されてしまう可能性があるため、二重送信を防止するための対策は必ず行うこと。
対策を行わない場合に発生する問題点や対策方法の詳細は、二重送信防止を参照されたい。
3.4.6. セッションの使用について¶
@SessionAttributes
アノテーションをControllerクラスに付与する必要がある。@SessionAttributes
アノテーションの利用を検討すること。@SessionAttributes
アノテーションの利用有無を判断すること。セッションの利用指針及びセッション使用時の実装方法の詳細は、セッション管理を参照されたい。