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

目次

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

Note

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


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

  1. Controllerは、リクエストの受付、業務処理の呼び出し、モデルの更新、Viewの決定といった処理を行い、リクエストを受けてからの一連の処理フローを制御する。
    アプリケーション層の実装において、もっとも重要な実装となる。
  2. フォームオブジェクトは、HTML formとアプリケーションの間での値の受け渡しを行う。
  3. View(JSP)は、モデル(フォームオブジェクトやドメインオブジェクトなど)からデータを取得し、画面(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では処理結果に対する描画処理を実装せず、描画処理はJSP等のViewで実装する。
    Controllerでは描画処理が実装されているViewのView名の返却のみ行う。
    View名に対応するViewの解決は、Spring Frameworkより提供されているViewResolverによって行われ、処理結果に対応するView(JSPなど)が呼び出される仕組みになっている。
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)
フォーム画面を描画するためのJSPの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(JSP)を作成する。

<h1>Abc Create Form</h1>
<form:form modelAttribute="abcForm"
    action="${pageContext.request.contextPath}/abc/create">
    <form:label path="input1">Input1</form:label>
    <form:input path="input1" />
    <form:errors path="input1" />
    <br>
    <form:label path="input2">Input2</form:label>
    <form:input path="input2" />
    <form:errors path="input2" />
    <br>
    <input type="submit" name="confirm" value="Confirm" /> <!-- (1) -->
</form: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)
入力内容確認画面を描画するためのJSPのView名を返却する。

Note

POSTメソッドを指定させる理由は、個人情報やパスワードなどの秘密情報がブラウザのアドレスバーに現れ、他人に容易に閲覧されることを防ぐためである。(もちろんセキュリティ対策としては十分ではなく、SSLなどのセキュアなサイトにする必要がある)。


以下に、ハンドラメソッド以外の部分の実装例についても説明しておく。

入力内容確認表示を行う場合、ハンドラメソッドの実装以外に、

  • 入力内容確認画面のViewの実装。
が必要になる。
Viewの詳細はViewの実装を参照されたい。

入力内容確認画面のView(JSP)を作成する。

<h1>Abc Create Form</h1>
<form:form modelAttribute="abcForm"
    action="${pageContext.request.contextPath}/abc/create">
    <form:label path="input1">Input1</form:label>
        ${f:h(abcForm.input1)}
    <form:hidden path="input1" /> <!-- (1) -->
    <br>
    <form:label path="input2">Input2</form:label>
        ${f:h(abcForm.input2)}
    <form:hidden path="input2" /> <!-- (1) -->
    <br>
    <input type="submit" name="redo" value="Back" /> <!-- (2) -->
    <input type="submit" value="Create" /> <!-- (3) -->
</form:form>
項番 説明
(1)
フォーム画面で入力された値は、Createボタン及びBackボタンが押下された際に再度サーバに送る必要があるため、HTML formのhidden項目とする。
(2)
フォーム画面に戻るためのsubmitボタンにはname="redo"というパラメータを指定しておく。
(3)
新規作成を行うためのsubmitボタンにはパラメータ名の指定は不要。

Note

この例では確認項目を表示する際にHTMLエスケープするため、f:h()関数を使用している。XSS対策のため、必ず行うこと。

詳細についてはCross Site Scriptingを参照されたい。


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

入力内容確認表示処理を呼び出す。
フォーム画面で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)
入力内容確認画面を描画するためのJSPの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)
新規作成完了画面を描画するため、JSPの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.jsp)の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.jsp

    Message : ${f:h(hello)}<br> <%-- (4) --%>
    Message : ${f:h(helloBean.message)}<br> <%-- (5) --%>
    
  • HTML of created by View(hello.jsp)

    Message : Hello World!<br> <!-- (6) -->
    Message : Bean Hello World!<br> <!-- (6) -->
    
    項番 説明
    (1)
    Modelオブジェクトを引数として受け取る。
    (2)
    引数で受け取ったModelオブジェクトのaddAttributeメソッドを呼び出し、渡したいデータをModelオブジェクトに追加する。
    例では、hello という属性名で HelloWorld! という文字列のデータを追加している。
    (3)
    addAttributeメソッドの第一引数を省略するとConventions#getVariableNameの仕様に基づき、値のクラス名から属性名を決定する。
    例では、model.addAttribute("helloBean", new HelloBean());を行ったのと同じ結果となる。
    (4)
    View(JSP)側では、「${属性名}」と記述することでModelオブジェクトに追加したデータを取得することができる。
    例ではHTMLエスケープを行うEL式の関数を呼び出しているため、「${f:h(属性名)}」としている。
    HTMLエスケープを行うEL式の関数の詳細については、Cross Site Scriptingを参照されたい。
    (5)
    「${属性名.JavaBeanのプロパティ名}」と記述することでModelに格納されているJavaBeanから値を取得することができる。
    (6)
    JSP実行後に出力される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のjspはHTML formへのバインディング方法を参照されたい。
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.jsp

    Message : ${f:h(hello)}<br> <%-- (6) --%>
    Message : ${f:h(helloBean.message)}<br> <%-- (7) --%>
    
  • HTML of created by View(complete.jsp)

    Message : Hello World!<br> <!-- (8) -->
    Message : Bean Hello World!<br> <!-- (8) -->
    
