3.4. アプリケーション層の実装

目次

本節では、HTML formを使った画面遷移型のアプリケーションにおけるアプリケーション層の実装について説明する。

Note

Ajaxの開発やREST APIの開発で必要となる実装についての説明は以下のページを参照されたい。


アプリケーション層の実装は、以下の3つにわかれる。

  1. Controllerは、リクエストの受付、業務処理の呼び出し、モデルの更新、Viewの決定といった処理を行い、リクエストを受けてからの一連の処理フローを制御する。
    アプリケーション層の実装において、もっとも重要な実装となる。
  2. フォームオブジェクトは、HTML formとアプリケーションの間での値の受け渡しを行う。
  3. View(Thymeleaf)は、モデル(フォームオブジェクトやドメインオブジェクトなど)からデータを取得し、画面(HTML)を生成する。

3.4.1. Controllerの実装

まず、Controllerの実装から説明する。
Controllerは、以下5つの役割を担う。
  1. リクエストを受け取るためのメソッドを提供する。
    @RequestMappingアノテーションもしくは@RequestMapping合成アノテーションが付与されたメソッドを実装することで、リクエストを受け取ることができる。
  2. リクエストパラメータの入力チェックを行う。
    入力チェックが必要なリクエストを受け取るメソッドでは、@Validatedアノテーションをフォームオブジェクトの引数に指定することで、リクエストパラメータの入力チェックを行うことができる。
    単項目チェックはBean Validation、相関チェックはSpring Validator又はBean Validationでチェックを行う。
  3. 業務処理の呼び出しを行う。
    Controllerでは業務処理の実装は行わず、Serviceのメソッドに処理を委譲する。
  4. 業務処理の処理結果をModelに反映する。
    Serviceのメソッドから返却されたドメインオブジェクトをModelに反映することで、Viewから処理結果を参照できるようにする。
  5. 処理結果に対応するView名を返却する。
    Controllerでは処理結果に対する描画処理を実装せず、描画処理はThymeleaf等のViewで実装する。
    Controllerでは描画処理が実装されているViewのView名の返却のみ行う。
    View名に対応するViewの解決は、Spring Frameworkより提供されているViewResolverによって行われ、処理結果に対応するView(Thymeleaf等)が呼び出される仕組みになっている。
responsibility of logic

Picture - Logic of controller

Note

Controllerでは、業務処理の呼び出し、処理結果のModelへの反映、遷移先(View名)の決定などのルーティング処理の実装に徹することを推奨する。


Controllerの実装について、以下4つの点に着目して説明する。


3.4.1.1. Controllerクラスの作成方法

Controllerは、POJOクラスに @Controller アノテーションを付加したクラス (Annotation-based Controller)として作成する。
Spring MVCの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
}
Spring Framework 4.3から@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つ)を目安にすることを推奨する。


