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

目次

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

Note

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


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

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

4.4.1. Controllerの実装

まず、Controllerの実装から説明する。
Controllerは、以下5つの役割を担う。
  1. リクエストを受け取るためのメソッドを提供する。
    @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つの点に着目して説明する。


4.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 {
    // ...
}


4.4.1.2. リクエストとハンドラメソッドのマッピング方法

リクエストを受け取るメソッドは、@RequestMappingアノテーションを付与する。
本ガイドラインでは、@RequestMappingが付加されたメソッドのことを「ハンドラメソッド」と呼ぶ。
@RequestMapping(value = "hello")
public String hello() {
    // ...
}

リクエストとハンドラメソッドをマッピングするためのルールは、@RequestMappingアノテーションの属性に指定する。

項番 属性名 説明
value
マッピング対象にするリクエストパスを指定する(複数可)。
method
マッピング対象にするHTTPメソッド(RequestMethod型)を指定する(複数可)。
GET/POSTについてはHTML form向けのリクエストをマッピングする際にも使用するが、それ以外のHTTPメソッド(PUT/DELETEなど)はREST API向けのリクエストをマッピングする際に使用する。
params
マッピング対象にするリクエストパラメータを指定する(複数可)。
主にHTML form向けのリクエストをマッピングする際に使用する。このマッピング方法を使用すると、HTML form上に複数のボタンが存在する場合のマッピングを簡単に実現する事ができる。
headers
マッピング対象とするリクエストヘッダを指定する(複数可)。
主にREST APIやAjax向けのリクエストをマッピングする際に使用する。
consumes
リクエストのContent-Typeヘッダを使ってマッピングすることが出来る。マッピング対象とするメディアタイプを指定する(複数可)。
主にREST APIやAjax向けのリクエストをマッピングする際に使用する。
produces
リクエストのAcceptヘッダを使ってマッピングすることが出来る。マッピング対象とするメディアタイプを指定する(複数可)。
主にREST APIやAjax向けのリクエストをマッピングする際に使用する。

Note

マッピングの組み合わせについて

複数の属性を組み合わせることで複雑なマッピングを行うことも可能だが、保守性を考慮し、可能な限りシンプルな定義になるようにマッピングの設計を行うこと。 2つの属性の組み合わせ(value属性と別の属性1つ)を目安にすることを推奨する。


以下、マッピングの具体例を6つ示す。
以降の説明では、以下のControllerクラスにハンドラメソッドを定義する前提となっている。
@Controller // (1)
@RequestMapping("sample") // (2)
public class SampleController {
    // ...
}
項番 説明
(1)
@Controllerアノテーションを付加することでAnnotation-basedなコントローラークラスとして認識され、component scanの対象となる。
(2)
クラスレベルで@RequestMapping("sample")アノテーションを付けることでこのクラス内のハンドラメソッドがsample配下のURLにマッピングされる。

4.4.1.2.1. リクエストパスでマッピング

下記の定義の場合、"sample/hello" というURLにアクセスすると、helloメソッドが実行される。