項番 説明
(1)
RedirectAttributesオブジェクトを引数として受け取る。
(2)
RedirectAttributesオブジェクトのaddFlashAttributeメソッドを呼び出し、渡したいデータを RedirectAttributesオブジェクトに追加する。
例では、 hello という属性名で HelloWorld! という文字列のデータを追加している。
(3)
addFlashAttributeメソッドの第一引数を省略するとConventions#getVariableNameの仕様 に基づき、値のクラス名から属性名を決定する。
例では、model.addFlashAttribute("helloBean", new HelloBean());を行ったのと同じ結果となる。
(4)
画面(View)を直接表示せず、次の画面を表示するためのリクエストにリダイレクトする。
(5)
リダイレクト後のハンドラメソッドでは、(2)(3)で追加したデータを表示する画面のView名を返却する。
(6)
View(JSP)側では、「${属性名}」と記述することでRedirectAttributesを通じてflash scopeに追加したデータを取得することができ る。
例ではHTMLエスケープを行うEL式の関数を呼び出しているため、「${f:h(属性名)}」としている。
HTMLエスケープを行うEL式の関数の詳細については、Cross Site Scriptingを参照されたい。
(7)
「${属性名.JavaBeanのプロパティ名}」と記述することでRedirectAttributesに格納されているJavaBeanから値を取得することがで きる。
(8)
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として応答する場合、ハンドラメソッドの返り値は、JSPのView名を返却する。
JSPを使ってHTMLを生成する場合のViewResolverは、基本的にはUrlBasedViewResolverの継承クラス(InternalResourceViewResolverTilesViewResolver等)となる。
以下では、JSP用のInternalResourceViewResolverを使用する場合の例を記載する。
  • spring-mvc.xml

    <mvc:view-resolvers>
        <mvc:jsp prefix="/WEB-INF/views/" /> <!-- (1) -->
    </mvc:view-resolvers>
    
  • SampleController.java

    @GetMapping("hello")
    public String hello() {
        // omitted
        return "sample/hello"; // (2)
    }
    
    項番 説明
    (1)

    <mvc:jsp>要素を使用して、JSP用のInternalResourceViewResolverを定義する。

    • prefix属性には、JSPファイルが格納されているベースディレクトリ(ファイルパスのプレフィックス)を指定する。
    • suffix属性には、デフォルト値として.jspが適用されているため、明示的に指定する必要はない。

    Note

    <mvc:view-resolvers>要素を使用すると、ViewResolverをシンプルに定義することが出来るため、本ガイドラインでは<mvc:view-resolvers>を使用することを推奨する。

    (2)
    ハンドラメソッドの返り値としてsample/helloというView名を返却した場合、/WEB-INF/views/sample/hello.jspが呼び出されてHTMLが応答される。

Note

上記の例ではJSPを使ってHTMLを生成しているが、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) -->
        <mvc:jsp prefix="/WEB-INF/views/" />
    </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の定義順が優先順位となる。
    上記例では、JSP用のInternalResourceViewResolverを定義するための要素(<mvc:jsp>)より上に定義することで、JSP用のInternalResourceViewResolverより先にBeanNameViewResolverによるView解決が行われる。

    Note

    <mvc:view-resolvers>要素を使用すると、ViewResolverをシンプルに定義することが出来るため、本ガイドラインでは<mvc:view-resolvers>を使用することを推奨する。

    (2)
    ハンドラメソッドの返り値としてsample/reportというView名を返却した場合、 (5)で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(JSP) 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(JSP)にデータを渡すことを推奨する。


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) を型として指定できる。