以下、マッピングの具体例を5つ示す。
以降の説明では、以下のControllerクラスにハンドラメソッドを定義する前提となっている。
@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() {
リクエストパスを複数指定した場合は、OR条件で扱われる。
下記の定義の場合、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メソッドが実行される。
POSTでリクエストする場合は、リクエストパラメータはURLになくてもリクエストBODYに存在していればよい。
@GetMapping(value = "hello", params = "form")
public String hello() {
リクエストパラメータを複数指定した場合は、AND条件で扱われる。
下記の定義の場合、 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.2.4. リクエストヘッダでマッピング

主にREST APIやAjax向けのリクエストをマッピングする際に使用するため、詳細は以下のページを参照されたい。


3.4.1.2.5. Content-Typeヘッダでマッピング

主にREST APIやAjax向けのリクエストをマッピングする際に使用するため、詳細は以下のページを参照されたい。


3.4.1.2.6. Acceptヘッダでマッピング

主にREST APIやAjax向けのリクエストをマッピングする際に使用するため、詳細は以下のページを参照されたい。


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を削除する。
  • 機能全体の画面フローは以下の通り。
    画面フロー図には記載していないが、入力チェックエラーが発生した場合はフォーム画面を再描画するものとする。
Screen flow of entity management function

Picture - Screen flow of entity management function


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をマッピングすると以下のようになる。

Screen flow of entity management function and assigned URL

Picture - Screen flow of entity management function and assigned 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
Entity新規作成、Entity更新、Entity削除処理では、処理内に複数のリクエストが存在しているため、HTTPメソッドとHTTPパラメータによってハンドラメソッドを切り替えている。
以下に、Entity新規作成処理を例に、処理内に複数のリクエストが存在する場合のリクエストフローを示す。
URLは全て/abc/createで、HTTPメソッドとHTTPパラメータの組み合わせでハンドラメソッドを切り替えている点に注目すること。
Request flow of entity create processing

Picture - Request flow of entity create processing


以下に、Entity新規作成処理のハンドラメソッドの実装コードを示す。
ここではリクエストとハンドラメソッドのマッピングについて理解してもらうのが目的なので、@RequestMapping@GetMapping@PostMappingの書き方に注目すること。
ハンドラメソッドの引数や返り値(View名及びView)の詳細については、次章以降で説明する。

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の実装。
が必要になる。
フォームオブジェクトおよび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メソッドが呼び出されフォーム画面が表示される。
../_images/applicationCreateFormDisplay.png

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の詳細は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を参照されたい。


以下に、入力内容確認の動作について説明する。

入力内容確認表示処理を呼び出す。
フォーム画面でInput1にaaを、Input2に”5”を入力し、Confirmボタンを押下する。
Confirmボタンを押下すると、abc/create?confirmというURIにPOSTメソッドでアクセスする。
confirmというHTTPパラメータがあるため、ControllerのcreateConfirmメソッドが呼び出され、入力内容確認画面が表示される。
../_images/applicationCreateConfirmDisplay.png

Confirmボタンを押下するとPOSTメソッドでHTTPパラメータが送信されるため、URIには現れていないが、HTTPパラメータとしてconfirmが含まれている。

../_images/applicationCreateConfirmNetwork.png

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名を返却する。

以下に、フォーム再表示の動作について説明する。

フォーム再表示リクエストを呼び出す。
入力内容確認画面で、Backボタンを押下する。
Backボタンを押下すると、abc/create?redoというURIにPOSTメソッドでアクセスする。
redoというHTTPパラメータがあるため、ControllerのcreateRedoメソッドが呼び出され、フォーム画面が再表示される。
../_images/applicationCreateConfirmDisplay.png

Backボタンを押下するとPOSTメソッドでHTTPパラメータが送信されるため、URIには現れていないが、HTTPパラメータとしてredoが含まれている。また、フォームの入力値をhidden項目として送信されるため、フォーム画面で入力値を復元することが出来る。

../_images/applicationBackToCreateFormDisplay.png
../_images/applicationBackToCreateFormNetwork.png

Note

戻るボタンの実現方法には、ボタンの属性に onclick="javascript:history.back()" を設定する方法もある。両者では以下が異なり、要件に応じて選択する必要がある。

  • ブラウザの戻るボタンを押した場合の挙動
  • 戻るボタンがあるページに直接アクセスして戻るボタンを押した場合の挙動
  • ブラウザの履歴

3.4.1.3.7. 新規作成の実装

フォームの入力内容を登録する場合は、POSTで登録対象のデータ(hiddenパラメータ)を送信させる。
新規作成リクエストはこの処理のメインリクエストになるので、HTTPパラメータによる振り分けは行っていない。
この処理ではデータベースの状態を変更するので、二重送信によって新規作成処理が複数回実行されないように制御する必要がある。
そのため、この処理が終了した後はView(画面)を直接表示するのではなく、次の画面(新規作成完了画面)へリダイレクトしている。このパターンをPOST-Redirect-GET(PRG)パターンと呼ぶ。 PRG パターンの詳細については 二重送信防止 を参照されたい。
@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の詳細については二重送信防止を参照されたい。


以下に、「新規作成」の動作について説明する。

新規作成処理を呼び出す。
入力内容確認画面で、Createボタンを押下する。
Createボタンを押下すると、abc/createというURIにPOSTメソッドでアクセスする。
ボタンを識別するためのHTTPパラメータを送信していないので、Entity新規作成処理のメインのリクエストと判断され、Controllerのcreateメソッドが呼び出される。
新規作成リクエストでは、直接画面を返さず、新規作成完了表示(/abc/create?complete)へリダイレクトしているため、HTTPステータスが302になっている。
../_images/applicationCreateNetwork.png

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名を返却する。

以下に、「新規作成完了表示」の動作について説明する。

新規作成完了後、リダイレクト先に指定されたURI(/abc/create?complete)にアクセスする。
completeというHTTPパラメータがあるため、ControllerのcreateCompleteメソッドが呼び出され、新規作成完了画面が表示される。
../_images/applicationCreateCompleteDisplay.png
../_images/applicationCreateCompleteNetwork.png

Note

PRGパターンを利用しているため、ブラウザをリロードしても、新規作成処理は実行されず、新規作成完了が再度表示されるだけである。


3.4.1.3.9. HTML form上に複数のボタンを配置する場合の実装

1つのフォームに対して複数のボタンを設置したい場合、ボタンを識別するためのHTTPパラメータを送ることで、実行するハンドラメソッドを切り替える。
ここではサンプルアプリケーションの入力内容確認画面のCreateボタンとBackボタンを例に説明する。

下図のように、入力内容確認画面のフォームには、新規作成を行うCreateボタンと新規作成フォーム画面を再表示するBackボタンが存在する。

Multiple button in the HTML form

Picture - Multiple button in the HTML form

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の全ソースを示す。
Entity一覧取得、Entity参照、Entity更新、Entity削除も同じ要領で実装することになるが、説明は割愛する。
@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例示す。


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

    ModeladdAttributeすることで、HttpServletRequestsetAttributeされるため、Spring MVCの管理下にないモジュール(例えばServletFilterなど)からも値を参照することが出来る。


3.4.1.4.2. URLのパスから値を取得する

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属性に、抜き出したい箇所をパス変数として指定する。パス変数は、「{変数名}」の形式で指定する。
上記例では、idversionという二つのパス変数を指定している。
(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. リクエストパラメータをまとめて取得する

リクエストパラメータをオブジェクトにまとめて取得する場合は、フォームオブジェクトを使用する。
フォームオブジェクトは、HTML formを表現するJavaBeanである。フォームオブジェクトの詳細は フォームオブジェクトの実装 を参照されたい。

以下は、@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
}
フォームオブジェクトクラスを作成する。
このフォームオブジェクトに対応するHTML formのテンプレートHTMLは HTMLへのバインディング方法 を参照されたい。
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引数に格納される。
フォームオブジェクトにString型以外を指定した場合に発生する型変換エラーも 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

ModeladdAttributeメソッドに非常によく似ているが、データの生存期間が異なる。

RedirectAttributesaddFlashAttributeではflash scopeというスコープにデータが格納され、リダイレクト後の1リクエスト(PRGパターンのG)でのみ追加したデータを参照することができる。2回目以降のリクエストの時にはデータは消えている。

Survival time of flush scope

Picture - Survival time of flush scope


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.10. Cookieに値を書き込む

Cookieに値を書き込む場合は、HttpServletResponseオブジェクトのaddCookieメソッドを直接呼び出してCookieに追加する。
Spring MVCからCookieに値を書き込む仕組みが提供されていないため(3.2.3時点)、この場合に限り HttpServletResponse を引数に取っても良い。
@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を応答する

ハンドラメソッドの実行結果をHTMLとして応答する場合、ハンドラメソッドの返り値は、ThymeleafのView名を返却する。
Thymeleafを使って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名を返却する。
View名からViewを解決する方法としては、個別のViewResolverを作成する方法もあるが、ここではSpring Frameworkから提供されている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. 処理の実装

Controllerでは、業務処理の実装は行わないという点がポイントとなる。
業務処理の実装はServiceで行い、Controllerでは業務処理が実装されているServiceのメソッドを呼び出す。
業務処理の実装の詳細についてはドメイン層の実装を参照されたい。

Note

Controllerは、基本的には画面遷移の決定などの処理のルーティングとModelの設定のみ実装することに徹し、可能な限りシンプルな状態に保つこと。

この方針で統一することにより、Controllerで実装すべき処理が明確になり、開発規模が大きくなった場合でもControllerのメンテナンス性を保つことができる。


Controllerで実装すべき処理を以下に4つ示す。


3.4.1.6.1. 入力値の相関チェック

入力値に対する相関チェックは、org.springframework.validation.Validatorインタフェースを実装したValidationクラス、もしくは、Bean Validationで検証を行う。
相関チェックの実装の詳細については、入力チェックを参照されたい。
相関チェックの実装自体はControllerのハンドラメソッドで行うことはないが、相関チェックを行うValidatororg.springframework.web.bind.WebDataBinderに追加する必要がある。
@Inject
PasswordEqualsValidator passwordEqualsValidator; // (1)

@InitBinder
protected void initBinder(WebDataBinder binder){
    binder.addValidators(passwordEqualsValidator); // (2)
}
項番 説明
(1)
相関チェックを行うValidatorをInjectする。
(2)
InjectしたValidatorWebDataBinderに追加する。
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. ドメインオブジェクトへの値反映

本ガイドラインでは、HTML formから送信されたデータは直接ドメインオブジェクトにバインドするのではなく、フォームオブジェクトにバインドする方法を推奨している。
そのため、ControllerではServiceのメソッドに渡すドメインオブジェクトにフォームオブジェクトの値を反映する処理を行う必要がある。
@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に追加する。
ドメインオブジェクトへ値を反映する処理は、Controllerのハンドラメソッド内で実装してもよいが、コード量が多くなる場合はハンドラメソッドの可読性を考慮してHelperクラスのメソッドに処理を委譲することを推奨する。
以下にHelperメソッドに処理を委譲した場合の例を示す。
  • 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クラスのメソッド呼び出し、業務処理を実行している。

    Note

    Helperクラスに処理を委譲する以外の方法として、Bean変換機能を使用する方法がある。

    Bean変換機能の詳細は、Beanマッピング(MapStruct)を参照されたい。


3.4.1.6.4. フォームオブジェクトへの値反映

本ガイドラインでは、HTML formの項目にバインドするデータはドメインオブジェクトではなく、フォームオブジェクトを使用する方法を推奨している。
そのため、ControllerではServiceのメソッドから返却されたドメインオブジェクトの値をフォームオブジェクトに反映する処理を行う必要がある。
@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)
    ドメインオブジェクトの値をフォームオブジェクトに反映するためのメソッドにて、ドメインオブジェクトの値をフォームオブジェクトに反映する。

    Note

    Helperクラスに処理を委譲する以外の方法として、Bean変換機能を使用する方法がある。

    Bean変換機能の詳細は、Beanマッピング(MapStruct)を参照されたい。


3.4.2. フォームオブジェクトの実装

フォームオブジェクトはHTML上のformを表現するオブジェクト(JavaBean)であり、以下の役割を担う。

  1. データベース等で保持している業務データを保持し、HTML formから参照できるようにする。
  2. HTML formから送信されたリクエストパラメータを保持し、ハンドラメソッドで参照できるようにする。
../_images/applicationFormobject.png

フォームオブジェクトの実装について、以下4点に着目して説明する。


3.4.2.1. フォームオブジェクトの作成方法

フォームオブジェクトはJavaBeanとして作成する。
Spring Frameworkでは、HTML formから送信されたリクエストパラメータ(文字列)を、フォームオブジェクトに定義されている型に変換してからバインドする機能を提供しているため、フォームオブジェクトに定義するフィールドの型は、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 AM
M- : Dec 9, 2013
-M : 3:41:45 AM

Note

DateTimeFormatでは、java.util.Datejava.util.Calendarjava.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. 入力チェック用のアノテーションの指定

フォームオブジェクトのバリデーションは、Bean Validationを使用して行うため、フィールドの制約条件を示すアノテーションを指定する必要がある。
入力チェックの詳細は、入力チェックを参照されたい。

3.4.2.2. フォームオブジェクトの初期化方法

HTMLにバインドするフォームオブジェクトの事をform-backing beanと呼び、@ModelAttributeアノテーションを使うことで結びつけることができる。
form-backing beanの初期化は、@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要素にバインドすることができる。
JSPには<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は以下の役割を担う。

  1. クライアントに応答するレスポンスデータ(HTML)を生成する。
    Viewはモデル(フォームオブジェクトやドメインオブジェクトなど)から必要なデータを取得し、クライアントが描画するために必要な形式でレスポンスデータを生成する。

3.4.3.1. ThymeleafのテンプレートHTMLの実装

クライアントにHTMLを応答する場合はThymeleafを使用する。そのために、ViewはHTML形式で実装する。
Thymeleafによって生成されたHTMLを呼び出すためのViewResolverは、Thymeleaf + Springより提供されているThymeleafViewResolverを使用する。
ViewResolverの設定方法については、ブランクプロジェクトの設定を参照されたい。

以下に、基本的なテンプレートHTMLの実装方法について説明する。

本章では、ThymeleafおよびThymeleaf + Springのダイアレクト、ならびにThymeleafのSpring Security連携用ダイアレクトで提供されている代表的な属性やオブジェクトの使い方を説明しているが、全てについて説明はしていないので、詳細な使い方については、それぞれのドキュメントを参照すること。

項番 説明 ドキュメント
Thymeleafのダイアレクト
Thymeleaf + Springのダイアレクト
ThymeleafのSpring Security連携用ダイアレクト

3.4.3.1.1. Thymeleafのネームスペースを設定する

Thymeleafを使用してテンプレートHTMLを作成する場合は、th:textのようなThymeleaf独自の属性を使用する必要があるため、Thymeleafのネームスペースを付与する。
通常は、テンプレートHTMLのどこでも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. モデルに格納されている値を表示する

Thymeleafで動的な値をHTMLに表示するには、th:text属性を使用する。
モデル(フォームオブジェクトやドメインオブジェクトなど)に格納されている値をHTMLに表示する場合、th:text属性に変数式${}を使用すれば良い。
なお、式にはThymeleaf Standard Expressionと呼ばれるEL式を利用して、オブジェクトやプロパティを指定することができる。
  • 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が出力される。

Note

#numbersは、配列やリストなどを対象にフォーマットを行うことも可能である。

詳細については、Tutorial: Using Thymeleaf -Numbers-を参照されたい。


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) */-->
:header-rows: 1 :widths: 10 90
項番 説明
(1)
取得した値を#dates.formatメソッドでフォーマットし、変数式${}に指定する。
例では、日付をyyyy-MM-dd形式でフォーマットしている。
仮にhelloBean.dateItemの値が2013年3月2日の場合、画面には2013-03-02が出力される。