@RequestMapping(value = "hello")
public String hello() {
複数指定した場合は、OR条件で扱われる。
下記の定義の場合、 "sample/hello" 又は "sample/bonjour" というURLにアクセスすると、helloメソッドが実行される。
@RequestMapping(value = {"hello", "bonjour"})
public String hello() {

指定するリクエストパスは、具体的な値ではなくパターンを指定することも可能である。パターン指定の詳細は、Spring FrameworkのReference Documentを参照。


4.4.1.2.2. HTTPメソッドでマッピング

下記の定義の場合、 "sample/hello" というURLにPOSTメソッドでアクセスすると、helloメソッドが実行される。 サポートしているHTTPメソッドの一覧は RequestMethodのJavadoc を参照されたい。 指定しない場合、サポートしている全てのHTTPメソッドがマッピング対象となる。

@RequestMapping(value = "hello", method = RequestMethod.POST)
public String hello() {
複数指定した場合は、OR条件で扱われる。
下記の定義の場合、 "sample/hello" というURLにGET又はHEADメソッドでアクセスすると、helloメソッドが実行される。
@RequestMapping(value = "hello", method = {RequestMethod.GET, RequestMethod.HEAD})
public String hello() {

4.4.1.2.3. リクエストパラメータでマッピング

下記の定義の場合、 "sample/hello?form" というURLにアクセスすると、helloメソッドが実行される。
POSTでリクエストする場合は、リクエストパラメータはURLになくてもリクエストBODYに存在していればよい。
@RequestMapping(value = "hello", params = "form")
public String hello() {
複数指定した場合は、AND条件で扱われる。
下記の定義の場合、 "sample/hello?form&formType=foo" というURLにアクセスすると、helloメソッドが実行される。
@RequestMapping(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でない場合にマッピングされる。

4.4.1.2.4. リクエストヘッダでマッピング

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

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

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

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

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



4.4.1.3. リクエストとハンドラメソッドのマッピング方針

以下の方針でマッピングを行うことを推奨する。

  • 業務や機能といった意味のある単位で、リクエストのURLをグループ化する。
    URLのグループ化とは、 @RequestMapping(value = "xxx")をクラスレベルのアノテーションとして定義することを意味する。
  • 処理内の画面フローで使用するリクエストのURLは、同じURLにする。
    同じURLとは @RequestMapping(value = "xxx")のvalue属性の値を同じ値にすることを意味する。
    処理内の画面フローで使用するハンドラメソッドの切り替えは、HTTPメソッドとHTTPパラメータによって行う。

以下にベーシックな画面フローを行うサンプルアプリケーションを例にして、リクエストとハンドラメソッドの具体的なマッピング例を示す。


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


4.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 Template Patternsと呼ばれ、任意の値を指定する事ができる。 サンプルアプリケーションでは、操作するEntityのIDを指定する。

画面フロー図に各処理に割り振られたURLをマッピングすると以下のようになる。

Screen flow of entity management function and assigned URL

Picture - Screen flow of entity management function and assigned URL


4.4.1.3.3. リクエストとハンドラメソッドのマッピング

リクエストとハンドラメソッドのマッピングの設計を行う。
以下は、マッピング方針に則って設計したマッピング定義となる。
項番
処理名
URL
リクエスト名
HTTP
メソッド
HTTP
パラメータ
ハンドラメソッド
Entity一覧取得 /abc/list 一覧表示 GET - list
Entity新規作成 /abc/create フォーム表示 - form createForm
    入力内容確認表示 POST confirm createConfirm
    フォーム再表示 POST redo createRedo
    新規作成 POST - create
    新規作成完了表示 GET complete createComplete
Entity参照 /abc/{id} 詳細表示 GET - read
Entity更新 /abc/{id}/update フォーム表示 - 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の書き方に注目すること。
ハンドラメソッドの引数や返り値(View名及びView)の詳細については、次章以降で説明する。

4.4.1.3.4. フォーム表示の実装

フォーム表示する場合は、HTTPパラメータとして form を指定させる。

@RequestMapping(value = "create", params = "form") // (1)
public String createForm(AbcForm form, Model model) {
    // omitted
    return "abc/createForm"; // (2)
}
項番 説明
(1)
params属性に "form" を指定する。
(2)
フォーム画面を描画するためのJSPのView名を返却する。

Note

この処理でHTTPメソッドをGETに限る必要がないのでmethod属性を指定していない。


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

フォーム表示を行う場合、ハンドラメソッドの実装以外に、

  • フォームオブジェクトの生成処理の実装。フォームオブジェクトの詳細は、 フォームオブジェクトの実装 を参照されたい。
  • フォーム画面の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

4.4.1.3.5. 入力内容確認表示の実装

フォームの入力内容を確認する場合は、POSTメソッドでデータを送信し、HTTPパラメータに confirm を指定させる。

@RequestMapping(value = "create", method = RequestMethod.POST, 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)
method属性に RequestMethod.POST 、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

4.4.1.3.6. フォーム再表示の実装

フォームを再表示する場合は、HTTPパラメータにredoを指定させる。

@RequestMapping(value = "create", method = RequestMethod.POST, params = "redo") // (1)
public String createRedo(AbcForm form, Model model) {
    // omitted
    return "abc/createForm"; // (2)
}
項番 説明
(1)
method属性に RequestMethod.POST 、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()" を設定する方法もある。 両者では以下が異なり、要件に応じて選択する必要がある。

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

4.4.1.3.7. 新規作成の実装

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

4.4.1.3.8. 新規作成完了表示の実装

新規作成処理が完了した事を通知する場合は、HTTPパラメータに complete を指定させる。

@RequestMapping(value = "create", params = "complete") // (1)
public String createComplete() {
    // omitted
    return "abc/createComplete"; // (2)
}
項番 説明
(1)
params属性に "complete" を指定する。
(2)
新規作成完了画面を描画するため、JSPのView名を返却する。

Note

この処理もHTTPメソッドをGETに限る必要がないのでmethod属性を指定しなくても良い。


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

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

Note

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


4.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ボタン押下時の動作については、 フォーム再表示の実装 を参照されたい。


4.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 "/abc/create?form"
    @RequestMapping(value = "create", params = "form")
    public String createForm(AbcForm form, Model model) {
        // omitted
        return "abc/createForm";
    }

    // Handling request of "POST /abc/create?confirm"
    @RequestMapping(value = "create", method = RequestMethod.POST, 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"
    @RequestMapping(value = "create", method = RequestMethod.POST, params = "redo")
    public String createRedo(AbcForm form, Model model) {
        // omitted
        return "abc/createForm";
    }

    // Handling request of "POST /abc/create"
    @RequestMapping(value = "create", method = RequestMethod.POST)
    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 "/abc/create?complete"
    @RequestMapping(value = "create", params = "complete")
    public String createComplete() {
        // omitted
        return "abc/createComplete";
    }

}


4.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例示す。


4.4.1.4.1. 画面(View)にデータを渡す

画面(View)に表示するデータを渡したい場合は、org.springframework.ui.Model(以降 Model と呼ぶ) をハンドラメソッドの引数として受け取り、 Modelオブジェクトに渡したいデータ(オブジェクト)を追加する。

  • SampleController.java
@RequestMapping("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メソッドの第一引数を省略すると値のクラス名の先頭を小文字にした文字列が属性名になる。
例では、 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など)からも値を参照することが出来る。


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

URLのパスから値を取得する場合は、引数に@PathVariableアノテーションを付与する。
@PathVariableアノテーションを使用してパスから値を取得する場合、 @RequestMappingアノテーションのvalue属性に取得したい部分を変数化しておく必要がある。
@RequestMapping("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)
@RequestMappingアノテーションのvalue属性に、抜き出したい箇所をパス変数として指定する。パス変数は、「{変数名}」の形式で指定する。
上記例では、 "id""version" という二つのパス変数を指定している。
(2)
@PathVariableアノテーションのvalue属性には、パス変数の変数名を指定する。
上記例では、 "sample/hello/aaaa/1" というURLにアクセスした場合、引数idに文字列 "aaaa" が渡る。
(3)
@PathVariableアノテーションのvalue属性は省略可能で、省略した場合は引数名がリクエストパラメータ名となる。
上記例では、 "sample/hello/aaaa/1" というURLにアクセスした場合、引数versionに数値 "1" が渡る。
ただしこの方法は、
  • -gオプション(デバッグ情報を出力するモード)
  • Java8から追加された-parametersオプション(メソッド・パラメータにリフレクション用のメタデータを生成するモード)

のどちらかを指定してコンパイルする必要がある。

Note

バインドする引数の型はString以外でも良い。型が合わない場合はorg.springframework.beans.TypeMismatchExceptionがスローされ、デフォルトの動作は400(Bad Request)が応答される。 例えば、上記例で "sample/hello/aaaa/v1" というURLでアクセスした場合、"v1" をIntegerに変換できないため、例外がスローされる。

Warning

@PathVariableアノテーションのvalue属性を省略する場合、デプロイするアプリケーションは-gオプション又はJava8から追加された-parametersオプションを指定してコンパイルする必要がある。 これらのオプションを指定した場合、コンパイル後のクラスにはデバッグ時に必要となる情報や処理などが挿入されるため、メモリや処理性能に影響を与えることがあるので注意が必要である。 基本的には、value属性を明示的に指定する方法を推奨する。


4.4.1.4.3. リクエストパラメータを個別に取得する

リクエストパラメータを1つずつ取得したい場合は、引数に@RequestParamアノテーションを付与する。

@RequestMapping("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つ以上のリクエストパラメータをバインドする。(保守性、可読性の観点)

4.4.1.4.4. リクエストパラメータをまとめて取得する

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

以下は、@RequestParam で個別にリクエストパラメータを受け取っていたハンドラメソッドを、フォームオブジェクトで受け取るように変更した場合の実装例である。

@RequestParam を使って個別にリクエストパラメータを受け取っているハンドラメソッドは以下の通り。

@RequestMapping("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 を使って個別に受け取っていたリクエストパラメータをフォームオブジェクトとして受け取るようにする。

@RequestMapping("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オブジェクトにコピーすること。


4.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に格納されている。
@RequestMapping("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 が返却される。

4.4.1.4.6. リダイレクト先にデータを渡す

ハンドラメソッドを実行した後にリダイレクトする場合に、リダイレクト先で表示するデータを渡したい場合は、org.springframework.web.servlet.mvc.support.RedirectAttributes(以降RedirectAttributesと呼ぶ) をハンドラメソッドの引数として受け取り、 RedirectAttributesオブジェクトに渡したいデータを追加する。

  • SampleController.java
@RequestMapping("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)
}

@RequestMapping(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メソッドの第一引数を省略すると値に渡したオブジェクトのクラス名の先頭を小文字にした文字列が属性名になる。
例では、 model.addFlashAttribute("helloBean", new HelloBean()); を行ったのと同じ結果となる。
(4)
画面(View)を直接表示せず、次の画面を表示するためのリクエストにリダイレクトする。
(5)
リダイレクト後のハンドラメソッドでは、(2)(3)で追加したデータを表示する画面のView名を返却する。
(6)
View(JSP)側では、「${属性名}」と記述することでRedirectAttributesオブジェクトに追加したデータを取得することができる。
例では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


4.4.1.4.7. リダイレクト先へリクエストパラメータを渡す

リダイレクト先へ動的にリクエストパラメータを設定したい場合は、引数のRedirectAttributesオブジェクトに渡したい値を追加する。

@RequestMapping("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に直接指定してよい。


4.4.1.4.8. リダイレクト先URLのパスに値を埋め込む

リダイレクト先URLのパスに動的に値を埋め込みたい場合は、リクエストパラメータの設定と同様引数のRedirectAttributesオブジェクトに埋め込みたい値を追加する。

@RequestMapping("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メソッドを使用し、パス変数を使って埋め込むこと。


4.4.1.4.10. Cookieに値を書き込む

Cookieに値を書き込む場合は、HttpServletResponseオブジェクトのaddCookieメソッドを直接呼び出してCookieに追加する。
Spring MVCからCookieに値を書き込む仕組みが提供されていないため(3.2.3時点)、この場合に限り HttpServletResponse を引数に取っても良い。
@RequestMapping("writeCookie")
public String writeCookie(Model model,
        HttpServletResponse response) { // (1)
    Cookie cookie = new Cookie("foo", "hello world!");
    response.addCookie(cookie); // (2)
    // do something
    return "sample/writeCookie";
}
項番 説明
(1)
Cookieを書き込むために、HttpServletResponseオブジェクトを引数に指定する。
(2)
Cookieオブジェクトを生成し、HttpServletResponseオブジェクトに追加する。
上記例では、 "foo" というCookie名で "hello world!" という値を設定している。

Tip

HttpServletResponseを引数として受け取ることに変わりはないが、Cookieに値を書き込むためのクラスとして、 Spring Frameworkからorg.springframework.web.util.CookieGeneratorというクラスが提供されている。必要に応じて使用すること。


4.4.1.4.11. ページネーション情報を取得する

一覧検索を行うリクエストでは、ページネーション情報が必要となる。
org.springframework.data.domain.Pageable(以降Pageableと呼ぶ) オブジェクトをハンドラメソッドの引数に取ることで、ページネーション情報(ページ数、取得件数)を容易に扱うことができる。
詳細については ページネーション を参照すること。

4.4.1.4.12. アップロードファイルを取得する

アップロードされたファイルを取得する方法は大きく2つある。

  • フォームオブジェクトにMultipartFileのプロパティを用意する。
  • @RequestParamアノテーションを付与してorg.springframework.web.multipart.MultipartFileをハンドラメソッドの引数とする。

詳細については ファイルアップロード を参照されたい。


4.4.1.4.13. 画面に結果メッセージを表示する

Modelオブジェクト又はRedirectAttributesオブジェクトをハンドラメソッドの引数として受け取り、 ResultMessagesオブジェクトを追加することで処理の結果メッセージを表示できる。

詳細については メッセージ管理 を参照されたい。



4.4.1.5. ハンドラメソッドの返り値について

ハンドラメソッドの返り値についても様々な値をとることができる が、 基本的には次に挙げるもののみを使用すること。

  • String(View論理名)

以下に、目的別に返り値の使用方法について説明する。


4.4.1.5.1. HTMLを応答する

ハンドラメソッドの実行結果をHTMLとして応答する場合、ハンドラメソッドの返り値は、JSPのView名を返却する。
JSPを使ってHTMLを生成する場合のViewResolverは、基本的にはUrlBasedViewResolverの継承クラス(InternalViewResolverTilesViewResolver等)となる。
以下では、JSP用のInternalViewResolverを使用する場合の例を記載するが、画面レイアウトがテンプレート化されている場合はTilesViewResolverを使用することを推奨する。
TilesViewResolverの使用方法については、 Tilesによる画面レイアウト を参照されたい。
  • spring-mvc.xml

<bean>要素を使用する場合の定義例

<!-- (1) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/" /> <!-- (2) -->
    <property name="suffix" value=".jsp" /> <!-- (3) -->
    <property name="order" value="1" /> <!-- (4) -->
</bean>

Spring Framework 4.1から追加された<mvc:view-resolvers>要素を使用する場合の定義例

<mvc:view-resolvers>
    <mvc:jsp prefix="/WEB-INF/views/" /> <!-- (5) -->
</mvc:view-resolvers>
  • SampleController.java
@RequestMapping("hello")
public String hello() {
    // omitted
    return "sample/hello"; // (6)
}
項番 説明
(1)
JSP用のInternalViewResolverを定義する。
(2)

JSPファイルが格納されているベースディレクトリ(ファイルパスのプレフィックス)を指定する。

プレフィックスを指定しておくことで、ControllerでView名を返却する際に、JSPの物理的な格納場所を意識する必要がなくなる。

(3)

JSPファイルの拡張子(ファイルパスのサフィックス)を指定する。

サフィックスを指定しておくことで、ControllerでView名を返却する際に、JSPの拡張子を意識する必要がなくなる。

(4)

複数のViewResolverを指定した場合の実行順番を指定する。

Integerの範囲で指定することが可能で、値が小さいものから順に実行される。

(5)

Spring Framework 4.1から追加された<mvc:jsp>要素に使用して、JSP用のInternalViewResolverを定義する。

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

Note

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

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

Note

上記の例ではJSPを使ってHTMLを生成しているが、VelocityやFreeMarkerなど他のテンプレートエンジンを使用してHTMLを生成する場合でも、ハンドラメソッドの返り値は "sample/hello" のままでよい。 使用するテンプレートエンジンでの差分は ViewResolver によって解決される。


4.4.1.5.2. ダウンロードデータを応答する

データベースなどに格納されているデータをダウンロードデータ("application/octet-stream"等 )として応答する場合、
レスポンスデータの生成(ダウンロード処理)を行うViewを作成し、処理を委譲することを推奨する。
ハンドラメソッドでは、ダウンロード対象となるデータを Modelに追加し、ダウンロード処理を行うViewのView名を返却する。
View名からViewを解決する方法としては、個別のViewResolverを作成する方法もあるが、ここではSpring Frameworkから提供されているBeanNameViewResolverを使用する。
ダウンロード処理の詳細については、 ファイルダウンロード を参照されたい。
  • spring-mvc.xml

<bean>要素を使用する場合の定義例

<!-- (1) -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
    <property name="order" value="0"/> <!-- (2) -->
</bean>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
    <property name="order" value="1" />
</bean>

Spring Framework 4.1から追加された<mvc:view-resolvers>要素を使用する場合の定義例

<mvc:view-resolvers>
    <mvc:bean-name /> <!-- (3) -->
    <mvc:jsp prefix="/WEB-INF/views/" />
</mvc:view-resolvers>
  • SampleController.java
@RequestMapping("report")
public String report() {
    // omitted
    return "sample/report"; // (4)
}
  • XxxExcelView.java
@Component("sample/report") // (5)
public class XxxExcelView extends AbstractExcelView { // (6)
    @Override
    protected void buildExcelDocument(Map<String, Object> model,
            HSSFWorkbook workbook, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        HSSFSheet sheet;
        HSSFCell 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)

BeanNameViewResolverを定義する。

BeanNameViewResolverは、返却されたView名に一致するBeanをアプリケーションコンテキストから探してViewを解決するクラスとなっている。

(2)
JSP用のInternalViewResolverTilesViewResolverと併用する場合は、これらのViewResolverより、高い優先度を指定する事を推奨する。 上記例では、 "0" を指定することで、InternalViewResolverより先にBeanNameViewResolverによるView解決が行われる。
(3)

Spring Framework 4.1から追加された<mvc:bean-name>要素を使用して、BeanNameViewResolverを定義する。

<mvc:view-resolvers>要素を使用してViewResolverを定義する場合は、子要素に指定するViewResolverの定義順が優先順位となる。 上記例では、JSP用のInternalViewResolverを定義するための要素(<mvc:jsp>)より上に定義することで、JSP用のInternalViewResolverより先にBeanNameViewResolverによるView解決が行われる。

Note

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

(4)
ハンドラメソッドの返り値として "sample/report" というView名を返却した場合、 (5)でBean登録されたViewインスタンスによって生成されたデータがダウンロードデータとして応答される。
(5)

コンポーネントの名前にView名を指定して、ViewオブジェクトをBeanとして登録する。

上記例では、 "sample/report" というbean名(View名)で x.y.z.app.views.XxxExcelView のインスタンスがBean登録される。

(6)

Viewの実装例。

上記例では、 org.springframework.web.servlet.view.document.AbstractExcelView を継承し、Excelデータを生成するViewクラスの実装となる。



4.4.1.6. 処理の実装

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

Note

Controllerは、基本的には画面遷移の決定などの処理のルーティングとModelの設定のみ実装することに徹し、可能な限りシンプルな状態に保つこと。 この方針で統一することにより、Controllerで実装すべき処理が明確になり、開発規模が大きくなった場合でもControllerのメンテナンス性を保つことができる。


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


4.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が実行され、相関チェックを行うことが出来る。

4.4.1.6.2. 業務処理の呼び出し

業務処理が実装されているServiceをInjectし、InjectしたServiceのメソッドを呼び出すことで業務処理を実行する。

@Inject
SampleService sampleService; // (1)

@RequestMapping("hello")
public String hello(Model model){
    String message = sampleService.hello(); // (2)
    model.addAttribute("message", message);
    return "sample/hello";
}
項番 説明
(1)
業務処理が実装されている Service をInjectする。
(2)
Injectした Service のメソッドを呼び出し、業務処理を実行する。

4.4.1.6.3. ドメインオブジェクトへの値反映

本ガイドラインでは、HTML formから送信されたデータは直接ドメインオブジェクトにバインドするのではなく、フォームオブジェクトにバインドする方法を推奨している。
そのため、ControllerではServiceのメソッドに渡すドメインオブジェクトにフォームオブジェクトの値を反映する処理を行う必要がある。
@RequestMapping("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());
    // ...
    // and more ...
    // ...
    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)

@RequestMapping("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マッピング(Dozer) を参照されたい。


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

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



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

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

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

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


4.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
}

Tip

Spring Frameworkから提供されている型変換を行う仕組みについて

Spring Frameworkは、以下の3つの仕組みを使って型変換を行っており、基本的な型への変換は標準でサポートされている。各変換機能の詳細については、リンク先のページを参照されたい。

Warning

フォームオブジェクトには画面に表示のみ行う項目は保持せず、HTML formの項目のみ保持することを推奨する。 フォームオブジェクトに画面表示のみ行う項目の値を設定した場合、フォームオブジェクトをHTTPセッションオブジェクトに格納する際にメモリを多く消費する事になり、メモリ枯渇の原因になる可能性がある。 画面表示のみの項目は、Entityなどのドメイン層のオブジェクトをリクエストスコープに追加(Model.addAttribute)することでHTML(JSP)にデータを渡すことを推奨する。


4.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を参照されたい。

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

Warning

@DateTimeFormat の pattern でフォーマットを指定し、プロパティとして JSR-310 Date and Time APIが提供するjava.time.LocalDate を使用した場合、STRICTにチェックがされない ("20150229" を変換した場合、本来は型ミスマッチエラーとなるはずが、2015年2月28日 がバインドされる)。 Spring Framework 4.3で仕様が改善されて発生しなくなるが、TERASOLUNA Server Framework for Java (5.x)では Spring Framework 4.2 を使用しているので影響を受ける。 本事象の詳細は「@DateTimeFormat’s JSR-310 formatter is not strict in case of pattern」を参照されたい。


4.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" という属性名のフォームオブジェクトに対するバインド処理が行われる前にメソッドが呼び出される。

4.4.2.1.4. 入力チェック用のアノテーションの指定

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


4.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に追加する方法にすること。


4.4.2.3. HTML formへのバインディング方法

Modelに追加されたフォームオブジェクトは<form:xxx>タグを用いて、HTML(JSP)のformにバインドすることができる。
<form:xxx>タグの詳細は、 Using 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属性には、フォームオブジェクトのプロパティ名を指定する。

4.4.2.4. リクエストパラメータのバインディング方法

HTML formから送信されたリクエストパラメータは、フォームオブジェクトにバインドし、Controllerのハンドラメソッドの引数に渡すことができる。

@RequestMapping("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;
}

@RequestMapping("hello")
public String hello(
        @ModelAttribute("xxx") @Validated SampleForm form, // (2)
        BindingResult result,
        Model model) {
    // ...
}
項番 説明
(1)
フォームオブジェクトにリクエストパラメータが反映された状態で、Controllerのハンドラメソッドの引数に渡される。
(2)
ModelAttributeメソッドにて属性名を指定した場合、@ModelAttribute("xxx")といった感じで、フォームオブジェクトの属性名を明示的に指定する必要がある。

Warning

ModelAttributeメソッドで指定した属性名とメソッドの引数で指定した属性名が異なる場合、ModelAttributeメソッドで生成したインスタンスとは別のインスタンスが生成されるので注意が必要。 ハンドラメソッドで属性名の指定を省略した場合、クラス名の先頭を小文字にした値が属性名として扱われる。


4.4.2.4.1. バインディング結果の判定

HTML formから送信されたリクエストパラメータをフォームオブジェクトにバインドする際に発生したエラー(入力チェックエラーも含む)は、 org.springframework.validation.BindingResultに格納される。

@RequestMapping("hello")
public String hello(
        @Validated SampleForm form,
        BindingResult result, // (1)
        Model model) {
    if (result.hasErrors()) { // (2)
        return "sample/input";
    }
    // ...
}
項番 説明
(1)
フォームオブジェクトの直後にBindingResultを宣言すると、フォームオブジェクトへのバインド時のエラーと入力チェックエラーを参照することができる。
(2)
BindingResult.hasErrors()を呼び出すことで、フォームオブジェクトの入力値のエラー有無を判定することができる。

フィールドエラーの有無、グローバルエラー(相関チェックエラーなどのクラスレベルのエラー)の有無を個別に判定することもできるので、要件に応じて使い分けること。

項番 メソッド 説明
hasGlobalErrors() グローバルエラーの有無を判定するメソッド
hasFieldErrors() フィールドエラーの有無を判定するメソッド
hasFieldErrors(String field) 指定したフィールドのエラー有無を判定するメソッド

4.4.3. Viewの実装

Viewは以下の役割を担う。

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

4.4.3.1. JSPの実装

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

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

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

項番 JSPタグライブラリ名 ドキュメント
Spring’s form tag library
Spring’s tag library
JSTL
Common library’s tags & el functions

Warning

terasoluna-gfw-web 1.0.0.RELEASEを使用している場合は、Spring’s form tag libraryから提供されている<form:form>タグを使う際は、必ずaction属性を指定すること。

terasoluna-gfw-web 1.0.0.RELEASEが依存しているSpring MVC(3.2.4.RELEASE)では、<form:form>タグのaction属性を省略した場合、XSS(Cross-site scripting)の脆弱性が存在する。 脆弱性に関する情報については、National Vulnerability Database (NVD)のCVE-2014-1904を参照されたい。

尚、terasoluna-gfw-web 1.0.1.RELEASE以上では、XSS対策が行われているSpring MVC(3.2.10.RELEASE以上)に依存しているため、本脆弱性は存在しない。


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

ディレクティブの詳細は、 JavaServer Pages Specification(Version2.2)の “JSP.1.10 Directives” を参照されたい。

Note

<jsp-property-group>要素の詳細は、 JavaServer Pages Specification(Version2.2)の “JSP.3.3 JSP Property Groups” を参照されたい。


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

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

EL式を使用して表示する。

  • SampleController.java
@RequestMapping("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エスケープも行われる。

Note

<c:out> の詳細は、JavaServer Pages Standard Tag Library(Version 1.2)の “CHAPTER 4 General-Purpose Actions” を参照されたい。


4.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> の詳細は、JavaServer Pages Standard Tag Library(Version 1.2)の “CHAPTER 9 Formatting Actions” を参照されたい。


4.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:formatDate> の詳細は、JavaServer Pages Standard Tag Library(Version 1.2)の “CHAPTER 9 Formatting Actions” を参照されたい。

Note

日時オブジェクトの型として、Joda Timeから提供されている org.joda.time.DateTime などを利用する場合は、Jada Timeから提供されているJSPタグライブラリを使用すること。 Joda Timeの詳細は、 日付操作(Joda Time) を参照されたい。


4.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.RequestMapping;

@RequestMapping("hello")
@Controller
public class HelloController {

    // (1)
    @RequestMapping({"", "/"})
    public String hello() {
        return "hello/home";
    }

}
項番 説明
(1)
このメソッドに割り当てられるリクエストURLは、”{コンテキストパス}/hello"となる。

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

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

<form action="${pageContext.request.contextPath}/hello"> <!-- (2) -->
    <!-- ... -->
</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) -->
    <!-- ... -->
</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 Reference Documentation(Building URIs to Controllers and methods from views)」を参照されたい。

Note

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

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

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

Controllerのメソッドに割り当てられたリクエストマッピング名を確認したい場合は、 logback.xmlに以下の設定を追加すればよい。

<logger name="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
    <level value="trace" />
</logger>

上記設定を行った後に再起動すると、以下のようなログが出力されるようになる。

date:2014-12-09 18:34:29        thread:RMI TCP Connection(2)-127.0.0.1  X-Track:        level:TRACE     logger:o.s.w.s.m.m.a.RequestMappingHandlerMapping       message:Mapping name=HC#hello

4.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>タグの詳細は、 Using Spring’s form tag libraryを参照されたい。


4.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属性に、エラー表示したいプロパティのプロパティ名を指定する。

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

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

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

4.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 インタフェースのキー値として、セレクトボックスで選択した値を指定することで、コード名を表示することができる。

4.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属性に指定した値は、プロパティ値が取得できなかった場合に表示される。


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

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

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

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

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

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

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

Note

詳細は、 JavaServer Pages Standard Tag Library(Version 1.2)の “CHAPTER 5 Conditional Actions” を参照されたい。


4.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以外の属性については、 javax.servlet.jsp.jstl.core.LoopTagStatusJavaDocを参照されたい。
(3)
<c:forEach> タグのvar属性で指定した変数に格納されているオブジェクトから値を取得している。

Note

詳細は、 JavaServer Pages Standard Tag Library(Version 1.2)の “CHAPTER 6 Iterator Actions” を参照されたい。


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

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

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


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

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

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



4.4.3.2. JavaScriptの実装

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

Todo

TBD

次版以降で詳細を記載する予定である。


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

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

Note

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


4.4.4. 共通処理の実装


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

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


4.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> タグで指定した値 )にすること。

4.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 extends HandlerInterceptorAdapter { // (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>
    <!-- ... -->
    <mvc:interceptor>
        <mvc:mapping path="/**" /> <!-- (2) -->
        <mvc:exclude-mapping path="/resources/**" /> <!-- (3) -->
        <mvc:exclude-mapping path="/**/*.html" />
        <bean class="x.y.z.SuccessLoggingInterceptor" /> <!-- (4) -->
    </mvc:interceptor>
    <!-- ... -->
</mvc:interceptors>
項番 説明
(1)
サンプルではSpring Frameworkから提供されている org.springframework.web.servlet.handler.HandlerInterceptorAdapter の子クラスとしてHandlerInterceptorを作成している。 HandlerInterceptorAdapterHandlerInterceptor インタフェースの空実装を提供しているため、子クラスで不要なメソッドの実装をしないで済む。
(2)
作成したHandlerInterceptorを適用するパスのパターンを指定する。
(3)
作成したHandlerInterceptorを適用しないパスのパターンを指定する。
(4)
作成したHandlerInterceptorを spring-mvc.xml<mvc:interceptors> タグ内に追加する。

4.4.4.2. Controllerの共通処理の実装

ここでいう共通処理とは、すべてのControllerで共通的に実装する必要がある処理のことを指す。

4.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;

    // ....

}
  • 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
@RequestMapping(value = "home")
public String home(CommonParameters commonParams) { // (5)
    logger.debug("param1 : {}",commonParams.getParam1());
    logger.debug("param2 : {}",commonParams.getParam2());
    logger.debug("param3 : {}",commonParams.getParam3());
    // ...
    return "sample/home";

}
  • spring-mvc.xml
<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <!-- ... -->
        <bean class="x.y.z.CommonParametersMethodArgumentResolver" /> <!-- (6) -->
        <!-- ... -->
    </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に変換してから渡す方法が有効的である。 ここでいうパラメータとは、リクエストパラメータに限らない。


4.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(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface LoginFormModelAttribute {}
    // ...
}
@LoginFormModelAttribute
@Controller
public class WelcomeController {
    // ...
}
@LoginFormModelAttribute
@Controller
public class LoginController {
    // ...
}

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

(2)
assignableTypes

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

指定したクラス又はインタフェースに割り当て可能(キャスト可能)なControllerに対して共通処理が適用される。 本属性を使用する場合は、共通処理を適用するControllerであることを示すためのマーカーインタフェースを属性値に指定するスタイルを採用することを推奨する。 このスタイルを採用した場合、Controller側では、適用したい共通処理用のマーカーインタフェースを実装するだけでよい。 以下の指定例を示す。

@ControllerAdvice(assignableTypes = ISODateInitBinder.ISODateApplicable.class)
public class ISODateInitBinder {
    public static interface ISODateApplicable {}
    // ...
}
@Controller
public class SampleController implements ISODateApplicable {
    // ...
}

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

(3)
basePackageClasses

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

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

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

  • @ControllerAdviceを付与したクラス
  • パッケージを識別するためのマーカーインタフェース

を属性値に指定するスタイルを採用することを推奨する。 以下に指定例を示す。

package com.example.app

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

@Controller
public class SampleController {
    // ...
}

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

package com.example.app.common

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

public interface AppPackage {
}

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

(4)
basePackages

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

指定したパッケージ配下のControllerに対して共通処理が適用される。 以下に指定例を示す。

@ControllerAdvice(basePackages = "com.example.app")
public class AppGlobalExceptionHandler {
    // ...
}
(5)
value

basePackagesへのエイリアス。

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

@ControllerAdvice("com.example.app")
public class AppGlobalExceptionHandler {
    // ...
}

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
@RequestMapping(value = "home")
public String home(@ModelAttribute CommonParameters commonParams) { // (2)
    logger.debug("param1 : {}",commonParams.getParam1());
    logger.debug("param2 : {}",commonParams.getParam2());
    logger.debug("param3 : {}",commonParams.getParam3());
    // ...
    return "sample/home";
}
項番 説明
(1)
@ModelAttributeメソッドを実装する。全てのControllerに対して@ModelAttributeメソッドが適用される。
(2)
@ModelAttributeメソッドで生成されたオブジェクトが渡る。

4.4.5. 二重送信防止について

送信ボタンの複数回押下や完了画面の再読み込み(F5ボタンによる再読み込み)などで、 同じ処理が複数回実行されてしまう可能性があるため、二重送信を防止するための対策は必ず行うこと。

対策を行わない場合に発生する問題点や対策方法の詳細は、 二重送信防止 を参照されたい。


4.4.6. セッションの使用について

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

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