TERASOLUNA Server Framework for Java (5.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にバインドするフォームオブジェクトの事を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 formへのバインディング方法

Modelに追加されたフォームオブジェクトは<form:xxx>タグを用いて、HTML(JSP)のformにバインドすることができる。
<form:xxx>タグの詳細は、Spring Framework Documentation -Spring’s form tag library-を参照されたい。
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <!-- (1) -->
<form:form modelAttribute="sampleForm"
           action="${pageContext.request.contextPath}/sample/hello"> <!-- (2) -->
    Id         : <form:input path="id" /><form:errors path="id" /><br /> <!-- (3) -->
    Name       : <form:input path="name" /><form:errors path="name" /><br />
    Age        : <form:input path="age" /><form:errors path="age" /><br />
    Gender     : <form:input path="genderCode" /><form:errors path="genderCode" /><br />
    Birth Date : <form:input path="birthDate" /><form:errors path="birthDate" /><br />
</form:form>
項番 説明
(1)
<form:form>タグを使用するためのtaglibの定義を行う。
(2)
<form:form>タグのmodelAttribute属性には、Modelに格納されているフォームオブジェクトの属性名を指定する。
(3)
<form:input>タグのpath属性には、フォームオブジェクトのプロパティ名を指定する。

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. JSPの実装

クライアントにHTMLを応答する場合は、JSPを使用してViewを実装する。
JSPを呼び出すためのViewResolverは、Spring Frameworkより提供されているので、提供されているクラスを利用する。ViewResolverの設定方法は、HTMLを応答するを参照されたい。

以下に、基本的なJSPの実装方法について説明する。

本章では代表的なJSPタグライブラリの使い方は説明しているが、全てのJSPタグライブラリの説明はしていないので、詳細な使い方については、それぞれのドキュメントを参照すること。


3.4.3.1.1. インクルード用の共通JSPの作成

全てのJSPで必要となるディレクティブの宣言などを行うためのJSPを作成する。
このJSPをweb.xml<jsp-config>/<jsp-property-group>/<include-prelude>要素に指定することで、個々のJSPで宣言する必要がなくなる。
なお、このファイルはブランクプロジェクトで提供している。
  • include.jsp

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%-- (1) --%>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
    
    <%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%> <%-- (2) --%>
    <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
    <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
    
    <%@ taglib uri="http://terasoluna.org/functions" prefix="f"%> <%-- (3) --%>
    <%@ taglib uri="http://terasoluna.org/tags" prefix="t"%>
    
  • web.xml

    <jsp-config>
        <jsp-property-group>
            <url-pattern>*.jsp</url-pattern>
            <el-ignored>false</el-ignored>
            <page-encoding>UTF-8</page-encoding>
            <scripting-invalid>false</scripting-invalid>
            <include-prelude>/WEB-INF/views/common/include.jsp</include-prelude> <!-- (4) -->
        </jsp-property-group>
    </jsp-config>
    
    項番 説明
    (1)
    JSTLのJSPタグライブラリを宣言している。 例では、corefmtを利用している。
    (2)
    Spring FrameworkのJSPタグライブラリを宣言している。 例では、springformsecを利用している。
    (3)
    共通ライブラリから提供しているJSPタグライブラリを宣言している。
    (4)
    インクルード用のJSP(/WEB-INF/views/common/include.jsp)に指定した内容が、各JSP(<url-pattern>で指定されているファイル)の先頭にインクルードされる。

    Note

    ディレクティブの詳細は、Jakarta Server Pages 3.1 Specification Document(1.10. Directives)を参照されたい。

    Note

    <jsp-property-group>要素の詳細は、Jakarta Server Pages 3.1 Specification Document(3.3. JSP Property Groups)を参照されたい。


3.4.3.1.2. モデルに格納されている値を表示する

モデル(フォームオブジェクトやドメインオブジェクトなど)に格納されている値をHTMLに表示する場合、EL式又はJSTLから提供されているJSPタグライブラリを使用する。

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.jsp

    Message : ${f:h(helloBean.message)} <%-- (2) --%>
    
    項番 説明
    (1)
    Modelオブジェクトに HelloBeanオブジェクトを追加する。
    (2)
    View(JSP)側では、「${属性名.JavaBeanのプロパティ名}」と記述することでModelオブジェクトに追加したデータを取得することができる。
    例ではHTMLエスケープを行うEL式の関数を呼び出しているため、「${f:h(属性名.JavaBeanのプロパティ名)}」としている。

    Note

    共通部品よりEL式用のHTMLエスケープ関数(f:h)を提供しているので、EL式を使用してHTMLに値を出力する場合は、必ず使用すること。

    HTMLエスケープを行うEL式の関数の詳細については、Cross Site Scriptingを参照されたい。

JSTLのJSPタグライブラリから提供されている<c:out>タグを使用して表示する。

Message : <c:out value="${helloBean.message}" /> <%-- (1) --%>
項番 説明
(1)
EL式で取得した値を<c:out>タグのvalue属性に指定する。HTMLエスケープも行われる。

3.4.3.1.3. モデルに格納されている数値を表示する

数値型の値をフォーマットして出力する場合、JSTLから提供されているJSPタグライブラリを使用する。

JSTLのJSPタグライブラリから提供されている<fmt:formatNumber>タグを使用して表示する。
Number Item : <fmt:formatNumber value="${helloBean.numberItem}" pattern="0.00" /> <%-- (1) --%>
項番 説明
(1)
EL式で取得した値を<fmt:formatNumber>タグのvalue属性に指定する。表示するフォーマットはpattern属性に指定する。例では、0.00を指定している。
仮に${helloBean.numberItem}で取得した値が1.2の場合、画面には1.20が出力される。

Note

<fmt:formatNumber>の詳細は、Jakarta Standard Tag Library 3.0 Specification Document(9 Formatting Actions)を参照されたい。


3.4.3.1.4. モデルに格納されている日時を表示する

日時型の値をフォーマットして出力する場合、JSTLから提供されているJSPタグライブラリを使用する。

JSTLのJSPタグライブラリから提供されている<fmt:formatDate>タグを使用して表示する。

Date Item : <fmt:formatDate value="${helloBean.dateItem}" pattern="yyyy-MM-dd" /> <%-- (1) --%>
項番 説明
(1)
EL式で取得した値を<fmt:formatDate>タグのvalue属性に指定する。表示するフォーマットはpattern属性に指定する。例では、yyyy-MM-ddを指定している。
仮に${helloBean.dateItem}で取得した値が2013年3月2日の場合、画面には2013-03-02が出力される。

Note

<fmt:formatNumber>の詳細は、Jakarta Standard Tag Library 3.0 Specification Document(9 Formatting Actions)を参照されたい。

Note

日時オブジェクトの型として、JSR-310 Date and Time APIから提供されているjava.time.LocalDateTimeなどを利用する場合は、日付操作(JSR-310 Date and Time API)を参照されたい。


3.4.3.1.5. リクエストURLを生成する

HTMLの<form>要素(JSPタグライブラリの<form:form>要素)のaction属性や<a>要素のhref属性などに対してリクエストURL(Controllerのメソッドを呼び出すためのURL)を設定する場合は、 以下のいずれかの方法を使用してURLを生成する。

  • 文字列としてリクエストURLを組み立てる
  • Spring Framework 4.1から追加されたEL関数を使用してリクエスト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となる。

文字列としてリクエストURLを組み立てる

まず、文字列としてリクエストURLを組み立てる方法について説明する。

<form action="${pageContext.request.contextPath}/hello"> <!-- (2) -->
    <!-- omitted -->
</form>
項番 説明
(2)
pageContext(JSPの暗黙オブジェクト)からWebアプリケーションに割り振られているコンテキスパスを取得し(${pageContext.request.contextPath})、コンテキストパスの後ろに呼び出すControllerのメソッドに割り振られているサーブレットパス(上記例では、/hello)を加える。

Tip

URLを組み立てるJSPタグライブラリとして、

  • JSTLから提供されている<c:url>
  • Spring Frameworkから提供されている<spring:url>

が存在する。これらのJSPタグライブラリを使用して、リクエストURLを組み立ててもよい。

リクエストURLを動的に組み立てる必要がある場合は、これらのJSPタグライブラリを使用してURLを組み立てた方がよいケースがある。


Spring Framework 4.1から追加されたEL関数を使用してリクエストURLを組み立てる

つぎに、Spring Framework 4.1から追加されたEL関数(spring:mvcUrl)を使用してリクエストURLを組み立てる方法について説明する。

spring:mvcUrl関数を使用すると、Controllerのメソッドのメタ情報(メソッドシグネチャやアノテーションなど)と連携して、リクエストURLを組み立てる事ができる。

<form action="${spring:mvcUrl('HC#hello').build()}"> <!-- (3) -->
    <!-- omitted -->
</form>
項番 説明
(3)

spring:mvcUrl関数の引数には、呼び出すControllerのメソッドに割り振られているリクエストマッピング名を指定する。

spring:mvcUrl関数からは、リクエストURLを組み立てるクラス(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メソッドの具体的な使用例については、Spring Framework Documentation -Links in Views-を参照されたい。

Note

リクエストマッピング名について

リクエストマッピング名は、デフォルト実装(org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMethodMappingNamingStrategyの実装)では、「クラス名の大文字部分(クラスの短縮名) + “#” + メソッド名」となる。

リクエストマッピング名は重複しないようにする必要がある。名前が重複してしまった場合は、@RequestMapping合成アノテーションのname属性に一意となる名前を指定する必要がある。


3.4.3.1.6. HTML formへフォームオブジェクトをバインドする

HTML formへフォームオブジェクトをバインドし、フォームオブジェクトで保持している値を表示する場合、Spring Frameworkから提供されているJSPタグライブラリを使用する。

Spring Frameworkから提供されている <form:form> タグを使用してバインドする。

<form:form action="${pageContext.request.contextPath}/sample/hello"
           modelAttribute="sampleForm"> <%-- (1) --%>
    Id : <form:input path="id" /> <%-- (2) --%>
</form:form>
項番 説明
(1)
<form:form>タグのmodelAttribute属性に、Modelに格納されているフォームオブジェクトの属性名を指定する。
(2)
<form:xxx>タグのpath属性に、バインドしたいプロパティのプロパティ名を指定する。xxxの部分は、入力項目のタイプによってかわる。

Note

<form:form><form:xxx>タグの詳細は、Spring Framework Documentation -Spring’s form tag library-を参照されたい。


3.4.3.1.7. 入力チェックエラーを表示する

入力チェックエラーの内容を表示する場合、Spring Frameworkから提供されているJSPタグライブラリを使用する。

Spring Frameworkから提供されている<form:errors>タグを使用して表示する。
詳細は、入力チェックを参照されたい。
<form:form action="${pageContext.request.contextPath}/sample/hello"
           modelAttribute="sampleForm">
    Id : <form:input path="id" /><form:errors path="id" /><%-- (1) --%>
</form:form>
項番 説明
(1)
<form:errors>タグのpath属性に、エラー表示したいプロパティのプロパティ名を指定する。

3.4.3.1.8. 処理結果のメッセージを表示する

処理結果を通知するメッセージを表示する場合、共通部品から提供しているJSPタグライブラリを使用する。

共通部品から提供している<t:messagesPanel>タグを使用する。
詳細は、メッセージ管理を参照されたい。
<div class="messages">
    <h2>Message pattern</h2>
    <t:messagesPanel /> <%-- (1) --%>
</div>
項番 説明
(1)
resultMessagesという属性名で格納されているメッセージを出力する。

3.4.3.1.9. コードリストを表示する

共通部品から提供されているコードリストを表示する場合は、Spring Frameworkから提供されているJSPタグライブラリを使用する。

JSPからコードリストを参照する場合は、java.util.Mapインタフェースと同じ方法で参照することができる。
詳細は、コードリストを参照されたい。

コードリストをセレクトボックスに表示する。

<form:select path="orderStatus">
    <form:option value="" label="--Select--" />
    <form:options items="${CL_ORDERSTATUS}" /> <%-- (1) --%>
</form:select>
項番 説明
(1)
コードリスト名(CL_ORDERSTATUS) を属性名として、コードリスト(java.util.Mapインタフェース)が格納されている。
そのためJSPでは、EL式を使ってコードリスト(java.util.Mapインタフェース)にアクセスすることができる。
取得したMapインタフェースを<form:options>のitems属性に渡すことで、コードリストをセレクトボックスに表示することができる。

セレクトボックスで選択した値のコード名を表示する。

Order Status : ${f:h(CL_ORDERSTATUS[orderForm.orderStatus])}
項番 説明
(1)
セレクトボックス作成時と同様に、コードリスト名(CL_ORDERSTATUS) を属性名として、コードリスト(java.util.Mapインタフェース)を取得する。
取得したMapインタフェースのキー値として、セレクトボックスで選択した値を指定することで、コード名を表示することができる。

3.4.3.1.10. 固定文言を表示する

画面名、項目名、ガイダンス用のメッセージなどについては、国際化の必要がない場合はJSPに直接記載してもよい。
ただし、国際化の必要がある場合はSpring Frameworkから提供されているJSPタグライブラリを使用して、プロパティファイルから取得した値を表示する。
Spring Frameworkから提供されている<spring:message>タグを使用して表示する。
詳細は、国際化を参照されたい。
  • properties

    # (1)
    label.orderStatus=注文ステータス
    
  • jsp

    <spring:message code="label.orderStatus" text="Order Status" /> : <%-- (2) --%>
        ${f:h(CL_ORDERSTATUS[orderForm.orderStatus])}
    
    項番 説明
    (1)
    プロパティファイルにラベルの値を定義する。
    (2)
    <spring:message>のcode属性にプロパティファイルのキー名を指定するとキー名に一致するプロパティ値が表示される。

Note

text属性に指定した値は、プロパティ値が取得できなかった場合に表示される。


3.4.3.1.11. 条件によって表示を切り替える

モデルが保持する値によって表示を切り替えたい場合は、JSTLから提供されているJSPタグライブラリを使用する。

JSTLのJSPタグライブラリから提供されている<c:if>タグ又は<c:choose>を使用して、表示の切り替えを行う。

<c:if>を使用して表示を切り替える。

<c:if test="${orderForm.orderStatus != 'complete'}"> <%-- (1) --%>
    <%-- omitted --%>
</c:if>
項番 説明
(1)
<c:if>のtest属性に分岐に入る条件を実装する。例では注文ステータスが'complete'ではない場合に分岐内の表示処理が実行される。

<c:choose>を使用して表示を切り替える。

<c:choose>
    <c:when test="${customer.type == 'premium'}"> <%-- (1) --%>
        <%-- omitted --%>
    </c:when>
    <c:when test="${customer.type == 'general'}">
        <%-- omitted --%>
    </c:when>
    <c:otherwise> <%-- (2) --%>
        <%-- omitted --%>
    </c:otherwise>
</c:choose>
項番 説明
(1)
<c:when>タグのtest属性に分岐に入る条件を実装する。例では顧客の種別が'premium'の場合に分岐内の表示処理が実行される。
test属性で指定した条件がfalseの場合は、次の<c:when>タグの処理が実行される。
(2)
全ての<c:when>タグのtest属性の結果がfalseの場合、<c:otherwise>タグ内の表示処理が実行される。

3.4.3.1.12. コレクションの要素に対して表示処理を繰り返す

モデルが保持するコレクションに対して表示処理を繰り返したい場合は、JSTLから提供されているJSPタグライブラリを使用する。

JSTLのJSPタグライブラリから提供されている<c:forEach>を使用して表示処理を繰り返す。

<table>
    <tr>
        <th>No</th>
        <th>Name</th>
    </tr>
    <c:forEach var="customer" items="${customers}" varStatus="status"> <%-- (1) --%>
        <tr>
            <td>${status.count}</td> <%-- (2) --%>
            <td>${f:h(customer.name)}</td> <%-- (3) --%>
        </tr>
    </c:forEach>
</table>
項番 説明
(1)
<c:forEach>タグのitems属性にコレクションを指定する事で、<c:forEach>タグ内の表示処理が繰り返し実行される。
処理対象となっている要素のオブジェクトを参照する場合は、var属性にオブジェクトを格納するための変数名を指定する。
(2)
<c:forEach>タグのvarStatus属性で指定した変数から現在処理を行っている要素位置(count)を取得している。
count以外の属性については、jakarta.servlet.jsp.jstl.core.LoopTagStatusのJavaDocを参照されたい。
(3)
<c:forEach>タグのvar属性で指定した変数に格納されているオブジェクトから値を取得している。

3.4.3.1.13. ページネーション用のリンクを表示する

一覧表示を行う画面にてページネーション用のリンクを表示する場合は、共通部品から提供しているJSPタグライブラリを使用する。

共通部品から提供している<t:pagination>を使用してページネーション用のリンクを表示する。
詳細は、 ページネーション を参照されたい。

3.4.3.1.14. 権限によって表示を切り替える

ログインしているユーザの権限によって表示を切り替える場合は、Spring Securityから提供されているJSPタグライブラリを使用する。

Spring Securityから提供されている<sec:authorize>を使用して表示の切り替えを行う。
詳細は、認可を参照されたい。

3.4.3.2. JavaScriptの実装

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


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

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

Note

<form:xxx>タグを使ってフォームを生成した場合、id属性は自動で設定される。class属性については、アプリケーション開発者によって指定が必要。


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アノテーションの利用有無を判断すること。

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