3.4. アプリケーション層の実装¶
目次
本節では、HTML formを使った画面遷移型のアプリケーションにおけるアプリケーション層の実装について説明する。
アプリケーション層の実装は、以下の3つにわかれる。
- Controllerは、リクエストの受付、業務処理の呼び出し、モデルの更新、Viewの決定といった処理を行い、リクエストを受けてからの一連の処理フローを制御する。アプリケーション層の実装において、もっとも重要な実装となる。
- フォームオブジェクトは、HTML formとアプリケーションの間での値の受け渡しを行う。
- View(JSP)は、モデル(フォームオブジェクトやドメインオブジェクトなど)からデータを取得し、画面(HTML)を生成する。
3.4.1. Controllerの実装¶
- リクエストを受け取るためのメソッドを提供する。
@RequestMapping
アノテーションもしくは@RequestMapping
合成アノテーションが付与されたメソッドを実装することで、リクエストを受け取ることができる。 - リクエストパラメータの入力チェックを行う。入力チェックが必要なリクエストを受け取るメソッドでは、
@Validated
アノテーションをフォームオブジェクトの引数に指定することで、リクエストパラメータの入力チェックを行うことができる。単項目チェックはBean Validation、相関チェックはSpring Validator又はBean Validationでチェックを行う。 - 業務処理の呼び出しを行う。Controllerでは業務処理の実装は行わず、Serviceのメソッドに処理を委譲する。
- 業務処理の処理結果をModelに反映する。Serviceのメソッドから返却されたドメインオブジェクトを
Model
に反映することで、Viewから処理結果を参照できるようにする。 - 処理結果に対応するView名を返却する。Controllerでは処理結果に対する描画処理を実装せず、描画処理はJSP等のViewで実装する。Controllerでは描画処理が実装されているViewのView名の返却のみ行う。View名に対応するViewの解決は、Spring Frameworkより提供されている
ViewResolver
によって行われ、処理結果に対応するView(JSPなど)が呼び出される仕組みになっている。
Note
Controllerでは、業務処理の呼び出し、処理結果のModel
への反映、遷移先(View名)の決定などのルーティング処理の実装に徹することを推奨する。
Controllerの実装について、以下4つの点に着目して説明する。
3.4.1.1. Controllerクラスの作成方法¶
org.springframework.web.servlet.mvc.Controller
インタフェースを実装する方法 (Interface-based Controller)もあるが、Spring3以降はDeprecatedになっているため、原則使用しない。@Controller public class SampleController { // omitted }
3.4.1.2. リクエストとハンドラメソッドのマッピング方法¶
@RequestMapping
アノテーションを付与する。@RequestMapping(value = "hello", RequestMethod.GET) public String hello() { // omitted }
@RequestMapping
の合成アノテーションである@GetMapping
や@PostMapping
が追加された。@GetMapping(value = "hello") public String hello() { // omitted }
@GetMapping
や@PostMapping
を使用すると、シンプルにマッピングを定義することができ、意図しないHTTPメソッドのマッピング防止とソースコードの可読性向上が期待できる。リクエストとハンドラメソッドをマッピングするためのルールは、@RequestMapping
アノテーション、@RequestMapping
合成アノテーションの属性に指定する。
項番 属性名 説明
value マッピング対象にするリクエストパスを指定する(複数可)。
method マッピング対象にするHTTPメソッド(RequestMethod
型)を指定する(複数可)。GET/POSTについてはHTML form向けのリクエストをマッピングする際にも使用するが、それ以外のHTTPメソッド(PUT/DELETEなど)はREST API向けのリクエストをマッピングする際に使用する。本ガイドラインではHTTPメソッドの指定はこの属性を使用せず、@GetMapping
/@PostMapping
/@PutMapping
/@DeleteMapping
などの@RequestMapping
合成アノテーションを使用することを推奨する。
params マッピング対象にするリクエストパラメータを指定する(複数可)。主にHTML form向けのリクエストをマッピングする際に使用する。このマッピング方法を使用すると、HTML form上に複数のボタンが存在する場合のマッピングを簡単に実現する事ができる。
headers マッピング対象とするリクエストヘッダを指定する(複数可)。主にREST APIやAjax向けのリクエストをマッピングする際に使用する。
consumes リクエストのContent-Typeヘッダを使ってマッピングすることが出来る。マッピング対象とするメディアタイプを指定する(複数可)。主にREST APIやAjax向けのリクエストをマッピングする際に使用する。
produces リクエストのAcceptヘッダを使ってマッピングすることが出来る。マッピング対象とするメディアタイプを指定する(複数可)。主にREST APIやAjax向けのリクエストをマッピングする際に使用する。Note
マッピングの組み合わせについて
複数の属性を組み合わせることで複雑なマッピングを行うことも可能だが、保守性を考慮し、可能な限りシンプルな定義になるようにマッピングの設計を行うこと。2つの属性の組み合わせ(value属性と別の属性1つ)を目安にすることを推奨する。
@Controller // (1) @RequestMapping("sample") // (2) public class SampleController { // omitted }
項番 説明 (1)@Controller
アノテーションを付加することでAnnotation-basedなコントローラークラスとして認識され、component scanの対象となる。 (2)クラスレベルで
@RequestMapping("sample")
アノテーションを付けることでこのクラス内のハンドラメソッドがsample配下のURLにマッピングされる。Note
@RequestMapping
の値(value属性)を省略した場合、サーブレットルート(”/
” )のURLにマッピングされる。
3.4.1.2.1. HTTPメソッドでマッピング¶
下記の定義の場合、sample
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。
@GetMapping public String hello() {
sample
というURLにPOSTメソッドでアクセスすると、helloメソッドが実行される。@PostMapping public String hello() {
Note
1つのハンドラメソッドに対して複数のHTTPメソッドを指定したい場合
1つのハンドラメソッドに対して@GetMapping
や@PostMapping
を同時に使用することはできない。この場合は@RequestMapping
を使用し、method属性に複数の値を指定することで実現できる。
下記の定義の場合、sample/hello
というURLにGET又はPOSTメソッドでアクセスすると、helloメソッドが実行される。
@RequestMapping(value = "hello", method = {RequestMethod.GET, RequestMethod.POST}) public String hello() {
ただし、HTTPメソッドを複数指定することにより機能障害やセキュリティホールに繋がる可能性がある。
ハンドラメソッドの目的に応じて使用するHTTPリクエストメソッドを1つに絞り、@GetMapping
や@PostMapping
を使用することを推奨する。
3.4.1.2.2. リクエストパスでマッピング¶
下記の定義の場合、sample/hello
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。
@GetMapping(value = "hello") public String hello() {
sample/hello
又はsample/bonjour
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。@GetMapping(value = {"hello", "bonjour"}) public String hello() {
指定するリクエストパスは、具体的な値ではなくパターンを指定することも可能である。パターン指定の詳細は、Spring Framework Documentation -URI patterns-を参照されたい。
3.4.1.2.3. リクエストパラメータでマッピング¶
sample/hello?form
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。@GetMapping(value = "hello", params = "form") public String hello() {
sample/hello?form&formType=foo
というURLにGETメソッドでアクセスすると、helloメソッドが実行される。@GetMapping(value = "hello", params = {"form", "formType=foo"}) public String hello(@RequestParam("formType") String formType) {
サポートされている指定形式は以下の通り。
項番 形式 説明
paramName 指定したparameNameのリクエストパラメータが存在する場合にマッピングされる。
!paramName 指定したparameNameのリクエストパラメータが存在しない場合にマッピングされる。
paramName=paramValue 指定したparameNameの値がparamValueの場合にマッピングされる。
paramName!=paramValue 指定したparameNameの値がparamValueでない場合にマッピングされる。
3.4.1.3. リクエストとハンドラメソッドのマッピング方針¶
以下の方針でマッピングを行うことを推奨する。
- 業務や機能といった意味のある単位で、リクエストのURLをグループ化する。URLのグループ化とは、
@RequestMapping(value = "xxx")
をクラスレベルのアノテーションとして定義することを意味する。 - 処理内の画面フローで使用するリクエストのURLは、同じURLにする。同じURLとは
@RequestMapping(value = "xxx")
のvalue属性の値を同じ値にすることを意味する。処理内の画面フローで使用するハンドラメソッドの切り替えは、HTTPメソッドとHTTPパラメータによって行う。 - ハンドラメソッドには@RequestMappingではなく、@GetMappingや@PostMappingなどの@RequestMapping合成アノテーションを使用する意図しないHTTPメソッドのマッピング防止と可読性の向上のために
@RequestMapping
合成アノテーションの使用を推奨する。
Warning
Spring MVCでは @RequestMapping(value = "xxx")
のvalue属性によってリクエストがマッピングされる際、サーブレットパスとパス情報は区別されず、パス情報が存在する場合はパス情報、存在しない場合はサーブレットパスがマッピングに利用される。
そのため、サーブレットパスとパス情報に同一のパスを設定した場合、意図せぬパス(URL)がマッピングされる可能性がある。
具体的には、リクエストパスでマッピングのようにハンドラメソッドにマッピングするパスを「/sample/hello
」と定義した場合、web.xmlでサーブレットパスを同じ「/sample/hello/*
」と定義すると、本来マッピングしたい”/sample/hello/sample/hello”だけでなく、意図しない”/sample/hello”もマッピングされてしまう。
業務上、意図せぬパス(URL)でハンドラメソッドにアクセスできてしまう可能性があり、また、Spring MVCのリクエストマッピング(@RequestMapping
)ではサーブレット内のパスを指定するのに対し、Spring Security(Servlet Filter)の認可(<sec:intercept-url>
)ではWebアプリケーション内のパスを指定する。このため、意図しないパス(上記の場合、”/sample/hello”)への認可設定が漏れ、認可をバイパスされる脆弱性を作りこんでしまう恐れがある。
サーブレットパスとパス情報には異なる値を設定するようにされたい。
以下にベーシックな画面フローを行うサンプルアプリケーションを例にして、リクエストとハンドラメソッドの具体的なマッピング例を示す。
3.4.1.3.1. サンプルアプリケーションの概要¶
サンプルアプリケーションの機能概要は以下の通り。
- EntityのCRUD処理を行う機能を提供する。
- 以下の5つの処理を提供する。
項番 処理名 処理概要 Entity一覧取得 作成済みのEntityを全て取得し、一覧画面に表示する。 Entity新規作成 指定した内容で新たにEntityを作成する。処理内には、画面フロー(フォーム画面、確認画面、完了画面)が存在する。 Entity参照 指定されたIDのEntityを取得し、詳細画面に表示する。 Entity更新 指定されたIDのEntityを更新する。処理内には、画面フロー(フォーム画面、確認画面、完了画面)が存在する。 Entity削除 指定されたIDのEntityを削除する。 - 機能全体の画面フローは以下の通り。画面フロー図には記載していないが、入力チェックエラーが発生した場合はフォーム画面を再描画するものとする。
3.4.1.3.2. リクエストURL¶
必要となるリクエストのURLの設計を行う。
- 機能内で必要となるリクエストのリクエストURLをグループ化する。ここではAbcというEntityのCRUD操作を行う機能となるので、
/abc/
から始まるURLとする。 処理毎にリクエストURLを設ける。
項番 処理名 処理毎のURL(パターン) Entity一覧取得 /abc/list Entity新規作成 /abc/create Entity参照 /abc/{id} Entity更新 /abc/{id}/update Entity削除 /abc/{id}/delete Note
Entity参照、Entity更新、Entity削除処理のURL内に指定している
{id}
は、URI patternsと呼ばれ、任意の値を指定する事ができる。サンプルアプリケーションでは、操作するEntityのIDを指定する。
画面フロー図に各処理に割り振られたURLをマッピングすると以下のようになる。
3.4.1.3.3. リクエストとハンドラメソッドのマッピング¶
項番 処理名 URL リクエスト名 HTTPメソッド HTTPパラメータ ハンドラメソッド
Entity一覧取得 /abc/list 一覧表示 GET - list
Entity新規作成 /abc/create フォーム表示 GET form createForm
入力内容確認表示 POST confirm createConfirm
フォーム再表示 POST redo createRedo
新規作成 POST - create
新規作成完了表示 GET complete createComplete
Entity参照 /abc/{id} 詳細表示 GET - read
Entity更新 /abc/{id}/update フォーム表示 GET form updateForm
入力内容確認表示 POST confirm updateConfirm
フォーム再表示 POST redo updateRedo
更新 POST - update
更新完了表示 GET complete updateComplete
Entity削除 /abc/{id}/delete 削除 POST - delete
削除完了表示 GET complete deleteComplete
/abc/create
で、HTTPメソッドとHTTPパラメータの組み合わせでハンドラメソッドを切り替えている点に注目すること。@RequestMapping
、@GetMapping
、@PostMapping
の書き方に注目すること。3.4.1.3.4. フォーム表示の実装¶
フォーム表示する場合は、HTTPパラメータとしてform
を指定させる。
@GetMapping(value = "create", params = "form") // (1) public String createForm(AbcForm form, Model model) { // omitted return "abc/createForm"; // (2) }
項番 説明 (1)@GetMapping
を使用し、params属性にform
を指定する。 (2)フォーム画面を描画するためのJSPの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メソッドが呼び出されフォーム画面が表示される。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(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を参照されたい。
以下に、入力内容確認の動作について説明する。
aa
を、Input2に”5
”を入力し、Confirmボタンを押下する。abc/create?confirm
というURIにPOSTメソッドでアクセスする。confirm
というHTTPパラメータがあるため、ControllerのcreateConfirmメソッドが呼び出され、入力内容確認画面が表示される。Confirmボタンを押下するとPOSTメソッドでHTTPパラメータが送信されるため、URIには現れていないが、HTTPパラメータとしてconfirm
が含まれている。
3.4.1.3.6. フォーム再表示の実装¶
フォームを再表示する場合は、HTTPパラメータにredoを指定させる。
@PostMapping(value = "create", params = "redo") // (1) public String createRedo(AbcForm form, Model model) { // omitted return "abc/createForm"; // (2) }
項番 説明 (1)@PostMapping
を使用し、params属性にredo
を指定する。 (2)入力内容確認画面を描画するためのJSPのView名を返却する。
以下に、フォーム再表示の動作について説明する。
abc/create?redo
というURIにPOSTメソッドでアクセスする。redo
というHTTPパラメータがあるため、ControllerのcreateRedoメソッドが呼び出され、フォーム画面が再表示される。Backボタンを押下するとPOSTメソッドでHTTPパラメータが送信されるため、URIには現れていないが、HTTPパラメータとしてredo
が含まれている。また、フォームの入力値をhidden項目として送信されるため、フォーム画面で入力値を復元することが出来る。
Note
戻るボタンの実現方法には、ボタンの属性に onclick="javascript:history.back()"
を設定する方法もある。両者では以下が異なり、要件に応じて選択する必要がある。
- ブラウザの戻るボタンを押した場合の挙動
- 戻るボタンがあるページに直接アクセスして戻るボタンを押した場合の挙動
- ブラウザの履歴
3.4.1.3.7. 新規作成の実装¶
@PostMapping(value = "create") // (1) public String create(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); // return "abc/createForm"; } // omitted return "redirect:/abc/create?complete"; // (2) }
項番 説明 (1)@PostMapping
を使用し、params属性は指定しない。 (2)PRGパターンとするため、新規作成完了表示リクエストにリダイレクトするためのURLをView名として返却する。 Note
“redirect:/xxx”を返却すると”/xxx”へリダイレクトさせることができる。
Warning
PRGパターンとすることで、ブラウザのF5ボタン押下時のリロードによる二重送信を防ぐ事はできるが、二重送信の対策としては十分ではない。二重送信の対策としては、共通部品として提供しているTransactionTokenCheckを行う必要がある。
TransactionTokenCheckの詳細については二重送信防止を参照されたい。
以下に、「新規作成」の動作について説明する。
abc/create
というURIにPOSTメソッドでアクセスする。/abc/create?complete
)へリダイレクトしているため、HTTPステータスが302になっている。3.4.1.3.8. 新規作成完了表示の実装¶
新規作成処理が完了した事を通知する場合は、HTTPパラメータにcomplete
を指定させる。
@GetMapping(value = "create", params = "complete") // (1) public String createComplete() { // omitted return "abc/createComplete"; // (2) }
項番 説明 (1)@GetMapping
を使用し、params属性にcomplete
を指定する。 (2)新規作成完了画面を描画するため、JSPのView名を返却する。
以下に、「新規作成完了表示」の動作について説明する。
/abc/create?complete
)にアクセスする。complete
というHTTPパラメータがあるため、ControllerのcreateCompleteメソッドが呼び出され、新規作成完了画面が表示される。3.4.1.3.9. HTML form上に複数のボタンを配置する場合の実装¶
下図のように、入力内容確認画面のフォームには、新規作成を行うCreateボタンと新規作成フォーム画面を再表示するBackボタンが存在する。
Backボタンを押下した場合、新規作成フォーム画面を再表示するためのリクエスト( /abc/create?redo
)を送信する必要があるため、
HTML form内に以下のコードが必要となる。
<input type="submit" name="redo" value="Back" /> <!-- (1) --> <input type="submit" value="Create" />
項番 説明 (1)上記のように、入力内容確認画面( abc/createConfirm.jsp
)のBackボタンにname="redo"
というパラメータを指定する。
Backボタン押下時の動作については、フォーム再表示の実装を参照されたい。
3.4.1.3.10. サンプルアプリケーションのControllerのソースコード¶
@Controller @RequestMapping("abc") public class AbcController { @ModelAttribute public AbcForm setUpAbcForm() { return new AbcForm(); } // Handling request of "GET /abc/create?form" @GetMapping(value = "create", params = "form") public String createForm(AbcForm form, Model model) { // omitted return "abc/createForm"; } // Handling request of "POST /abc/create?confirm" @PostMapping(value = "create", params = "confirm") public String createConfirm(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); } // omitted return "abc/createConfirm"; } // Handling request of "POST /abc/create?redo" @PostMapping(value = "create", params = "redo") public String createRedo(AbcForm form, Model model) { // omitted return "abc/createForm"; } // Handling request of "POST /abc/create" @PostMapping(value = "create") public String create(@Validated AbcForm form, BindingResult result, Model model) { if (result.hasErrors()) { return createRedo(form, model); } // omitted return "redirect:/abc/create?complete"; } // Handling request of "GET /abc/create?complete" @GetMapping(value = "create", params = "complete") public String createComplete() { // omitted return "abc/createComplete"; } }
3.4.1.4. ハンドラメソッドの引数について¶
ハンドラメソッドの引数は様々な値をとることができるが、基本的には次に挙げるものは原則として使用しないこと。
- ServletRequest
- HttpServletRequest
- org.springframework.web.context.request.WebRequest
- org.springframework.web.context.request.NativeWebRequest
- java.io.InputStream
- java.io.Reader
- java.io.OutputStream
- java.io.Writer
- java.util.Map
- org.springframework.ui.ModelMap
Note
HttpServletRequest
のgetAttribute/setAttributeやMap
のget/putのような汎用的なメソッドの利用を許可すると自由な値の受け渡しができてしまい、プロジェクトの規模が大きくなると保守性を著しく低下させる可能性がある。
同様の理由で、他で代替できる場合はHttpSession
を極力使用しないことを推奨する。
共通的なパラメータ(リクエストパラメータ)をJavaBeanに格納してControllerの引数に渡したい場合は、後述のHandlerMethodArgumentResolverの実装を使用することで実現できる。
以下に、引数の使用方法について、目的別に13例示す。
- 画面(View)にデータを渡す
- URLのパスから値を取得する
- リクエストパラメータを個別に取得する
- リクエストパラメータをまとめて取得する
- 入力チェックを行う
- リダイレクト先にデータを渡す
- リダイレクト先へリクエストパラメータを渡す
- リダイレクト先URLのパスに値を埋め込む
- Cookieから値を取得する
- Cookieに値を書き込む
- ページネーション情報を取得する
- アップロードファイルを取得する
- 画面に結果メッセージを表示する
3.4.1.4.1. 画面(View)にデータを渡す¶
画面(View)に表示するデータを渡したい場合は、org.springframework.ui.Model
(以降 Model
と呼ぶ) をハンドラメソッドの引数として受け取り、Model
オブジェクトに渡したいデータ(オブジェクト)を追加する。
SampleController.java
@GetMapping("hello") public String hello(Model model) { // (1) model.addAttribute("hello", "Hello World!"); // (2) model.addAttribute(new HelloBean("Bean Hello World!")); // (3) return "sample/hello"; // returns view name }
hello.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
Model
にaddAttribute
することで、HttpServletRequest
にsetAttribute
されるため、Spring MVCの管理下にないモジュール(例えばServletFilterなど)からも値を参照することが出来る。
3.4.1.4.2. URLのパスから値を取得する¶
@PathVariable
アノテーションを付与する。@PathVariable
アノテーションを使用してパスから値を取得する場合、 @GetMapping
アノテーションのvalue属性に取得したい部分を変数化しておく必要がある。@GetMapping("hello/{id}/{version}") // (1) public String hello( @PathVariable("id") String id, // (2) @PathVariable Integer version, // (3) Model model) { // do something return "sample/hello"; // returns view name }
項番 説明 (1)@GetMapping
アノテーションのvalue属性に、抜き出したい箇所をパス変数として指定する。パス変数は、「{変数名}」の形式で指定する。上記例では、id
とversion
という二つのパス変数を指定している。 (2)@PathVariable
アノテーションのvalue属性には、パス変数の変数名を指定する。上記例では、sample/hello/aaaa/1
というURLにアクセスした場合、引数idに文字列aaaa
が渡る。 (3)@PathVariable
アノテーションのvalue属性は省略可能で、省略した場合は引数名がリクエストパラメータ名となる。上記例では、sample/hello/aaaa/1
というURLにアクセスした場合、引数versionに数値 “1
” が渡る。ただしこの方法は、
-g
オプション(デバッグ情報を出力するモード)- Java8から追加された
-parameters
オプション(メソッド・パラメータにリフレクション用のメタデータを生成するモード)のどちらかを指定してコンパイルする必要がある。
Note
バインドする引数の型はString以外でも良い。型が合わない場合は
org.springframework.beans.TypeMismatchException
がスローされ、デフォルトの動作は400(Bad Request)が応答される。例えば、上記例で
sample/hello/aaaa/v1
というURLでアクセスした場合、v1
をIntegerに変換できないため、例外がスローされる。Warning
@PathVariable
アノテーションのvalue属性を省略する場合、デプロイするアプリケーションは-g
オプション又はJava8から追加された-parameters
オプションを指定してコンパイルする必要がある。これらのオプションを指定した場合、コンパイル後のクラスにはデバッグ時に必要となる情報や処理などが挿入されるため、メモリや処理性能に影響を与えることがあるので注意が必要である。
基本的には、value属性を明示的に指定する方法を推奨する。
Warning
Spring Framework 5.3.0より、パスの最後をパス変数にする場合、バインドされる値に拡張子が含まれるように変更された。
これはSpring MVCにおいてリクエストパスの拡張子によるパターンマッチングが非推奨となったことによる影響で、従来は拡張子がパス変数と別に扱われていたが、パス変数の一部として扱われるようになったためである。
これを回避するには以下の2種類の方法がある。
mvc:annotation-driven
の設定でsuffix-pattern
を有効にする(全体)<mvc:annotation-driven> <!-- ommitted --> <mvc:path-matching suffix-pattern="true" /> </mvc:annotation-driven>
@GetMapping
で拡張子無しと有りの両方のパスにマッピングする(個別)@GetMapping({ "hello/{id}/{version}", "hello/{id}/{version}.*" }) public String hello( @PathVariable("id") String id, @PathVariable Integer version, Model model) { // do something return "sample/hello"; // returns view name }なお、リクエストパスの拡張子によるパターンマッチングはブラウザから送信されるAcceptヘッダーを一貫して解釈することが困難だった古い時代の手法であり、拡張子ではなくAcceptヘッダーやURLのクエリパラメータでマッピングを切り分けることが、Springでは推奨されている。
詳細は Spring Framework Documentation -Suffix Match-を参照されたい。
3.4.1.4.3. リクエストパラメータを個別に取得する¶
リクエストパラメータを1つずつ取得したい場合は、引数に@RequestParam
アノテーションを付与する。
@GetMapping("bindRequestParams") public String bindRequestParams( @RequestParam("id") String id, // (1) @RequestParam String name, // (2) @RequestParam(value = "age", required = false) Integer age, // (3) @RequestParam(value = "genderCode", required = false, defaultValue = "unknown") String genderCode, // (4) Model model) { // do something return "sample/hello"; // returns view name }
項番 説明 (1)@RequestParam
アノテーションのvalue属性には、リクエストパラメータ名を指定する。上記例では、sample/hello?id=aaaa
というURLにアクセスした場合、引数idに文字列aaaa
が渡る。 (2)@RequestParam
アノテーションのvalue属性は省略可能で、省略した場合は引数名がリクエストパラメータ名となる。上記例では、sample/hello?name=bbbb&....
というURLにアクセスした場合、引数nameに文字列bbbb
が渡る。ただしこの方法は、
-g
オプション(デバッグ情報を出力するモード)- Java8から追加された
-parameters
オプション(メソッド・パラメータにリフレクション用のメタデータを生成するモード)のどちらかを指定してコンパイルする必要がある。
(3) デフォルトの動作では、指定したリクエストパラメータが存在しないとエラーとなる。リクエストパラメータが存在しないケースを許容する場合は、required属性をfalse
に指定する。上記例では、age
というリクエストパラメータがない状態でアクセスした場合、引数ageにnull
が渡る。 (4) 指定したリクエストパラメータが存在しない場合にデフォルト値を使用したい場合は、defaultValue属性にデフォルト値を指定する。上記例では、genderCode
というリクエストパラメータがない状態でアクセスした場合、引数genderCodeにunknown
が渡る。Note
必須パラメータを指定しないでアクセスした場合は、
org.springframework.web.bind.MissingServletRequestParameterException
がスローされ、デフォルトの動作は400(Bad Request)が応答される。ただし、defaultValue属性を指定している場合、例外はスローされず、defaultValue属性で指定した値が渡る。
Note
バインドする引数の型はString以外でも良い。型が合わない場合は
org.springframework.beans.TypeMismatchException
がスローされ、デフォルトの動作は400(Bad Request)が応答される。例えば、上記例で
sample/hello?age=aaaa&...
というURLでアクセスした場合、aaaa
をIntegerに変換できないため、例外がスローされる。
以下の条件に当てはまる場合は、次に説明するフォームオブジェクトにバインドすること。
- リクエストパラメータがHTML form内の項目である。
- リクエストパラメータはHTML form内の項目ではないが、リクエストパラメータに必須チェック以外の入力チェックを行う必要がある。
- リクエストパラメータの入力チェックエラーのエラー詳細をパラメータ毎に出力する必要がある。
- 3つ以上のリクエストパラメータをバインドする。(保守性、可読性の観点)
3.4.1.4.4. リクエストパラメータをまとめて取得する¶
以下は、@RequestParam
で個別にリクエストパラメータを受け取っていたハンドラメソッドを、フォームオブジェクトで受け取るように変更した場合の実装例である。
@RequestParam
を使って個別にリクエストパラメータを受け取っているハンドラメソッドは以下の通り。
@GetMapping("bindRequestParams") public String bindRequestParams( @RequestParam("id") String id, @RequestParam String name, @RequestParam(value = "age", required = false) Integer age, @RequestParam(value = "genderCode", required = false, defaultValue = "unknown") String genderCode, Model model) { // do something return "sample/hello"; // returns view name }
public class SampleForm implements Serializable{ private static final long serialVersionUID = 1477614498217715937L; private String id; private String name; private Integer age; private String genderCode; // omit setters and getters }Note
リクエストパラメータ名とフォームオブジェクトのプロパティ名は一致させる必要がある。
上記のフォームオブジェクトに対して id=aaa&name=bbbb&age=19&genderCode=men?tel=01234567
というパラメータが送信された場合、id
,name
,age
,genderCode
は名前が一致するプロパティに値が格納されるが、tel
は名前が一致するプロパティがないため、フォームオブジェクトに取り込まれない。
@RequestParam
を使って個別に受け取っていたリクエストパラメータをフォームオブジェクトとして受け取るようにする。
@GetMapping("bindRequestParams") public String bindRequestParams(@Validated SampleForm form, // (1) BindingResult result, Model model) { // do something return "sample/hello"; // returns view name }
項番 説明 (1)SampleForm
オブジェクトを引数として受け取る。Note
フォームオブジェクトを引数に用いた場合、
@RequestParam
の場合とは異なり、必須チェックは行われない。フォームオブジェクトを使用する場合は、次に説明する入力チェックを行うを行うこと。Warning
EntityなどDomainオブジェクトをそのままフォームオブジェクトとして使うこともできるが、実際には、WEBの画面上にしか存在しないパラメータ(確認用パスワードや、規約確認チェックボックス等)が存在する。
Domainオブジェクトにそのような画面項目に依存する項目を入れるべきではないので、Domainオブジェクトとは別にフォームオブジェクト用のクラスを作成することを推奨する。
リクエストパラメータからDomainオブジェクトを作成する場合は、一旦フォームオブジェクトにバインドしてからプロパティ値をDomainオブジェクトにコピーすること。
3.4.1.4.5. 入力チェックを行う¶
リクエストパラメータがバインドされているフォームオブジェクトに対して入力チェックを行う場合は、フォームオブジェクト引数に@Validated
アノテーションを付け、フォームオブジェクト引数の直後にorg.springframework.validation.BindingResult
(以降BindingResult
と呼ぶ) を引数に指定する。
入力チェックの詳細については、入力チェックを参照されたい。
フォームオブジェクトクラスのフィールドに入力チェックで必要となるアノテーションを付加する。
public class SampleForm implements Serializable { private static final long serialVersionUID = 1477614498217715937L; @NotNull @Size(min = 10, max = 10) private String id; @NotNull @Size(min = 1, max = 10) private String name; @Min(1) @Max(100) private Integer age; @Size(min = 1, max = 10) private Integer genderCode; // omit setters and getters }
@Validated
アノテーションを付与する。@Validated
アノテーションを付けた引数は、ハンドラメソッド実行前に入力チェックが行われ、チェック結果が直後のBindingResult
引数に格納される。BindingResult
に格納されている。@GetMapping("bindRequestParams") public String bindRequestParams(@Validated SampleForm form, // (1) BindingResult result, // (2) Model model) { if (result.hasErrors()) { // (3) return "sample/input"; // back to the input view } // do something return "sample/hello"; // returns view name }
項番 説明 (1)SampleForm
オブジェクトに@Validated
アノテーションを付与し、入力チェック対象のオブジェクトにする。 (2)入力チェック結果が格納される BindingResult
を引数に指定する。 (3)入力チェックエラーが存在するか判定する。エラーがある場合は、 true
が返却される。
3.4.1.4.6. リダイレクト先にデータを渡す¶
ハンドラメソッドを実行した後にリダイレクトする場合に、リダイレクト先で表示するデータを渡したい場合は、org.springframework.web.servlet.mvc.support.RedirectAttributes
(以降RedirectAttributes
と呼ぶ) をハンドラメソッドの引数として受け取り、
RedirectAttributes
オブジェクトに渡したいデータを追加する。
SampleController.java
@GetMapping("hello") public String hello(RedirectAttributes redirectAttrs) { // (1) redirectAttrs.addFlashAttribute("hello", "Hello World!"); // (2) redirectAttrs.addFlashAttribute(new HelloBean("Bean Hello World!")); // (3) return "redirect:/sample/hello?complete"; // (4) } @GetMapping(value = "hello", params = "complete") public String helloComplete() { return "sample/complete"; // (5) }
complete.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
Model
のaddAttribute
メソッドに非常によく似ているが、データの生存期間が異なる。
RedirectAttributes
のaddFlashAttribute
ではflash scopeというスコープにデータが格納され、リダイレクト後の1リクエスト(PRGパターンのG)でのみ追加したデータを参照することができる。2回目以降のリクエストの時にはデータは消えている。
3.4.1.4.7. リダイレクト先へリクエストパラメータを渡す¶
リダイレクト先へ動的にリクエストパラメータを設定したい場合は、引数のRedirectAttributes
オブジェクトに渡したい値を追加する。
@GetMapping("hello") public String hello(RedirectAttributes redirectAttrs) { String id = "aaaa"; redirectAttrs.addAttribute("id", id); // (1) // must not return "redirect:/sample/hello?complete&id=" + id; return "redirect:/sample/hello?complete"; }
項番 説明 (1) 属性名にリクエストパラメータ名、属性値にリクエストパラメータの値を指定して、RedirectAttributes
オブジェクトのaddAttribute
メソッドを呼び出す。上記例では、/sample/hello?complete&id=aaaa
にリダイレクトされる。
Warning
上記例ではコメント化しているが、return "redirect:/sample/hello?complete&id=" + id;
と結果は同じになる。ただし、 RedirectAttributes
オブジェクトのaddAttribute
メソッドを用いるとURIエンコーディングも行われるので、動的に埋め込むリクエストパラメータについては、返り値のリダイレクトURLとして組み立てるのではなく、必ずaddAttributeメソッドを使用してリクエストパラメータに設定すること。
動的に埋め込まないリクエストパラメータ(上記例だと”complete”)については、返り値のリダイレクトURLに直接指定してよい。
3.4.1.4.8. リダイレクト先URLのパスに値を埋め込む¶
リダイレクト先URLのパスに動的に値を埋め込みたい場合は、リクエストパラメータの設定と同様引数のRedirectAttributes
オブジェクトに埋め込みたい値を追加する。
@GetMapping("hello") public String hello(RedirectAttributes redirectAttrs) { String id = "aaaa"; redirectAttrs.addAttribute("id", id); // (1) // must not return "redirect:/sample/hello/" + id + "?complete"; return "redirect:/sample/hello/{id}?complete"; // (2) }
項番 説明 (1) 属性名とパスに埋め込みたい値を指定して、RedirectAttributes
オブジェクトのaddAttribute
メソッドを呼び出す。 (2) リダイレクトURLの埋め込みたい箇所に「{属性名}」のパス変数を指定する。上記例では、/sample/hello/aaaa?complete
にリダイレクトされる。
Warning
上記例ではコメント化しているが、"redirect:/sample/hello/" + id + "?complete";
と結果は同じになる。ただし、 RedirectAttributes
オブジェクトのaddAttribute
メソッドを用いるとURLエンコーディングも行われるので、動的に埋め込むパス値については、返り値のリダイレクトURLとして記述せずに、必ずaddAttributeメソッドを使用し、パス変数を使って埋め込むこと。
3.4.1.4.9. Cookieから値を取得する¶
Cookieから取得したい場合は、引数に@CookieValue
アノテーションを付与する。
@GetMapping("readCookie") public String readCookie(@CookieValue("JSESSIONID") String sessionId, Model model) { // (1) // do something return "sample/readCookie"; // returns view name }
項番 説明 (1)@CookieValue
アノテーションのvalue属性には、Cookie名を指定する。上記例では、Cookieから”JSESSIONID”というCookie名の値が引数sessionIdに渡る。
Note
@RequestParam
同様、required属性、defaultValue属性があり、引数の型にはString型以外の指定も可能である。
詳細は、リクエストパラメータを個別に取得するを参照されたい。
3.4.1.4.10. Cookieに値を書き込む¶
HttpServletResponse
オブジェクトのaddCookie
メソッドを直接呼び出してCookieに追加する。@GetMapping("writeCookie") public String writeCookie(Model model, HttpServletResponse response) { // (1) Cookie cookie = new Cookie("foo", "HelloWorld!"); response.addCookie(cookie); // (2) // do something return "sample/writeCookie"; }
項番 説明 (1)Cookieを書き込むために、 HttpServletResponse
オブジェクトを引数に指定する。 (2)Cookie
オブジェクトを生成し、HttpServletResponse
オブジェクトに追加する。上記例では、foo
というCookie名でHelloWorld!
という値を設定している。
Tip
HttpServletResponse
を引数として受け取ることに変わりはないが、Cookieに値を書き込むためのクラスとして、Spring Frameworkからorg.springframework.web.util.CookieGenerator
というクラスが提供されている。必要に応じて使用すること。
Note
HTTP Cookieの処理を規定するRFC 6265では、Cookieの名前や値に一部使用できない文字があることに注意されたい。
RFC 6265(HTTP State Management Mechanism)の4.1 SetCookieのSyntaxを参照されたい。
3.4.1.4.11. ページネーション情報を取得する¶
org.springframework.data.domain.Pageable
(以降Pageable
と呼ぶ) オブジェクトをハンドラメソッドの引数に取ることで、ページネーション情報(ページ数、取得件数)を容易に扱うことができる。詳細についてはページネーションを参照されたい。
3.4.1.4.12. アップロードファイルを取得する¶
アップロードされたファイルを取得する方法は大きく2つある。
- フォームオブジェクトに
MultipartFile
のプロパティを用意する。 @RequestParam
アノテーションを付与してorg.springframework.web.multipart.MultipartFile
をハンドラメソッドの引数とする。
詳細については ファイルアップロード を参照されたい。
3.4.1.4.13. 画面に結果メッセージを表示する¶
Model
オブジェクト又はRedirectAttributes
オブジェクトをハンドラメソッドの引数として受け取り、ResultMessages
オブジェクトを追加することで処理の結果メッセージを表示できる。
詳細については メッセージ管理 を参照されたい。
3.4.1.5. ハンドラメソッドの返り値について¶
ハンドラメソッドの返り値についても様々な値をとることができるが、基本的には次に挙げるもののみを使用すること。
- String(View名)
以下に、目的別に返り値の使用方法について説明する。
3.4.1.5.1. HTMLを応答する¶
ViewResolver
は、基本的にはUrlBasedViewResolver
の継承クラス(InternalResourceViewResolver
や TilesViewResolver
等)となる。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名を返却する。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. 処理の実装¶
Note
Controllerは、基本的には画面遷移の決定などの処理のルーティングとModel
の設定のみ実装することに徹し、可能な限りシンプルな状態に保つこと。
この方針で統一することにより、Controllerで実装すべき処理が明確になり、開発規模が大きくなった場合でもControllerのメンテナンス性を保つことができる。
Controllerで実装すべき処理を以下に4つ示す。
3.4.1.6.1. 入力値の相関チェック¶
org.springframework.validation.Validator
インタフェースを実装したValidationクラス、もしくは、Bean Validationで検証を行う。Validator
をorg.springframework.web.bind.WebDataBinder
に追加する必要がある。@Inject PasswordEqualsValidator passwordEqualsValidator; // (1) @InitBinder protected void initBinder(WebDataBinder binder){ binder.addValidators(passwordEqualsValidator); // (2) }
項番 説明 (1)相関チェックを行う Validator
をInjectする。 (2) InjectしたValidator
をWebDataBinder
に追加する。WebDataBinder
に追加しておくことで、ハンドラメソッド呼び出し前に行われる入力チェック処理にて、(1)で追加したValidator
が実行され、相関チェックを行うことが出来る。
3.4.1.6.2. 業務処理の呼び出し¶
業務処理が実装されているServiceをInjectし、InjectしたServiceのメソッドを呼び出すことで業務処理を実行する。
@Inject SampleService sampleService; // (1) @GetMapping("hello") public String hello(Model model){ String message = sampleService.hello(); // (2) model.addAttribute("message", message); return "sample/hello"; }
項番 説明 (1) 業務処理が実装されているService
をInjectする。 (2)Injectした Service
のメソッドを呼び出し、業務処理を実行する。
3.4.1.6.3. ドメインオブジェクトへの値反映¶
@GetMapping("hello") public String hello(@Validated SampleForm form, BindingResult result, Model model){ // omitted Sample sample = new Sample(); // (1) sample.setField1(form.getField1()); sample.setField2(form.getField2()); sample.setField3(form.getField3()); // ... // omitted // ... String message = sampleService.hello(sample); // (2) model.addAttribute("message", message); // (3) return "sample/hello"; }
項番 説明 (1) Serviceの引数となるドメインオブジェクトを生成し、フォームオブジェクトにバインドされている値を反映する。 (2)Serviceのメソッドを呼び出し、業務処理を実行する。 (3)業務処理から返却されたデータを Model
に追加する。
SampleController.java
@Inject SampleHelper sampleHelper; // (1) @GetMapping("hello") public String hello(@Validated SampleForm form, BindingResult result){ // omitted String message = sampleHelper.hello(form); // (2) model.addAttribute("message", message); return "sample/hello"; }
SampleHelper.java
public class SampleHelper { @Inject SampleService sampleService; public String hello(SampleForm form){ // (3) Sample sample = new Sample(); sample.setField1(form.getField1()); sample.setField2(form.getField2()); sample.setField3(form.getField3()); // ... // and more ... // ... String message = sampleService.hello(sample); return message; } }
項番 説明 (1)ControllerにHelperクラスのオブジェクトをInjectする。 (2)InjectしたHelperクラスのメソッドを呼び出すことで、ドメインオブジェクトへの値の反映を行っている。 Helperクラスに処理を委譲することで、Controllerの実装をシンプルな状態に保つことができる。 (3)ドメインオブジェクトを生成した後にServiceクラスのメソッド呼び出し、業務処理を実行している。
3.4.1.6.4. フォームオブジェクトへの値反映¶
@GetMapping("hello") public String hello(SampleForm form, BindingResult result, Model model){ // omitted Sample sample = sampleService.getSample(form.getId()); // (1) form.setField1(sample.getField1()); // (2) form.setField2(sample.getField2()); form.setField3(sample.getField3()); // ... // and more ... // ... model.addAttribute(sample); // (3) return "sample/hello"; }
項番 説明 (1)業務処理が実装されているServiceのメソッドを呼び出し、ドメインオブジェクトを取得する。 (2)取得したドメインオブジェクトの値をフォームオブジェクトに反映する。 (3)表示のみ行う項目がある場合は、データを参照できるようにするために、 Model
にドメインオブジェクトを追加する。Note
画面に表示のみ行う項目については、フォームオブジェクトに項目をもつのではなく、Entityなどのドメインオブジェクトから直接値を参照することを推奨する。
フォームオブジェクトへの値反映処理は、Controllerのハンドラメソッド内で実装してもよいが、コード量が多くなる場合はハンドラメソッドの可読性を考慮してHelperクラスのメソッドに委譲することを推奨する。
SampleController.java
@GetMapping("hello") public String hello(@Validated SampleForm form, BindingResult result){ // omitted Sample sample = sampleService.getSample(form.getId()); sampleHelper.applyToForm(sample, form); // (1) model.addAttribute(sample); return "sample/hello"; }
SampleHelper.java
public void applyToForm(SampleForm destForm, Sample srcSample){ destForm.setField1(srcSample.getField1()); // (2) destForm.setField2(srcSample.getField2()); destForm.setField3(srcSample.getField3()); // ... // and more ... // ... }
項番 説明 (1)ドメインオブジェクトの値をフォームオブジェクトに反映するためのメソッドを呼び出す。 (2)ドメインオブジェクトの値をフォームオブジェクトに反映するためのメソッドにて、ドメインオブジェクトの値をフォームオブジェクトに反映する。
3.4.2. フォームオブジェクトの実装¶
フォームオブジェクトはHTML上のformを表現するオブジェクト(JavaBean)であり、以下の役割を担う。
- データベース等で保持している業務データを保持し、HTML(JSP) formから参照できるようにする。
- HTML formから送信されたリクエストパラメータを保持し、ハンドラメソッドで参照できるようにする。
フォームオブジェクトの実装について、以下4点に着目して説明する。
3.4.2.1. フォームオブジェクトの作成方法¶
java.lang.String
だけではなく、任意の型で定義することができる。public class SampleForm implements Serializable { private String id; private String name; private Integer age; private String genderCode; private Date birthDate; // ommitted getter/setter }Warning
フォームオブジェクトには画面に表示のみ行う項目は保持せず、HTML formの項目のみ保持することを推奨する。
フォームオブジェクトに画面表示のみ行う項目の値を設定した場合、フォームオブジェクトをHTTPセッションオブジェクトに格納する際にメモリを多く消費する事になり、メモリ枯渇の原因になる可能性がある。
画面表示のみの項目は、Entityなどのドメイン層のオブジェクトをリクエストスコープに追加(
Model.addAttribute
)することでHTML(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 AMM- : Dec 9, 2013-M : 3:41:45 AMNote
DateTimeFormat
では、java.util.Date
、java.util.Calendar
、java.lang.Long
(タイムスタンプとしてのLong
) およびjava.tim.*
(JSR-310 Date And Time API) を型として指定できる。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. 入力チェック用のアノテーションの指定¶
3.4.2.2. フォームオブジェクトの初期化方法¶
@ModelAttribute
アノテーションを使うことで結びつけることができる。@ModelAttribute
アノテーションを付与したメソッドで行う。setUpXxxForm
というメソッド名で定義することを推奨する。@ModelAttribute // (1) public SampleForm setUpSampleForm() { SampleForm form = new SampleForm(); // populate form return form; }@ModelAttribute("xxx") // (2) public SampleForm setUpSampleForm() { SampleForm form = new SampleForm(); // populate form return form; }@ModelAttribute public SampleForm setUpSampleForm( @CookieValue(value = "name", required = false) String name, // (3) @CookieValue(value = "age", required = false) Integer age, @CookieValue(value = "birthDate", required = false) Date birthDate) { SampleForm form = new SampleForm(); form.setName(name); form.setAge(age); form.setBirthDate(birthDate); return form; }
項番 説明 (1)Model
に追加するための属性名は、クラス名の先頭を小文字にした値(デフォルト値)が設定される。この例ではsampleForm
が属性名になる。返却したオブジェクトは、model.addAttribute(form)
相当の処理が実行されModel
に追加される。 (2)Model
に追加するための属性名を指定したい場合は、@ModelAttribute
アノテーションのvalue属性に指定する。この例では /xxx
が属性名になる。返却したオブジェクトは、model.addAttribute("xxx", form)
相当の処理が実行されModel
に追加される。デフォルト値以外の属性名を指定した場合、ハンドラメソッドの引数としてフォームオブジェクトを受け取る時に@ModelAttribute("xxx")
の指定が必要になる。 (3) ModelAttributeメソッドは、ハンドラメソッドと同様に初期化に必要なパラメータを渡すこともできる。例では、@CookieValue
アノテーションを使用してCookieの値をフォームオブジェクトに設定している。
Note
フォームオブジェクトにデフォルト値を設定したい場合はModelAttributeメソッドで値を設定すること。
例の(3)ではCookieから値を取得しているが、定数クラスなどに定義されている固定値を直接設定してもよい。
Note
ModelAttributeメソッドはController内に複数定義することができる。各メソッドはControllerのハンドラメソッドが呼び出される前に毎回実行される。
Warning
ModelAttributeメソッドはリクエストごとにメソッドが実行されるため、特定のリクエストの時のみに必要なオブジェクトについてModelAttributeメソッドを使って生成すると、無駄なオブジェクトの生成及び初期化処理が行われる点に注意すること。
特定のリクエストのみで必要なオブジェクトについては、ハンドラメソッド内で生成しModel
に追加する方法にすること。
3.4.2.3. HTML 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は以下の役割を担う。
- クライアントに応答するレスポンスデータ(HTML)を生成する。Viewはモデル(フォームオブジェクトやドメインオブジェクトなど)から必要なデータを取得し、クライアントが描画するために必要な形式でレスポンスデータを生成する。
3.4.3.1. JSPの実装¶
ViewResolver
は、Spring Frameworkより提供されているので、提供されているクラスを利用する。ViewResolver
の設定方法は、HTMLを応答するを参照されたい。以下に、基本的なJSPの実装方法について説明する。
- インクルード用の共通JSPの作成
- モデルに格納されている値を表示する
- モデルに格納されている数値を表示する
- モデルに格納されている日時を表示する
- リクエストURLを生成する
- HTML formへフォームオブジェクトをバインドする
- 入力チェックエラーを表示する
- 処理結果のメッセージを表示する
- コードリストを表示する
- 固定文言を表示する
- 条件によって表示を切り替える
- コレクションの要素に対して表示処理を繰り返す
- ページネーション用のリンクを表示する
- 権限によって表示を切り替える
本章では代表的なJSPタグライブラリの使い方は説明しているが、全てのJSPタグライブラリの説明はしていないので、詳細な使い方については、それぞれのドキュメントを参照すること。
項番 JSPタグライブラリ名 ドキュメント
Spring’s form tag library
Spring’s JSP Tag Library
JSTL
Common library’s tags & el functions
3.4.3.1.1. インクルード用の共通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タグライブラリを宣言している。 例では、 core
とfmt
を利用している。(2)Spring FrameworkのJSPタグライブラリを宣言している。 例では、 spring
とform
とsec
を利用している。(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エスケープも行われる。Note
<c:out>
の詳細は、Jakarta Standard Tag Library 3.0 Specification Document(4 General-Purpose Actions)を参照されたい。
3.4.3.1.3. モデルに格納されている数値を表示する¶
数値型の値をフォーマットして出力する場合、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
どちらの方法を使用してもよいが、一つのアプリケーションの中で混在して使用することは、保守性を低下させる可能性があるので避けた方がよい。
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タグライブラリを使用する。
<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タグライブラリを使用する。
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. 固定文言を表示する¶
<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タグライブラリを使用する。
<sec:authorize>
を使用して表示の切り替えを行う。3.4.3.3. スタイルシートの実装¶
Note
<form:xxx>
タグを使ってフォームを生成した場合、id属性は自動で設定される。class属性については、アプリケーション開発者によって指定が必要。
3.4.4. 共通処理の実装¶
3.4.4.1. Controllerの呼び出し前後で行う共通処理の実装¶
本項でいう共通処理とは、Controllerを呼び出し前後に行う必要がある共通的な処理のことを指す。
3.4.4.1.1. Servlet Filterの実装¶
MDC
に値を格納している。java
public class ClientInfoPutFilter extends OncePerRequestFilter { // (1) private static final String ATTRIBUTE_NAME = "X-Forwarded-For"; protected final void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String remoteIp = request.getHeader(ATTRIBUTE_NAME); if (remoteIp == null) { remoteIp = request.getRemoteAddr(); } MDC.put(ATTRIBUTE_NAME, remoteIp); try { filterChain.doFilter(request, response); } finally { MDC.remove(ATTRIBUTE_NAME); } } }
web.xml
<filter> <!-- (2) --> <filter-name>clientInfoPutFilter</filter-name> <filter-class>x.y.z.ClientInfoPutFilter</filter-class> </filter> <filter-mapping> <!-- (3) --> <filter-name>clientInfoPutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
項番 説明 (1)サンプルではSpring Frameworkから提供されている org.springframework.web.filter.OncePerRequestFilter
の子クラスとしてServlet Filterを作成することで、同一リクエスト内で1回だけ実行されることを保証している。(2)作成したServlet Filterを web.xml
に登録する。(3)登録したServlet Filterを適用するURLのパターンを指定する。
Servlet FilterをSpring FrameworkのBeanとして定義することもできる。
web.xml
<filter> <filter-name>clientInfoPutFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <!-- (1) --> </filter> <filter-mapping> <filter-name>clientInfoPutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
applicationContext.xml
<bean id="clientInfoPutFilter" class="x.y.z.ClientInfoPutFilter" /> <!-- (2) -->
項番 説明 (1)サンプルではSpring Frameworkから提供されているorg.springframework.web.filter.DelegatingFilterProxy
をServlet Filterのクラスに指定することで、(2)で定義したServlet Filterに処理が委譲される。(2)作成したServlet FilterのクラスをBean定義ファイル(applicationContext.xml
)に追加する。その際に、id属性にはweb.xml
で指定したフィルター名(<filter-name>
タグで指定した値 )にすること。
3.4.4.1.2. HandlerInterceptorの実装¶
HandlerInterceptorでは以下の3つのポイントで処理を実行することが出来る。
- Controllerのハンドラメソッドを実行する前
HandlerInterceptor#preHandle
メソッドとして実装する。 - Controllerのハンドラメソッドが正常終了した後
HandlerInterceptor#postHandle
メソッドとして実装する。 - Controllerのハンドラメソッドの処理が完了した後(正常/異常に関係なく実行される)
HandlerInterceptor#afterCompletion
メソッドとして実装する。
public class SuccessLoggingInterceptor implements HandlerInterceptor { // (1) private static final Logger logger = LoggerFactory .getLogger(SuccessLoggingInterceptor.class); @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method m = handlerMethod.getMethod(); logger.info("[SUCCESS CONTROLLER] {}.{}", new Object[] { m.getDeclaringClass().getSimpleName(), m.getName()}); } }
spring-mvc.xml
<mvc:interceptors> <!-- omitted --> <mvc:interceptor> <mvc:mapping path="/**" /> <!-- (2) --> <mvc:exclude-mapping path="/resources/**" /> <!-- (3) --> <bean class="x.y.z.SuccessLoggingInterceptor" /> <!-- (4) --> </mvc:interceptor> <!-- omitted --> </mvc:interceptors>
項番 説明 (1)サンプルではSpring Frameworkから提供されている org.springframework.web.servlet.HandlerInterceptor
の実装クラスとしてHandlerInterceptorを作成している。(2)作成したHandlerInterceptorを適用するパスのパターンを指定する。 (3)作成したHandlerInterceptorを適用しないパスのパターンを指定する。 (4)作成したHandlerInterceptorを spring-mvc.xml
の<mvc:interceptors>
タグ内に追加する。
Note
非同期リクエストを処理するorg.springframework.web.servlet.AsyncHandlerInterceptor
も提供されている。
Note
HandlerInterceptorのパス指定においてはワイルドカード(*
や**
)を使用することができる。このうち**
はSpring Framework 5.3.0よりパスの最後にしか使用できなくなった。最後以外に使用した場合は起動時やアクセス時にエラーとなる。
3.4.4.2. Controllerの共通処理の実装¶
ここでいう共通処理とは、すべてのControllerで共通的に実装する必要がある処理のことを指す。
3.4.4.2.1. HandlerMethodArgumentResolverの実装¶
Spring FrameworkのデフォルトでサポートされていないオブジェクトをControllerの引数として渡したい場合は、HandlerMethodArgumentResolverを実装してControllerの引数として受け取れるようにする。
JavaBean
public class CommonParameters implements Serializable { // (1) private String param1; private String param2; private String param3; // omitted }
HandlerMethodArgumentResolver
public class CommonParametersMethodArgumentResolver implements HandlerMethodArgumentResolver { // (2) @Override public boolean supportsParameter(MethodParameter parameter) { return CommonParameters.class.equals(parameter.getParameterType()); // (3) } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { CommonParameters params = new CommonParameters(); // (4) params.setParam1(webRequest.getParameter("param1")); params.setParam2(webRequest.getParameter("param2")); params.setParam3(webRequest.getParameter("param3")); return params; }
Controller
@GetMapping(value = "home") public String home(CommonParameters commonParams) { // (5) logger.debug("param1 : {}",commonParams.getParam1()); logger.debug("param2 : {}",commonParams.getParam2()); logger.debug("param3 : {}",commonParams.getParam3()); // omitted return "sample/home"; }
spring-mvc.xml
<mvc:annotation-driven> <mvc:argument-resolvers> <!-- omitted --> <bean class="x.y.z.CommonParametersMethodArgumentResolver" /> <!-- (6) --> <!-- omitted --> </mvc:argument-resolvers> </mvc:annotation-driven>
項番 説明 (1)共通パラメータを保持するJavaBean。(2)org.springframework.web.method.support.HandlerMethodArgumentResolver
インタフェースを実装する。(3)処理対象とする型を判定する。例では、共通パラメータを保持するJavaBeanの型がControllerの引数として指定されていた場合に、このクラスのresolveArgumentメソッドが呼び出される。(4)リクエストパラメータから値を取得し、共通パラメータを保持するJavaBeanに設定し返却する。(5)Controllerのハンドラメソッドの引数に共通パラメータを保持するJavaBeanを指定する。(4)で返却されるオブジェクトが渡される。(6)作成したHandlerMethodArgumentResolverを spring-mvc.xml
の<mvc:argument-resolvers>
タグ内に追加する。
Note
全てのControllerのハンドラメソッドで共通的に渡すパラメータがある場合は、HandlerMethodArgumentResolverを使ってJavaBeanに変換してから渡す方法が有効的である。ここでいうパラメータとは、リクエストパラメータに限らない。
3.4.4.2.2. @ControllerAdvice
の実装¶
@ControllerAdvice
アノテーションを付与したクラスでは、複数のControllerで実行したい共通的な処理を実装する。
@ControllerAdvice
アノテーションを付与したクラスを作成すると、
@InitBinder
を付与したメソッド@ExceptionHandler
を付与したメソッド@ModelAttribute
を付与したメソッド
で実装した処理を、複数のControllerに適用する事ができる。
Tip
@ControllerAdvice
アノテーションは、Spring Framework 3.2 から追加された仕組みだが、全てのControllerに処理が適用される仕組みになっていたため、アプリケーション全体の共通処理しか実装できなかった。
Spring Framework 4.0 からは、共通処理を適用するControllerを柔軟に指定する事ができるように改善されている。
この改善により、様々な粒度で共通処理を実装する事ができるようになった。
以下に、共通処理を適用するControllerを指定する方法(属性の指定方法)について説明する。
項番 | 属性 | 説明と指定例 |
---|---|---|
(1)
|
annotations |
アノテーションを指定する。 指定したアノテーションが付与されたControllerに対して共通処理が適用される。
以下に指定例を示す。
@ControllerAdvice(annotations = LoginFormModelAttributeSetter.LoginFormModelAttribute.class)
public class LoginFormModelAttributeSetter {
@Target(TYPE)
@Retention(RUNTIME)
public static @interface LoginFormModelAttribute {}
// omitted
}
@LoginFormModelAttribute
@Controller
public class WelcomeController {
// omitted
}
@LoginFormModelAttribute
@Controller
public class LoginController {
// omitted
}
上記例では、 |
(2)
|
assignableTypes |
クラス又はインタフェースを指定する。
指定したクラス又はインタフェースに割り当て可能(キャスト可能)なControllerに対して共通処理が適用される。
本属性を使用する場合は、共通処理を適用するControllerであることを示すためのマーカーインタフェースを属性値に指定するスタイルを採用することを推奨する。
このスタイルを採用した場合、Controller側では、適用したい共通処理用のマーカーインタフェースを実装するだけでよい。
以下の指定例を示す。
@ControllerAdvice(assignableTypes = ISODateInitBinder.ISODateApplicable.class)
public class ISODateInitBinder {
public static interface ISODateApplicable {}
// omitted
}
@Controller
public class SampleController implements ISODateApplicable {
// omitted
}
上記例では、 |
(3)
|
basePackageClasses |
クラス又はインタフェースを指定する。 指定したクラス又はインタフェースのパッケージ配下のControllerに対して共通処理が適用される。 本属性を使用する場合は、
を属性値に指定するスタイルを採用することを推奨する。
以下に指定例を示す。
package com.example.app
@ControllerAdvice(basePackageClasses = AppGlobalExceptionHandler.class)
public class AppGlobalExceptionHandler {
// omitted
}
package com.example.app.sample
@Controller
public class SampleController {
// omitted
}
上記例では、 package com.example.app.common
@ControllerAdvice(basePackageClasses = AppPackage.class)
public class AppGlobalExceptionHandler {
// omitted
}
package com.example.app
public interface AppPackage {
}
|
(4)
|
basePackages |
パッケージ名を指定する。 指定したパッケージ配下のControllerに対して共通処理が適用される。
以下に指定例を示す。
@ControllerAdvice(basePackages = "com.example.app")
public class AppGlobalExceptionHandler {
// omitted
}
|
(5)
|
value |
@ControllerAdvice("com.example.app")
public class AppGlobalExceptionHandler {
// omitted
}
|
Tip
basePackageClasses
属性 / basePackages
属性 / value
属性は、共通処理を適用したいControllerが格納されているベースパッケージを指定するための属性であるが、basePackageClasses
属性を使用した場合、
- 存在しないパッケージを指定してしまう事を防ぐことが出来る
- IDE上で行ったパッケージ名変更と連動することが出来る
ため、タイプセーフな指定方法と言える。
@InitBinder
メソッドの実装サンプルを示す。yyyy/MM/dd
に設定している。@ControllerAdvice // (1) @Order(0) // (2) public class SampleControllerAdvice { // (3) @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); } }
項番 説明 (1)@ControllerAdvice
アノテーションを付与することで、ControllerAdviceのBeanであることを示している。 (2)@Order
アノテーションを付与することで、共通処理が適用される優先度を指定する。複数のControllerAdviceに依存関係があるなど、ControllerAdviceに順序性を持たせたい場合は必ず指定すること。順序性を持たせる必要がなければ指定しなくてもよい。 (3)@InitBinder
メソッドを実装する。全てのControllerに対して@InitBinder
メソッドが適用される。
@ExceptionHandler
メソッドの実装サンプルを示す。org.springframework.dao.PessimisticLockingFailureException
をハンドリングしてロックエラー画面のViewを返却している。// (1) @ExceptionHandler(PessimisticLockingFailureException.class) public String handlePessimisticLockingFailureException( PessimisticLockingFailureException e) { return "error/lockError"; }
項番 説明 (1)@ExceptionHandler
メソッドを実装する。全てのControllerに対して@ExceptionHandler
メソッドが適用される。
@ModelAttribute
メソッドの実装サンプルを示す。Model
に格納している。ControllerAdvice
// (1) @ModelAttribute public CommonParameters setUpCommonParameters( @RequestParam(value = "param1", defaultValue="def1") String param1, @RequestParam(value = "param2", defaultValue="def2") String param2, @RequestParam(value = "param3", defaultValue="def3") String param3) { CommonParameters params = new CommonParameters(); params.setParam1(param1); params.setParam2(param2); params.setParam3(param3); return params; }
Controller
@GetMapping("home") public String home(@ModelAttribute CommonParameters commonParams) { // (2) logger.debug("param1 : {}",commonParams.getParam1()); logger.debug("param2 : {}",commonParams.getParam2()); logger.debug("param3 : {}",commonParams.getParam3()); // omitted return "sample/home"; }
項番 説明 (1)@ModelAttribute
メソッドを実装する。全てのControllerに対して@ModelAttribute
メソッドが適用される。(2)@ModelAttribute
メソッドで生成されたオブジェクトが渡る。
3.4.5. 二重送信防止について¶
送信ボタンの複数回押下や完了画面の再読み込み(F5ボタンによる再読み込み)などで、 同じ処理が複数回実行されてしまう可能性があるため、二重送信を防止するための対策は必ず行うこと。
対策を行わない場合に発生する問題点や対策方法の詳細は、二重送信防止を参照されたい。
3.4.6. セッションの使用について¶
@SessionAttributes
アノテーションをControllerクラスに付与する必要がある。@SessionAttributes
アノテーションの利用を検討すること。@SessionAttributes
アノテーションの利用有無を判断すること。セッションの利用指針及びセッション使用時の実装方法の詳細は、セッション管理を参照されたい。