Note

#datesは、配列やリストを対象にフォーマットを行うことも可能である。

詳細については、Tutorial: Using Thymeleaf -Dates-を参照されたい。


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が出力される。

Note

#calendarsは、配列やリストを対象にフォーマットを行うことも可能である。

詳細については、Tutorial: Using Thymeleaf -Calendars-を参照されたい。


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

    どちらの方法を使用してもよいが、一つのアプリケーションの中で混在して使用することは、保守性を低下させる可能性があるので避けた方がよい。


以降の説明で使用するControllerのメソッドの実装サンプルを示す。
以降の説明では、以下に示すメソッドを呼び出すためのリクエストURLを生成するための実装方法について説明する。
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. メッセージを表示する

プロパティファイルからメッセージを取得し表示する場合、Thymeleafのメッセージ式 #{} または変数式で #messages を使用する。
単純なメッセージの表示にはメッセージ式を使用することを推奨する。
なお、画面名、項目名、ガイダンス用のメッセージなどについては、国際化の必要がない場合はHTMLに直接記載してもよい。
ただし、国際化の必要がある場合はプロパティファイルからメッセージを取得し表示することを推奨する。
詳細は、 国際化 を参照されたい。
  • 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) -->
    
    :header-rows: 1 :widths: 10 90
    項番 説明
    (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. 条件を判定する

ThymeleafのテンプレートHTMLでは、式内で条件を判定し、その結果によって異なる動作をさせることができる。
判定された結果は、trueまたはfalseで返却される。
演算子の詳細については、Tutorial: Using Thymeleaf -Standard Expression Syntax-を参照されたい。

演算子を用いた条件の判定の結果によって、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.agenullだった場合、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:ifth:unlessの混在は可読性を低下させる場合があるため、いずれかへの統一を推奨する。

本ガイドラインにおいては、th:ifに統一している。

  • th:if属性を使用して表示を切り替える。

    <div th:if="${orderForm.orderStatus} != 'complete'"> <!--/* (1) */-->
            <!--/* ... */-->
    </div>
    
    :header-rows: 1 :widths: 10 90
    項番 説明
    (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>
    
    :header-rows: 1 :widths: 10 90
    項番 説明
    (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. ローカル変数を定義する

Thymeleafでは、テンプレートHTMLの特定の要素に定義され、その要素と要素配下のみで評価可能な変数のことをローカル変数と呼ぶ。
このローカル変数を定義する場合、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}__のように記述する。
式を構築する仕組みであるため、利用には細心の注意が必要となる。必ずWarningを参照されたい。
<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の機能( RequestMappingHandlerAdaptersynchronizeOnSession )で対策可能であり、 同一セッション内のリクエストの同期化 でも案内している。

上記の典型的なケースの対策が行われていても、Springの機能( RequestMappingHandlerAdaptersynchronizeOnSession )により同期化が行われるのはControllerの範囲であるため、ViewであるThymeleafのテンプレートHTMLでセッションスコープのフォームオブジェクトから値を取得している場合は、別スレッドから書き換えられた不正な値が得られる可能性がある。

当ガイドラインのプリプロセッシングに対する推奨事項としては以下の通りになる。

  1. プリプロセッシングを利用せずとも実装できないか検討を行う。
  2. プリプロセッシングを利用しなければならない場合は、サーバ上の安全なデータを用いる。
  3. どうしてもユーザの入力値などの安全でないデータをプリプロセッシングする必要がある場合は、入力値を利用する直前に入力チェックを行っていること、及びその値が状態により変更されないことを十分に確認した上で利用する。
    (ただし、上述の通り、開発者が問題を認識することが難しい上、問題を見逃した場合の影響が甚大であるため、安易に採用しないこと。)

3.4.3.1.14. フォームオブジェクトのプロパティをバインドする

Thymeleaf + Springの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. 権限によって表示を切り替える

ログインしているユーザの権限によって表示を切り替える場合は、ThymeleafのSpring Security連携用ダイアレクトで提供されているsec:authorize属性や #authorizationを使用する。
詳細は、認可を参照されたい。

3.4.3.2. JavaScriptの実装

画面描画後に画面項目の制御(表示/非表示、活性/非活性などの制御)を行う必要がある場合は、JavaScriptを使用して、項目の制御を行う。


3.4.3.3. スタイルシートの実装

画面のデザインに関わる属性値の指定はテンプレートHTMLに直接指定するのではなく、スタイルシート(cssファイル)に指定することを推奨する。
テンプレートHTMLでは、項目を一意に特定するためのid属性の指定と項目の分類を示すclass属性の指定を行い、実際の項目の配置や見た目にかかわる属性値の指定はスタイルシート(cssファイル)で指定する。
このような構成にすることで、HTMLの実装からデザインに関わる処理を減らすことができる。
同時にちょっとしたデザイン変更であれば、テンプレートHTMLを修正せずにスタイルシート(cssファイル)の修正のみで対応可能となる。

3.4.4. 共通処理の実装

3.4.4.1. Controllerの呼び出し前後で行う共通処理の実装

本項でいう共通処理とは、Controllerを呼び出し前後に行う必要がある共通的な処理のことを指す。


3.4.4.1.1. Servlet Filterの実装

Spring MVCに依存しない共通処理については、Servlet Filterで実装する。
ただし、Controllerのハンドラメソッドにマッピングされるリクエストに対してのみ共通処理を行いたい場合は、Servlet FilterではなくHandlerInterceptorで実装すること。
以下に、Servlet Filterのサンプルを示す。
サンプルコードでは、クライアントのIPアドレスをログ出力するために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の実装

Spring MVCに依存する共通処理については、 HandlerInterceptorで実装する。
HandlerInterceptorは、リクエストにマッピングされたハンドラメソッドが決定した後に呼び出されるので、アプリケーションが許可しているリクエストに対してのみ共通処理を行うことができる。

HandlerInterceptorでは以下の3つのポイントで処理を実行することが出来る。

  • Controllerのハンドラメソッドを実行する前
    HandlerInterceptor#preHandleメソッドとして実装する。
  • Controllerのハンドラメソッドが正常終了した後
    HandlerInterceptor#postHandleメソッドとして実装する。
  • Controllerのハンドラメソッドの処理が完了した後(正常/異常に関係なく実行される)
    HandlerInterceptor#afterCompletionメソッドとして実装する。
以下に、HandlerInterceptorのサンプルを示す。
サンプルコードでは、Controllerの処理が正常終了した後にinfoレベルのログを出力している。
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の引数として受け取れるようにする。

以下に、HandlerMethodArgumentResolverのサンプルを示す。
サンプルコードでは、 共通的なリクエストパラメータをJavaBeanに変換して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
}

上記例では、WelcomeControllerLoginController@LoginFormModelAttributeアノテーションを付与しているため、WelcomeControllerLoginControllerに共通処理が適用される。

(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
}

上記例では、SampleController@ISODateApplicableインタフェース(マーカーインタフェース)を実装しているため、SampleControllerに共通処理が適用される。

(3)
basePackageClasses

クラス又はインタフェースを指定する。

指定したクラス又はインタフェースのパッケージ配下のControllerに対して共通処理が適用される。

本属性を使用する場合は、

  • @ControllerAdviceを付与したクラス
  • パッケージを識別するためのマーカーインタフェース
を属性値に指定するスタイルを採用することを推奨する。
以下に指定例を示す。
package com.example.app

@ControllerAdvice(basePackageClasses = AppGlobalExceptionHandler.class)
public class AppGlobalExceptionHandler {
    // omitted
}
package com.example.app.sample

@Controller
public class SampleController {
    // omitted
}

上記例では、SampleController@ControllerAdviceを付与したクラス(AppGlobalExceptionHandler)が格納されているパッケージ(com.example.app)配下に格納されているため、SampleControllerに共通処理が適用される。

package com.example.app.common

@ControllerAdvice(basePackageClasses = AppPackage.class)
public class AppGlobalExceptionHandler {
    // omitted
}
package com.example.app

public interface AppPackage {
}

@ControllerAdviceが付与されているクラスとControllerが格納されているクラスのパッケージ階層が異なる場合や、複数のベースパッケージに共通処理を適用したい場合は、パッケージを識別するためのマーカインタフェースを用意すればよい。

(4)
basePackages

パッケージ名を指定する。

指定したパッケージ配下のControllerに対して共通処理が適用される。
以下に指定例を示す。
@ControllerAdvice(basePackages = "com.example.app")
public class AppGlobalExceptionHandler {
    // omitted
}
(5)
value

basePackagesへのエイリアス。

basePackages属性を指定した際と同じ動作となる。 以下に指定例を示す。

@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メソッドの実装サンプルを示す。
サンプルコードでは、 共通的なリクエストパラメータをJavaBeanに変換して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. セッションの使用について

Spring MVCのデフォルトの動作では、モデル(フォームオブジェクトやドメインオブジェクトなど)はセッションには格納されない。
セッションに格納したい場合は、@SessionAttributesアノテーションをControllerクラスに付与する必要がある。
入力フォームが複数の画面にわかれている場合は、 一連の画面遷移を行うリクエストでモデル(フォームオブジェクトやドメインオブジェクトなど)を共有できるため、 @SessionAttributesアノテーションの利用を検討すること。
ただし、セッションを使用する際の注意点があるので、そちらを確認した上で@SessionAttributesアノテーションの利用有無を判断すること。

セッションの利用指針及びセッション使用時の実装方法の詳細は、セッション管理を参照されたい。