Caution
本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。
目次
本節では、Webアプリケーションのセッション管理について説明する。
項番 説明 (1) Webブラウザ(Client)は、セッションが確立していない状態で、Webアプリケーション(Server)にアクセスする。 (2) Webアプリケーションは、Webブラウザとのセッションを管理するために、HttpSessionオブジェクトを生成する。HttpSessionオブジェクトを生成したタイミングで、セッションIDが払い出される。 (3) Webアプリケーションは、Webブラウザから送信されたデータを、HttpSessionオブジェクトに格納する。 (4) Webアプリケーションは、Webブラウザにレスポンスを返却する。レスポンスの「Set-Cookie」ヘッダに、「JSESSIONID = 払い出されたセッションID」を設定することで、セッションIDをWebブラウザに連携する。連携したセッションIDはCookieに格納される。 (5) Webブラウザは、リクエストの「Cookie」ヘッダに、「JSESSIONID = 払い出されたセッションID」を設定することで、セッションIDをWebアプリケーションと連携する。Webアプリケーションがデプロイされているアプリケーションサーバは、Webブラウザから連携されたセッションIDに対応するHttpSessionオブジェクトを取得し、リクエストに関連づける。 (6) Webアプリケーションは、リクエストに関連付けられたHttpSessionオブジェクトから、(1)のリクエストで格納したデータを取得する。リクエストをまたいで、同じデータにアクセスすることができる。 (7) Webアプリケーションは、Webブラウザにレスポンスを返却する。Note
セッションIDを連携するためのパラメータ名について
JavaEEのSerlvetの仕様では、セッションIDを連携するためのパラメータ名のデフォルトは、「JSESSIONID」となっている。
Note
以降の説明で登場する "セッション" は、Servlet APIより提供されている javax.servlet.http.HttpSession オブジェクトの事である。 HttpSession オブジェクトは、上記で説明した論理的なセッションを表現するJavaオブジェクトである。
本ガイドラインで推奨している方法でWebアプリケーションを作成した場合、以下のいずれかの処理でセッションが生成される。
項番 説明
Spring Securityから提供されている認証・認可を行う処理。Spring Securityの設定により、セッションの生成有無や、生成タイミングを指定することができる。Spring Securityで行われるセッション管理についての詳細は、Spring Securityにおけるセッション管理を参照されたい。
Spring Securityから提供されているCSRFトークンチェックを行う処理。既にセッションが確立されている場合は、新たなセッションは生成されない。CSRFトークンチェックの詳細については、CSRF対策を参照されたい。
共通ライブラリから提供されているトランザクショントークンチェックを行う処理。既にセッションが確立されている場合は、新たなセッションは生成されない。トランザクショントークンチェックの詳細については、二重送信防止を参照されたい。
RedirectAttributesインタフェースのaddFlashAttributeメソッドを使用して、リダイレクト先のリクエストにモデル(フォームオブジェクトやドメインオブジェクトなど)を引き渡す処理。既にセッションが確立されている場合は、新たなセッションは生成されない。RedirectAttributesおよびFlash scopeについての詳細は、リダイレクト先にデータを渡すを参照されたい。
@SessionAttributesアノテーションを使用して、モデル(フォームオブジェクトや、ドメインオブジェクトなど)をセッションに格納する処理。指定したモデル(フォームオブジェクトや、ドメインオブジェクトなど)がセッションに格納される。既にセッションが確立されている場合は、新たなセッションは生成されない。@SessionAttributesアノテーションの使用方法については、@SessionAttributesアノテーションの使用を参照されたい。
Spring Frameworkの、sessionスコープのBeanを使用する処理。既にセッションが確立されている場合は、新たなセッションは生成されない。sessionスコープのBeanの使用方法については、Spring FrameworkのsessionスコープのBeanの使用を参照されたい。Note
上記の項番4, 5, 6については、セッションの使用有無はControllerの実装によって指定するが、セッションの生成タイミングは、フレームワークによって制御される。 つまり、Controllerの処理として HttpSession のAPIを直接使用する必要はない。
本ガイドラインで推奨している方法でWebアプリケーションを作成した場合、以下のいずれかの処理でセッションに属性(オブジェクト)が格納される。
項番 説明
Spring Securityから提供されているCSRFトークンチェックを行う処理。払い出されたトークン値がセッションに格納される。CSRFトークンチェックの詳細については、CSRF対策を参照されたい。
共通ライブラリから提供されているトランザクショントークンチェックを行う処理。払い出されたトークン値がセッションに格納される。トランザクショントークンチェックの詳細については、二重送信防止を参照されたい。
RedirectAttributesインタフェースのaddFlashAttributeメソッドを使用して、リダイレクト先のリクエストにモデル(フォームオブジェクトやドメインオブジェクトなど)を引き渡す処理。RedirectAttributesインタフェースのaddFlashAttributeメソッドの引数に指定したオブジェクトが、セッション上に存在するFlash scopeという領域に格納される。RedirectAttributesおよびFlash scopeについての詳細は、リダイレクト先にデータを渡すを参照されたい。
@SessionAttributesアノテーションを使用して、モデル(フォームオブジェクトや、ドメインオブジェクトなど)をセッションに格納する処理。指定したモデル(フォームオブジェクトや、ドメインオブジェクトなど)がセッションに格納される。@SessionAttributesアノテーションの使用方法については、@SessionAttributesアノテーションの使用を参照されたい。
Spring Frameworkの、sessionスコープのBeanを使用する処理。sessionスコープのBeanがセッションに格納される。sessionスコープのBeanの使用方法については、Spring FrameworkのsessionスコープのBeanの使用を参照されたい。Note
オブジェクトをセッションに格納するタイミングはフレームワークによって制御されるため、Controllerの処理として HttpSession オブジェクトのsetAttributeメソッドを呼び出すことはない。
本ガイドラインで推奨している方法で、Webアプリケーションを作成した場合、以下のいずれかの処理でセッションから属性(オブジェクト)が削除される。
項番 説明
Spring Securityから提供されているログアウトを行う処理。認証されたユーザ情報がセッションから削除される。Spring Securityで行われるログアウト処理についての詳細は、認証を参照されたい。
共通ライブラリから提供されているトランザクショントークンチェックを行う処理。払い出されたトークン値が、ネームスペースに割り振られている上限値を超えた場合、使用されていないトークン値がセッションから削除される。トランザクショントークンチェックの詳細については、二重送信防止を参照されたい。
Flash scopeにオブジェクトを格納した後のリダイレクト処理。RedirectAttributesインタフェースのaddFlashAttributeメソッドの引数に指定したオブジェクトが、セッション上に存在するFlash scopeという領域から削除される。
Controllerの処理として、 SessionStatusオブジェクトのsetCompleteメソッドを呼び出した後のフレームワークの処理。@SessionAttributesアノテーションで指定したオブジェクトがセッションから削除される。Note
セッションからオブジェクトを削除するタイミングはフレームワークによって制御されるため、Controllerの処理として HttpSession オブジェクトのremoveAttributeメソッドを呼び出すことはない。
本ガイドラインで推奨している方法で、Webアプリケーションを作成した場合、以下のいずれかの処理でセッションが破棄される。
項番 説明
Spring Securityから提供されているログアウト処理。Spring Securityで行われるログアウト処理についての詳細は、認証を参照されたい。
アプリケーションサーバのセッションタイムアウト検知処理。
明示的に破棄する際のイメージを、以下に示す。
項番 説明 (1) Webブラウザからセッションを破棄する処理に、アクセスする。Spring Securityを使用する場合は、Spring Securityから提供されているログアウト処理が、セッションを破棄する処理を行っている。Spring Securityで行われるログアウト処理についての詳細は、認証を参照されたい。 (2) Webアプリケーションは、Webブラウザから連携されたセッションIDに対応するHttpSessionオブジェクトを破棄する。この時点でサーバ側には、 "SESSION01" というIDのHttpSessionオブジェクトが消滅する。 (3) Webブラウザから破棄されたセッションのセッションIDを使ってアクセスされた場合、セッションIDに対応するHttpSessionオブジェクトが存在しないため、別のセッションを生成する。上記例では、セッションIDが、 "SESSION02" のセッションを生成している。
タイムアウトによって、自動的に破棄される際のイメージを、以下に示す。
項番 説明 (1) 確立されたセッションに対して一定時間アクセスがない場合、アプリケーションサーバは、セッションタイムアウトを検知する。 (2) アプリケーションサーバは、セッションタイムアウトが検知されたセッションを破棄する。 (3) セッションタイムアウト発生後に、Webブラウザからアクセスされた場合、Webブラウザから送られてきたセッションIDに対応するHttpSessionオブジェクトが存在しないため、セッションタイムアウトエラーをWebブラウザに返却する。Note
セッションタイムアウトの設計
セッションにデータを格納する場合は、必ずセッションタイムアウトの設計を行うこと。特に、格納するデータのサイズが大きくなる場合は、タイムアウトは、可能な限り短く設定することを推奨する。
Note
デフォルトのセッションタイムアウト時間について
デフォルトのセッションタイムアウト時間は、アプリケーションサーバによって異なる。
- Tomcat : 1800 秒 (30分)
- WebLogic : 3600 秒 (60分)
- Websphere : 1800 秒 (30分)
- Resin : 1800 秒 (30分)
本ガイドラインで推奨している方法でWebアプリケーションを作成した場合、以下のいずれかの処理で、セッションタイムアウト後のリクエストを検知する。
項番 説明
Spring Securityから提供されているセッションのタイムアウトチェック処理。Spring Securityのデフォルトの設定では、セッションのタイムアウトチェックは行われない。そのため、セッションにデータを格納する場合は、Spring Securityのセッションのタイムアウトチェック処理を有効化するための設定が、必要となる。Spring Securityで行われるセッションのタイムアウトチェック処理の詳細は、Spring Securityにおけるセッション管理を参照されたい。
Spring Securityを使用しない場合は、Servlet Filter、または、Spring MVCのHandlerInterceptorにて、セッションのタイムアウトチェックを行う処理を実装する必要がある。
Spring Securityから提供されているセッションチェック処理を使用して、セッションタイムアウトを検知する際のイメージについて、以下に示す。
項番 説明 (1) 確立されたセッションに対して、一定時間アクセスがない場合、アプリケーションサーバは、セッションタイムアウトを検知し、セッションを破棄する。 (2) セッションタイムアウト発生後に、Webブラウザからアクセスが発生する。 (3) Spring Securityから提供されているセッションの存在チェック処理は、クライアントから連携されたセッションIDに対応するHttpSessionオブジェクトが存在しないため、セッションタイムアウトエラーとする。Spring Securityのデフォルト実装では、エラー画面を表示するための、URLへのリダイレクト要求が応答される。Note
セッションのタイムアウトチェックの必要性
「セッションにデータが格納されていること」が事前条件となる処理については、必ずセッションのタイムアウトチェックを行うこと。 セッションのタイムアウトチェックを行わないと、処理で必要なデータが取得できないため、予期しないシステムエラーの発生や、想定外の動作を引き起こす可能性がある。
Todo
TBD
Spring Securityから提供されているCSRF対策を有効にすると、POSTメソッドの呼び出し時にセッションタイムアウトが検知される前にSCRFトークンエラーが発生してしまう。
現在、 Spring SecurityのJIRA(SEC-2422)にて、Spring Securityの見解を問い合わせ中である。 Spring Securityの見解によっては、Servlet Filter を作成し、Spring SecurityのCSRF対策用のServlet Filterより前でチェックする必要がある。
Note
本ガイドラインでは、安易にセッションにデータを格納するのではなく、まずはセッションを使わない方針で検討し、本当に必要なデータのみセッションに格納することを推奨する。
Note
以下の条件にあてはまるデータについては、セッションにデータを格納した方がよい場合がある。
ユースケース間で連携はしないが、別のユースケースに移って戻った際に、状態を保持しておく必要があるデータ。例えば、一覧画面の検索条件が、このパターンに該当する。一覧画面の検索条件は、別のユースケース(例えば、「検索したデータを変更する」ユースケース)から戻った際に、別のユースケースに移る前の状態を保持することが機能要件となる事が多い。検索条件をhiddenで持ち回る方法もあるが、ユースケース間に余計な依存関係が生まれ、アプリケーションの実装も複雑になることが予想される。 ユースケース間で連携が必要なデータ。たとえば、ショッピングサイトのカートに格納するデータが、このパターンに該当する。ショッピングサイトのカートに格納するデータは、「商品をカートに追加する」ユースケース、「カートを表示する」ユースケース、「カートの状態を変更する」ユースケース、「カートにいれた商品を購入する」ユースケースでデータの連携が必要となるためである。ただし、スケラビリティを考慮する必要がある場合は、セッションではなくデータベースに格納した方がよいケースがある。
セッション利用時のメリットとデメリットは、以下の通りである。
メリット
デメリット
Note
APサーバをスケールアウトする場合、以下のいずれかの仕組みが必要となる。
それぞれの注意点を考慮した上で、スケールアウトする方法を判断すること。
メリット
デメリット
セッションに格納するデータは、以下の点を考慮する必要がある。
ディスクへの入出力が発生するケースは、以下の通りである。
ネットワークへの入出力が発生するケースは、以下の通りである。
セッションに格納するデータは、できる限りコンパクトにすることを推奨する。
セッションに格納されているデータの容量が大きい場合は、致命的なパフォーマンス低下を引き起こす原因となるので、容量の大きいデータは、セッションに格納しないように設計することを推奨する。
パフォーマンス低下を引き起こす主な原因は、以下の通り。
セッションに格納するデータをコンパクトにするために、以下の条件にあてはまるデータについては、セッションスコープではなく、リクエストスコープに格納することを検討すること。
Warning
本ガイドラインで推奨している方法でWebアプリケーションを作成した場合、以下のデータがセッションに格納されるため、何れかの仕組みを適用する必要がある。
本ガイドラインでは、セッションにデータを格納する場合は、以下のいずれかの方法を使用して行うことを推奨している。
Warning
Controllerの処理メソッドの引数に HttpSession オブジェクトを指定することで、 HttpSession のAPIを直接呼び出すことができるが、 原則としてはHttpSessionのAPIを直接使用しないことを強く推奨する。
HttpSession を直接使わないと実現できない処理については、 HttpSession のAPIを直接使用してもよいが、 多くの業務処理において、HttpSessionのAPIを直接使用する必要はないため、原則Controllerの処理メソッドの引数として、 HttpSession オブジェクトを指定しないようにすること。
@SessionAttributesアノテーションは、Controller内で行われる画面遷移において、データを持ち回る場合に使用する。
@SessionAttributesアノテーションをクラスに指定し、セッションに格納するオブジェクトを指定する。
@Controller @RequestMapping("wizard") @SessionAttributes(types = { WizardForm.class, Entity.class }) // (1) public class WizardController { // ... }
項番 説明 (1) @SessionAttributesアノテーションのtypes属性に、セッションに格納するオブジェクトの型を指定する。@ModelAttributeアノテーション、またはModelのaddAttributeメソッドを使用して、Modelオブジェクトに追加されたオブジェクトのうち、types属性で指定した型に一致するオブジェクトが、セッションに格納される。上記例では、 WizardFormクラスと Entityクラスのオブジェクトが、セッションに格納される。Note
ライフサイクルの管理単位
@SessionAttributesアノテーションを使って、セッションに格納したオブジェクトは、Controller単位で、ライフサイクルが管理される。
SessionStatusオブジェクトのsetCompleteメソッドを呼び出すと、@SessionAttributeアノテーションで指定したオブジェクトが、すべてセッションから削除される。 そのため、ライフサイクルが異なるオブジェクトを、セッションに格納する場合は、Controllerを分割する必要がある。
Warning
@SessionAttributeアノテーション使用時の注意点
Controller単位で、ライフサイクルされると上で説明したが、複数のControllerで同じ属性名のオブジェクトを、@SessionAttributeアノテーションを使って、セッションに格納した場合は、 Controllerをまたいで、ライフサイクルが管理される。
別ウィンドウやタブを開いて、同時に画面操作できる処理の場合は、同じオブジェクトに対してアクセスすることになるため、不具合を引き起こす原因になりうる。 そのため、複数のControllerで、同じフォームオブジェクトのクラスを使用する場合は、@ModelAttributeアノテーションのvalue属性に、それぞれ別の値(属性名)を指定した上で、 @SessionAttributes アノテーションの value属性に @ModelAttributeアノテーションのvalue属性に指定した値と同じ値を指定すること。
@Controller @RequestMapping("wizard") @SessionAttributes(value = { "wizardCreateForm" }) // (2) public class WizardController { // ... @ModelAttribute(value = "wizardCreateForm") public WizardForm setUpWizardForm() { return new WizardForm(); } // ... }
項番 説明 (2) @SessionAttributesアノテーションのvalue属性に、セッションに格納するオブジェクトの属性名を指定する。@ModelAttributeアノテーション、またはModelのaddAttributeメソッドを使用して、Modelオブジェクトに追加されたオブジェクトのうち、value属性で指定した属性名に一致するオブジェクトが、セッションに格納される。上記例では、属性名が"wizardCreateForm"のオブジェクトが、セッションに格納される。
セッションにオブジェクトを追加する場合、以下2つの方法を使用する。
Modelオブジェクトに追加されたオブジェクトは、@SessionAttributesアノテーションのtypesと、value属性の属性値にしたがって、 セッションに格納されるため、Controllerの処理メソッドで、セッションを意識した実装を行う必要はない。
@ModelAttribute(value = "wizardForm") // (1) public WizardForm setUpWizardForm() { return new WizardForm(); }
項番 説明 (1) Modelオブジェクトに格納する属性名を、value属性に指定する。上記例では、返却したオブジェクトが、"wizardForm"という属性名でセッションに格納される。value属性を指定した場合、セッションにオブジェクトを格納した後のリクエストで、@ModelAttributeアノテーションの付与されたメソッドが呼び出されなくなるため、無駄なオブジェクトの生成が行われないというメリットがある。Warning
@ModelAttributeアノテーションのvalue属性を省略した場合の動作について
value属性を省略した場合、デフォルトの属性名を生成するために、すべてのリクエストで、@ModelAttributeアノテーションの付与されたメソッドが呼ばれる。 そのため、無駄なオブジェクトが生成されるというデメリットがあるので、 セッションに格納する場合は、この方法は原則使用しないこと。
@ModelAttribute // (1) public WizardForm setUpWizardForm() { return new WizardForm(); }
項番 説明 (1) @ModelAttributeアノテーションが付与されたメソッドにて、セッションに追加するオブジェクトを生成し、返却する。上記例では、"wizardForm"アノテーションという属性名で返却したオブジェクトが、セッションに格納される。
@RequestMapping(value = "update/{id}", params = "form1") public String updateForm1(@PathVariable("id") Integer id, WizardForm form, Model model) { Entity loadedEntity = entityService.getEntity(id); model.addAttribute(loadedEntity); // (3) beanMapper.map(loadedEntity, form); return "wizard/form1"; }
項番 説明 (3) ModelオブジェクトのaddAttributeメソッドを使用して、セッションに格納するオブジェクトを追加する。上記例では、"entity"という属性名で、ドメイン層から取得したオブジェクトを、セッションに格納している。
@RequestMapping(value = "save", method = RequestMethod.POST) public String save(@Validated({ Wizard1.class, Wizard2.class, Wizard3.class }) WizardForm form, // (1) BindingResult result, Entity entity, // (2) RedirectAttributes redirectAttributes) { // ... return "redirect:/wizard/save?complete"; }
項番 説明 (1) Modelオブジェクトに格納されているオブジェクトを取得する。上記例では、"wizardForm"という属性名でセッションスコープに格納されているオブジェクトが、引数formに渡される。@Validated アノテーションで指定している Wizard1.class , Wizard2.class , Wizard3.class については、 Appendixの @SessionAttributesアノテーションを使ったウィザード形式の画面遷移の実装例 を参照されたい。 (2) 上記例では、"entity"という属性名でセッションスコープに格納されているオブジェクトが、引数entityに渡される。
Controllerの処理メソッドの引数に渡すオブジェクトが、Modelオブジェクトに存在しない場合、@ModelAttributeアノテーションの指定の有無で、動作が変わる。
Note
リダイレクト時の動作について
遷移先をリダイレクトにした場合は、生成されたオブジェクトは、セッションに格納されない。 そのため、生成されたオブジェクトを、リダイレクト先の処理で参照したい場合は、RedirectAttributesのaddFlashAttributeメソッドを使用して、Flashスコープにオブジェクトを格納する必要がある。
@RequestMapping(value = "save", method = RequestMethod.POST) public String save(@Validated({ Wizard1.class, Wizard2.class, Wizard3.class }) WizardForm form, // (3) BindingResult result, @ModelAttribute Entity entity, // (4) RedirectAttributes redirectAttributes) { // ... return "redirect:/wizard/save?complete"; }
項番 説明 (3) @Validatedアノテーションで、特定の検証グループ(Wizard1.class, Wizard2.class, Wizard3.class)を設定して入力チェックを行っている。入力チェックの詳細については、入力チェックを参照されたい。 (4) 引数に、@ModelAttributeアノテーションを指定している場合、セッションに対象のオブジェクトが存在しない時に呼び出されると、HttpSessionRequiredExceptionが発生する。HttpSessionRequiredExceptionは、ブラウザバックや、URL直接指定のアクセスなどの、クライアントの操作に起因して発生する例外になるため、クライアントエラーとして、例外ハンドリングを行う必要がある。
HttpSessionRequiredExceptionをクライアントエラーとするための設定は、以下の通りである。
<bean class="org.terasoluna.gfw.web.exception.SystemExceptionResolver"> <property name="exceptionCodeResolver" ref="exceptionCodeResolver" /> <!-- ... --> <property name="exceptionMappings"> <map> <!-- ... --> <entry key="HttpSessionRequiredException " value="common/error/operationError" /> <!-- (5) --> </map> </property> <property name="statusCodes"> <map> <!-- ... --> <entry key="common/error/operationError" value="400" /> <!-- (6) --> </map> </property> <!-- ... --> </bean>
項番 説明 (5) 共通ライブラリから提供しているSystemExceptionResolverのexceptionMappingsに、HttpSessionRequiredExceptionの例外ハンドリングの定義を追加する。上記例では、 例外発生時の遷移先として、/WEB-INF/views/common/error/operationError.jspを指定している。 (6) SystemExceptionResolverのstatusCodesに、HttpSessionRequiredException発生時の、HTTPレスポンスコードを指定する。上記例では、 例外発生時のHTTPレスポンスコードとして、 Bad Request(400)を指定している。
<bean id="exceptionCodeResolver" class="org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver"> <!-- Setting and Customization by project. --> <property name="exceptionMappings"> <map> <!-- ... --> <entry key="HttpSessionRequiredException" value="w.xx.0003" /> <!-- (7) --> </map> </property> <property name="defaultExceptionCode" value="e.xx.0001" /> <!-- (8) --> </bean>
項番 説明 (7) 共通ライブラリから提供しているSimpleMappingExceptionCodeResolverのexceptionMappingsに、HttpSessionRequiredExceptionの例外ハンドリングの定義を追加する。上記例では、 例外発生時の例外コードとして、"w.xx.0003"を指定している。この設定を追加しない場合は、デフォルトの例外コードが、ログに出力される。 (8) 例外発生時のデフォルトの例外コード。
Note
セッションから削除されるタイミングについて
SessionStatusオブジェクトのsetCompleteメソッドを呼び出すことで、@SessionAttributesアノテーションの属性値に指定されているオブジェクトが、セッションから削除される。 ただし、実際に削除されるタイミングは、setCompleteメソッドを呼び出したタイミングではない。
SessionStatusオブジェクトのsetCompleteメソッド自体は、内部のフラグを変更しているだけなので、実際の削除は、Controllerの処理メソッドの処理が終了した後に、フレームワークによって行われる。
Note
View(JSP)からのオブジェクトの参照について
SessionStatusオブジェクトのsetCompleteメソッドを呼び出すことで、セッションから削除されるが、同じオブジェクトが、Modelオブジェクトに残っているため、View(JSP)から参照することができる。
セッションに格納したオブジェクトの削除は、以下3カ所で行う必要がある。
Warning
削除が必要な理由
セッションに格納されているオブジェクトは、ガベージコレクションの対象とならないため、不要になったオブジェクトを削除しないと、メモリ枯渇の原因になりうる。 また、不要なオブジェクトがセッションに格納されていると、セッションのスワットアウトが発生した際の処理が重くなり、アプリケーション全体の性能に影響を与える可能性がある。
Warning
削除が必要な理由
画面操作の途中でブラウザやタブを閉じた場合、セッションに格納されているフォームオブジェクトに入力途中の情報が残るため、初期表示時に削除しないと、入力途中の情報が画面に表示されてしまう。 ただし、入力途中の情報が画面に表示されてもよい場合は、初期表示するためのリクエストで削除は必須ではない。
完了画面を表示するためのリクエストで削除する際の実装例は、以下の通りである。
// (1) @RequestMapping(value = "save", method = RequestMethod.POST) public String save(@ModelAttribute @Validated({ Wizard1.class, Wizard2.class, Wizard3.class }) WizardForm form, BindingResult result, Entity entity, RedirectAttributes redirectAttributes) { // ... return "redirect:/wizard/save?complete"; // (2) } // (3) @RequestMapping(value = "save", params = "complete", method = RequestMethod.GET) public String saveComplete(SessionStatus sessionStatus) { sessionStatus.setComplete(); // (4) return "wizard/complete"; }
項番 説明 (1) 更新処理を行うための処理メソッド。 (2) 完了画面を表示するためのリクエスト(3)へ、リダイレクトする。 (3) 完了画面を表示するための処理メソッド。 (4) SessionStatusオブジェクトのsetCompleteメソッドを呼び出し、オブジェクトをセッションから削除する。Modelオブジェクトに同じオブジェクトが残っているため、直接、View(JSP)の表示処理に影響は与えない。
一連の画面操作を中止するためのリクエストで削除する際の実装例は、以下の通りである。
// (1) @RequestMapping(value = "save", params = "cancel", method = RequestMethod.POST) public String saveCancel(SessionStatus sessionStatus) { sessionStatus.setComplete(); // (2) return "redirect:/wizard/menu"; // (3) }
項番 説明 (1) 一連の画面操作を中止するための処理メソッド。 (2) SessionStatusオブジェクトのsetCompleteメソッドを呼び出し、オブジェクトをセッションから削除する。 (3) 上記例では、メニュー画面へ、リダイレクトしている。
入力画面を、初期表示するためのリクエストで削除する際の実装例は、以下の通りである。
// (1) @RequestMapping(value = "create", method = RequestMethod.GET) public String initializeCreateWizardForm(SessionStatus sessionStatus) { sessionStatus.setComplete(); // (2) return "redirect:/wizard/create?form1"; // (3) } // (4) @RequestMapping(value = "create", params = "form1") public String createForm1() { return "wizard/form1"; }
項番 説明 (1) 入力画面を初期表示するための処理メソッド。 (2) SessionStatusオブジェクトのsetCompleteメソッドを呼び出す。 (3) 入力画面を表示するためのリクエスト(4)へ、リダイレクトする。SessionStatusオブジェクトのsetCompleteメソッドを呼び出すことで、セッションからは削除されるが、Modelオブジェクトに同じオブジェクトが残っているため、直接View(JSP)を呼び出してしまうと、入力途中の情報が表示されてしまう。そのため、セッションから削除したうえで、入力画面を表示するためのリクエストへ、リダイレクトする必要がある。 (4) 入力画面を表示するための処理メソッド。
より具体的な実装例については、Appendixの@SessionAttributesアノテーションを使ったウィザード形式の画面遷移の実装例を参照されたい。
Spring FrameworkのsessionスコープのBeanを、定義する。
sessionスコープのBeanを定義する方法は、以下2種類の方法がある。
component-scanを使用する方法を、以下に示す。
@Component @Scope("session") // (1) public class SessionCart implements Serializable { private static final long serialVersionUID = 1L; private Cart cart; public Cart getCart() { if (cart == null) { cart = new Cart(); } return cart; } public void setCart(Cart cart) { this.cart = cart; } }
項番 説明 (1) Beanのスコープを"session"にする。Note
JPAで扱うEntityクラスをsessionスコープのBeanとして定義したい場合は、直接sessionスコープのBeanとして定義するのではなく、ラッパークラスを用意することを推奨する。
JPAで扱うEntityクラスをsessionスコープのBeanとして定義すると、JPAのAPIでsessionスコープのBeanを直接扱うことができない(直接あつかうと、エラーとなる)。 そのため、JPAで扱うことができるEntityオブジェクトへの変換処理が、必要になってしまう。
上記例では、CartというJPAのEntityクラスを、SessionCartというラッパークラスに包んで、sessionスコープのBeanとしている。 こうすることで、JPAで扱うことができるEntityオブジェクトへの変換処理が不要となるため、Controllerで行う処理がシンプルになる。
<context:component-scan base-package="xxx.yyy.zzz.app" scoped-proxy="targetClass" /> // (2)
項番 説明 (2) <context:component-scan> 要素のscoped-proxy属性に、 "targetClass" を指定し、scoped-proxyを有効にする。Note
scoped-proxyを有効化する理由について
sessionスコープのBeanをsingletonスコープのControllerにInjectするために、scoped-proxyを有効化する必要がある。
Bean定義ファイル(XML)に定義する方法を、以下に示す。
<beans:bean id="sessionCart" class="xxx.yyy.zzz.app.SessionCart" scope="session"> <!-- (3) --> <aop:scoped-proxy /> <!-- (4) --> </beans:bean>
項番 説明 (3) Beanのスコープを"session"にする。 (4) <aop:scoped-proxy /> 要素を指定し、scoped-proxyを有効にする。
@Inject SessionCart sessionCart; // (1) // (2) @ModelAttribute public SessionCart setUpSessionCart() { return sessionCart; } @RequestMapping(value = "add") public String addCart(@Validated ItemForm form, BindingResult result) { if (result.hasErrors()) { return "item/item"; } CartItem cartItem = beanMapper.map(form, CartItem.class); Cart addedCart = cartService.addCartItem(sessionCart.getCart(), // (3) cartItem); sessionCart.setCart(addedCart); // (4) return "redirect:/cart"; }
項番 説明 (1) sessionスコープのBeanを、ControllerにInjectする。 (2) View(JSP)から参照できるようにするために、Modelオブジェクトに、sessionスコープのBeanを追加する。 (3) sessionスコープのBeanのメソッド呼び出しを行うと、セッションに格納されているオブジェクトが返却される。セッションにオブジェクトが格納されていない場合は、新たに生成されたオブジェクトが返却され、セッションにも格納される。上記例では、カートに追加する前に在庫数などのチェックを行うため、Serviceのメソッドを呼び出している。 (4) 上記例では、CartServiceのaddCartItemメソッドの引数に渡したCartオブジェクトと、返り値で返却されるCartオブジェクトが、別のインスタンスになる可能性があるため、返却された Cart オブジェクトをsessionスコープのBeanに設定している。(2)で説明した処理によって、sessionスコープのBeanは、Modelオブジェクトに格納されているため、View(JSP)からも、CartServiceのaddCartItemメソッドから返却されたCartオブジェクトを参照することができる。
@Controller @RequestMapping("order") @SessionAttributes("scopedTarget.sessionCart") // (1) public class OrderController { @Inject SessionCart sessionCart; // ... @RequestMapping(method = RequestMethod.POST) public String order() { // ... return "redirect:/order?complete"; } @RequestMapping(params = "complete", method = RequestMethod.GET) public String complete(Model model, SessionStatus sessionStatus) { sessionStatus.setComplete(); // (2) model.addAttribute(sessionCart.getCart()); // (3) return "order/complete"; } }
項番 説明 (1) @SessionAttributesアノテーションのvalue属性に、sessionスコープのBeanの属性名を指定する。属性名は、"scopedTarget."+ Bean名 となる。 (2) SessionStatusオブジェクトの、setCompleteメソッドを呼び出す。上記例では、"scopedTarget.sessionCart"という属性名で格納されているオブジェクトが、セッションから削除される。 (3) View(JSP)にて、sessionスコープのBeanで保持しているオブジェクトを参照する必要がある場合は、View(JSP)で参照するオブジェクトを、Modelオブジェクトに格納する必要がある。
より具体的な実装例については、AppendixのsessionスコープのBeanを使った複数のControllerを跨いだ画面遷移の実装例を参照されたい。
共通ライブラリの詳細は、HttpSessionEventLoggingListenerを参照されたい。
JSPの暗黙オブジェクトである sessionScopeを使用する場合は、 pageディレクティブのsession属性の値を true にする必要がある。 ブランクプロジェクトから提供している include.jsp では、 false となっている。
include.jsp は、 src/main/webapp/WEB-INF/views/common ディレクトリに格納されている。
<%@ page session="true"%> <%-- (1) --%> <%-- omitted --%>
項番 説明 (1) pageディレクティブのsession属性の値を true にする。
以下のようなBeanPostProcessorを作成し、Bean定義することで実現できる。
package com.example.app.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; public class EnableSynchronizeOnSessionPostProcessor implements BeanPostProcessor { private static final Logger logger = LoggerFactory .getLogger(EnableSynchronizeOnSessionPostProcessor.class); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // NO-OP return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; logger.info("enable synchronizeOnSession => {}", adapter); adapter.setSynchronizeOnSession(true); // (1) } return bean; } }
項番 説明 (1) org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapterのsetSynchronizeOnSessionメソッドの引数に、trueを指定すると、同一セッション内でのリクエストが同期化される。
<bean class="com.example.app.config.EnableSynchronizeOnSessionPostProcessor" /> <!-- (2) -->
項番 説明 (2) (1)で作成した、BeanPostProcessor をBeanを定義する。
ウィザード形式の画面遷移を行う処理を例に、@SessionAttributesアノテーションを使った実装の説明を行う。
処理の仕様は、以下の通りとする。
基本的な画面遷移は、以下の通りとする。
実装例は、以下の通りである。
public class WizardForm implements Serializable { private static final long serialVersionUID = 1L; // (1) @NotEmpty(groups = { Wizard1.class }) private String field1; // (2) @NotEmpty(groups = { Wizard2.class }) private String field2; // (3) @NotEmpty(groups = { Wizard3.class }) private String field3; // ... // (4) public static interface Wizard1 { } // (5) public static interface Wizard2 { } // (6) public static interface Wizard3 { } }
項番 説明 (1) 1ページ目の入力画面で入力するフィールド。 (2) 2ページ目の入力画面で入力するフィールド。 (3) 3ページ目の入力画面で入力するフィールド。 (4) 1ページ目の入力画面で入力されるフィールドであることを示すための、検証グループインタフェース。 (5) 2ページ目の入力画面で入力されるフィールドであることを示すための、検証グループインタフェース。 (6) 3ページ目の入力画面で入力されるフィールドであることを示すための、検証グループインタフェース。Note
検証グループについて
画面遷移時の入力チェックでは、該当ページのフィールドのみチェックする必要がある。 Bean Validationでは、検証グループを表すクラス、またはインタフェースを設けることで、検証するルールをグループ化することができる。 今回の実装例のケースでは、画面毎に検証グループを用意することで、画面毎の入力チェックを実現している。
@Controller @RequestMapping("wizard") @SessionAttributes(types = { WizardForm.class, Entity.class }) // (7) public class WizardController { @Inject WizardService wizardService; @Inject Mapper beanMapper;
項番 説明 (7) 上記例では、フォームオブジェクト(WizardForm.class)と、エンティティ(Entity.class)のオブジェクトを、セッションに格納する。@ModelAttribute("wizardForm") // (8) public WizardForm setUpWizardForm() { return new WizardForm(); }
項番 説明 (8) 上記例では、セッションに格納するフォームオブジェクト(WizardForm)を生成している。 無駄なオブジェクトの生成をなくすために、@ModelAttributeアノテーションのvalue属性を指定している。// (9) @RequestMapping(value = "create", method = RequestMethod.GET) public String initializeCreateWizardForm(SessionStatus sessionStatus) { sessionStatus.setComplete(); return "redirect:/wizard/create?form1"; } // (10) @RequestMapping(value = "create", params = "form1") public String createForm1() { return "wizard/form1"; }
項番 説明 (9) 登録用入力画面を、初期表示するための処理メソッド。操作途中のオブジェクトが、セッションに格納されている可能性があるため、この処理メソッドで、セッションに格納されているオブジェクトを削除しておく。 (10) 1ページ目の登録用入力画面を、表示するための処理メソッド。// (11) @RequestMapping(value = "{id}/update", method = RequestMethod.GET) public String initializeUpdateWizardForm(@PathVariable("id") Integer id, RedirectAttributes redirectAttributes, SessionStatus sessionStatus) { sessionStatus.setComplete(); redirectAttributes.addAttribute("id", id); return "redirect:/wizard/{id}/update?form1"; } // (12) @RequestMapping(value = "{id}/update", params = "form1") public String updateForm1(@PathVariable("id") Integer id, WizardForm form, Model model) { Entity loadedEntity = wizardService.getEntity(id); beanMapper.map(loadedEntity, form); // (13) model.addAttribute(loadedEntity); // (14) return "wizard/form1"; }
項番 説明 (11) 更新用入力画面を、初期表示するための処理メソッド。 (12) 1ページ目の更新用入力画面を、表示するための処理メソッド。 (13) 取得したエンティティの状態をフォームオブジェクトに設定する。上記例では、DozerというBeanマッパーライブラリを使用している。 (14) 取得したエンティティを Model オブジェクトに追加し、セッションに格納する。上記例では、"entity"という属性名で、セッションに格納される。// (15) @RequestMapping(value = "save", params = "form2", method = RequestMethod.POST) public String saveForm2(@Validated(Wizard1.class) WizardForm form, // (16) BindingResult result) { if (result.hasErrors()) { return saveRedoForm1(); } return "wizard/form2"; } // (17) @RequestMapping(value = "save", params = "form3", method = RequestMethod.POST) public String saveForm3(@Validated(Wizard2.class) WizardForm form, // (18) BindingResult result) { if (result.hasErrors()) { return saveRedoForm2(); } return "wizard/form3"; } // (19) @RequestMapping(value = "save", params = "confirm", method = RequestMethod.POST) public String saveConfirm(@Validated(Wizard3.class) WizardForm form, // (20) BindingResult result) { if (result.hasErrors()) { return saveRedoForm3(); } return "wizard/confirm"; }
項番 説明 (15) 2ページ目の入力画面を、表示するための処理メソッド。 (16) 1ページ目の入力画面で入力された値のみ、入力チェックするために、@Validatedアノテーションのvalue属性に、1ページ目の入力画面の検証グループ(Wizard1.class)を指定する。 (17) 3ページ目の入力画面を、表示するための処理メソッド。 (18) 2ページ目の入力画面で入力された値のみ、入力チェックするために、@Validatedアノテーションのvalue属性に、2ページ目の入力画面の検証グループ(Wizard2.class)を指定する。 (19) 確認画面を表示するための処理メソッド。 (20) 3ページ目の入力画面で入力された値のみ、入力チェックするために、@Validatedアノテーションのvalue属性に、3ページ目の入力画面の検証グループ(Wizard3.class)を指定する。// (21) @RequestMapping(value = "save", method = RequestMethod.POST) public String save(@ModelAttribute @Validated({ Wizard1.class, Wizard2.class, Wizard3.class }) WizardForm form, // (22) BindingResult result, Entity entity, // (23) RedirectAttributes redirectAttributes) { if (result.hasErrors()) { throw new InvalidRequestException(result); // (24) } beanMapper.map(form, entity); entity = wizardService.saveEntity(entity); // (25) redirectAttributes.addFlashAttribute(entity); // (26) return "redirect:/wizard/save?complete"; } // (27) @RequestMapping(value = "save", params = "complete", method = RequestMethod.GET) public String saveComplete(SessionStatus sessionStatus) { sessionStatus.setComplete(); return "wizard/complete"; }
項番 説明 (21) 保存処理を実行するための処理メソッド。 (22) 入力画面で入力された値を全てチェックするために、@Validatedアノテーションのvalue属性に、各入力画面の検証グループインタフェース(Wizard1.class, Wizard2.class, Wizard3.class)を指定する。 (23) 保存するEntity.classのオブジェクトを取得する。登録処理の場合は、新たに生成されたオブジェクト、更新処理の場合は、(14)の処理でセッションに格納したオブジェクトが取得される。 (24) アプリケーションが提供しているボタンを使って、画面遷移を行っていれば、このタイミングでエラーは発生しないので、不正な操作が行われた場合にInvalidRequestExceptionがthrowされる。なお、InvalidRequestExceptionは共通ライブラリから提供している例外クラスではないため、別途作成する必要がある。 (25) 入力値が反映されたEntity.classのオブジェクトを保存する。 (26) リダイレクト先の処理メソッドで保存したEntity.classのオブジェクトを参照できるようにするために、Flashスコープに格納する。 (27) 完了画面を表示するための処理メソッド。// (28) @RequestMapping(value = "save", params = "redoForm1") public String saveRedoForm1() { return "wizard/form1"; } // (29) @RequestMapping(value = "save", params = "redoForm2") public String saveRedoForm2() { return "wizard/form2"; } // (30) @RequestMapping(value = "save", params = "redoForm3") public String saveRedoForm3() { return "wizard/form3"; } }
項番 説明 (28) 1ページ目の入力画面を、再表示するための処理メソッド。 (29) 2ページ目の入力画面を、再表示するための処理メソッド。 (30) 3ページ目の入力画面を、再表示するための処理メソッド。
@Controller @RequestMapping("wizard") @SessionAttributes(types = { WizardForm.class, Entity.class }) // (7) public class WizardController { @Inject EntityService wizardService; @Inject Mapper beanMapper; @ModelAttribute("wizardForm") // (8) public WizardForm setUpWizardForm() { return new WizardForm(); } // (9) @RequestMapping(value = "create", method = RequestMethod.GET) public String initializeCreateWizardForm(SessionStatus sessionStatus) { sessionStatus.setComplete(); return "redirect:/wizard/create?form1"; } // (10) @RequestMapping(value = "create", params = "form1") public String createForm1() { return "wizard/form1"; } // (11) @RequestMapping(value = "{id}/update", method = RequestMethod.GET) public String initializeUpdateWizardForm(@PathVariable("id") Integer id, RedirectAttributes redirectAttributes, SessionStatus sessionStatus) { sessionStatus.setComplete(); redirectAttributes.addAttribute("id", id); return "redirect:/wizard/{id}/update?form1"; } // (12) @RequestMapping(value = "{id}/update", params = "form1") public String updateForm1(@PathVariable("id") Integer id, WizardForm form, Model model) { Entity loadedEntity = wizardService.getEntity(id); beanMapper.map(loadedEntity, form); // (13) model.addAttribute(loadedEntity); // (14) return "wizard/form1"; } // (15) @RequestMapping(value = "save", params = "form2", method = RequestMethod.POST) public String saveForm2(@Validated(Wizard1.class) WizardForm form, // (16) BindingResult result) { if (result.hasErrors()) { return saveRedoForm1(); } return "wizard/form2"; } // (17) @RequestMapping(value = "save", params = "form3", method = RequestMethod.POST) public String saveForm3(@Validated(Wizard2.class) WizardForm form, // (18) BindingResult result) { if (result.hasErrors()) { return saveRedoForm2(); } return "wizard/form3"; } // (19) @RequestMapping(value = "save", params = "confirm", method = RequestMethod.POST) public String saveConfirm(@Validated(Wizard3.class) WizardForm form, // (20) BindingResult result) { if (result.hasErrors()) { return saveRedoForm3(); } return "wizard/confirm"; } // (21) @RequestMapping(value = "save", method = RequestMethod.POST) public String save(@ModelAttribute @Validated({ Wizard1.class, Wizard2.class, Wizard3.class }) WizardForm form, // (22) BindingResult result, Entity entity, // (23) RedirectAttributes redirectAttributes) { if (result.hasErrors()) { throw new InvalidRequestException(result); // (24) } beanMapper.map(form, entity); entity = wizardService.saveEntity(entity); // (25) redirectAttributes.addFlashAttribute(entity); // (26) return "redirect:/wizard/save?complete"; } // (27) @RequestMapping(value = "save", params = "complete", method = RequestMethod.GET) public String saveComplete(SessionStatus sessionStatus) { sessionStatus.setComplete(); return "wizard/complete"; } // (28) @RequestMapping(value = "save", params = "redoForm1") public String saveRedoForm1() { return "wizard/form1"; } // (29) @RequestMapping(value = "save", params = "redoForm2") public String saveRedoForm2() { return "wizard/form2"; } // (30) @RequestMapping(value = "save", params = "redoForm3") public String saveRedoForm3() { return "wizard/form3"; } }
<html> <head> <title>Wizard Form(1/3)</title> </head> <body> <h1>Wizard Form(1/3)</h1> <form:form action="${pageContext.request.contextPath}/wizard/save" modelAttribute="wizardForm"> <form:label path="field1">Filed1</form:label> : <form:input path="field1" /> <form:errors path="field1" /> <div> <form:button name="form2">Next</form:button> </div> </form:form> </body> </html>
<html> <head> <title>Wizard Form(2/3)</title> </head> <body> <h1>Wizard Form(2/3)</h1> <%-- (31) --%> <form:form action="${pageContext.request.contextPath}/wizard/save" modelAttribute="wizardForm"> <form:label path="field2">Filed2</form:label> : <form:input path="field2" /> <form:errors path="field2" /> <div> <form:button name="redoForm1">Back</form:button> <form:button name="form3">Next</form:button> </div> </form:form> </body> </html>
項番 説明 (31) フォームオブジェクトをセッションに格納しているため、1ページ目の入力画面のフィールドを、hidden項目にする必要はない。
<html> <head> <title>Wizard Form(3/3)</title> </head> <body> <h1>Wizard Form(3/3)</h1> <%-- (32) --%> <form:form action="${pageContext.request.contextPath}/wizard/save" modelAttribute="wizardForm"> <form:label path="field3">Filed3</form:label> : <form:input path="field3" /> <form:errors path="field3" /> <div> <form:button name="redoForm2">Back</form:button> <form:button name="confirm">Confirm</form:button> </div> </form:form> </body> </html>
項番 説明 (32) フォームオブジェクトをセッションに格納しているため、1ページ目と2ページ目の入力画面のフィールドを、hidden項目にする必要はない。
<html> <head> <title>Confirm</title> </head> <body> <h1>Confirm</h1> <%-- (33) --%> <form:form action="${pageContext.request.contextPath}/wizard/save" modelAttribute="wizardForm"> <div> Filed1 : ${f:h(wizardForm.field1)} </div> <div> Filed2 : ${f:h(wizardForm.field2)} </div> <div> Filed3 : ${f:h(wizardForm.field3)} </div> <div> <form:button name="redoForm3">Back</form:button> <form:button>OK</form:button> </div> </form:form> </body> </html>
項番 説明 (33) フォームオブジェクトをセッションに格納しているため、入力画面のフィールドを、hidden項目にする必要はない。
<html> <head> <title>Complete</title> </head> <body> <h1>Complete</h1> <div> <div> ID : ${f:h(entity.id)} </div> <div> Filed1 : ${f:h(entity.field1)} </div> <div> Filed2 : ${f:h(entity.field2)} </div> <div> Filed3 : ${f:h(entity.field3)} </div> </div> <div> <a href="${pageContext.request.contextPath}/wizard/create"> Continue operation of Create </a> </div> <div> <a href="${pageContext.request.contextPath}/wizard/${entity.id}/update"> Continue operation of Update </a> </div> </body> </html>
<bean class="org.terasoluna.gfw.web.exception.SystemExceptionResolver"> <property name="exceptionCodeResolver" ref="exceptionCodeResolver" /> <!-- ... --> <property name="exceptionMappings"> <map> <!-- ... --> <entry key="InvalidRequestException" value="common/error/operationError" /> <!-- (34) --> </map> </property> <property name="statusCodes"> <map> <!-- ... --> <entry key="common/error/operationError" value="400" /> <!-- (35) --> </map> </property> <!-- ... --> </bean>
項番 説明 (34) 共通ライブラリから提供しているSystemExceptionResolverのexceptionMappingsに、保存処理実行時に不正なリクエストを検知したことを、通知する例外InvalidRequestExceptionの、例外ハンドリングの定義を追加する。上記例では、 例外発生時の遷移先として、/WEB-INF/views/common/error/operationError.jspを指定している。 (35) SystemExceptionResolverのstatusCodes に、HttpSessionRequiredException発生時のHTTPレスポンスコードを指定する。上記例では、 例外発生時のHTTPレスポンスコードとして、Bad Request(400)を指定している。
<bean id="exceptionCodeResolver" class="org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver"> <!-- Setting and Customization by project. --> <property name="exceptionMappings"> <map> <!-- ... --> <entry key="InvalidRequestException" value="w.xx.0004" /> <!-- (36) --> </map> </property> <property name="defaultExceptionCode" value="e.xx.0001" /> <!-- (37) --> </bean>
項番 説明 (36) 共通ライブラリから提供しているSimpleMappingExceptionCodeResolverのexceptionMappingsに、InvalidRequestExceptionの例外ハンドリングの定義を追加する。上記例では、 例外発生時の例外コードとして、"w.xx.0004"を指定している。この設定を追加しない場合は、デフォルトの例外コードが、ログに出力される。 (37) 例外発生時のデフォルトの例外コード。
複数のControllerをまたいで画面遷移を行う処理を例に、sessionスコープのBeanを使った実装の説明を行う。
処理の仕様は、以下の通りとする。
画面遷移は、以下の通りとする。
実装例は、以下の通りである。
@Component @Scope("session") public class SessionCart implements Serializable { private static final long serialVersionUID = 1L; private Cart cart; // (1) public Cart getCart() { if (cart == null) { cart = new Cart(); } return cart; } public void setCart(Cart cart) { this.cart = cart; } }
項番 説明 (1) CartというEntity(Domainオブジェクト)をラップしている。
@Controller @RequestMapping("item") public class ItemController { @Inject SessionCart sessionCart; @Inject CartService cartService; @Inject Mapper beanMapper; @ModelAttribute public ItemForm setUpItemForm() { return new ItemForm(); } // (2) @RequestMapping public String view(Model model) { return "item/item"; } // (3) @RequestMapping(value = "add") public String addCart(@Validated ItemForm form, BindingResult result) { if (result.hasErrors()) { return "item/item"; } CartItem cartItem = beanMapper.map(form, CartItem.class); Cart cart = cartService.addCartItem(sessionCart.getCart(), // (4) cartItem); sessionCart.setCart(cart); // (5) return "redirect:/cart"; // (6) } }
項番 説明 (2) 商品画面を、表示するための処理メソッド。 (3) 指定された商品を、カートに追加するための処理メソッド。 (4) セッションに格納されているCartオブジェクトを、Serviceのメソッドに渡す。 (5) Serviceのメソッドから返却されたCartオブジェクトを、sessionスコープのBeanに反映する。sessionスコープのBeanに反映することで、セッションおよびModelオブジェクトに反映される。 (6) 商品をカートに追加した後に、カート画面を表示するためのリクエストに、リダイレクトする。別Controllerの画面に遷移する場合は、直接View(JSP)を呼び出すのではなく、画面を表示するためのリクエストにリダイレクトすることを推奨する。
@Controller @RequestMapping("cart") public class CartController { @Inject SessionCart sessionCart; @Inject CartService cartService; @Inject Mapper beanMapper; @ModelAttribute public CartForm setUpCartForm() { return new CartForm(); } // (7) @ModelAttribute("sessionCart") public SessionCart setUpSessionCart() { return sessionCart; } // (8) @RequestMapping public String cart(CartForm form) { beanMapper.map(sessionCart.getCart(), form); return "cart/cart"; } // (9) @RequestMapping(params = "edit", method = RequestMethod.POST) public String edit(@Validated CartForm form, BindingResult result, Model model) { if (result.hasErrors()) { return "cart/cart"; } Cart cart = sessionCart.getCart(); Iterator<CartItemForm> itemForm = form.getCartItems().iterator(); for (CartItem item : cart.getCartItems()) { beanMapper.map(itemForm.next(), item); } cart = cartService.saveCart(cart); sessionCart.setCart(cart); // (10) return "redirect:/cart"; // (11) } }
項番 説明 (7) View(JSP)で参照するために、Modelオブジェクトに追加する。 (8) カート画面(数量変更画面)を表示するための処理メソッド。 (9) 数量変更を、行うための処理メソッド。 (10) Serviceのメソッドから返却されたCartオブジェクトをsessionスコープのBeanに反映する。sessionスコープのBeanに反映することで、セッションおよびModelオブジェクトに反映される。 (11) 数量変更を行った後に、カート画面(数量変更画面)を表示するためのリクエストに、リダイレクトする。更新処理を行った場合は、直接View(JSP)を呼び出すのではなく、画面を表示するためのリクエストにリダイレクトすることを推奨する。
@Controller @RequestMapping("order") @SessionAttributes("scopedTarget.sessionCart") public class OrderController { @Inject SessionCart sessionCart; @ModelAttribute public OrderForm setUpOrderForm() { return new OrderForm(); } // (12) @ModelAttribute("sessionCart") public SessionCart setUpSessionCart() { return sessionCart; } // (13) @RequestMapping public String view() { return "order/order"; } // (14) @RequestMapping(method = RequestMethod.POST) public String order() { // ... return "redirect:/order?complete"; } // (15) @RequestMapping(params = "complete", method = RequestMethod.GET) public String complete(Model model, SessionStatus sessionStatus) { sessionStatus.setComplete(); return "order/complete"; } }
項番 説明 (12) View(JSP)で参照するために、Modelオブジェクトに追加する。 (13) 注文画面を、表示するための処理メソッド。 (14) 注文処理を行うための処理メソッド。 (15) 注文完了画面を表示するための処理メソッド。
<html> <head> <title>Item</title> </head> <body> <h1>Item</h1> <form:form action="${pageContext.request.contextPath}/item/add" modelAttribute="itemForm"> <form:label path="itemCode">Item Code</form:label> : <form:input path="itemCode" /> <form:errors path="itemCode" /> <br> <form:label path="quantity">Quantity</form:label> : <form:input path="quantity" /> <form:errors path="quantity" /> <div> <%-- (15) --%> <form:button>Add</form:button> </div> </form:form> <div> <a href="${pageContext.request.contextPath}/cart">Go to Cart</a> </div> </body> </html>
項番 説明 (15) 商品を追加するためのボタン。
<html> <head> <title>Cart</title> </head> <body> <h1>Cart</h1> <c:choose> <c:when test="${ empty sessionCart.cart.cartItems }"> <div>Cart is empty.</div> </c:when> <c:otherwise> CART ID : ${f:h(sessionCart.cart.id)} <form:form modelAttribute="cartForm"> <table border="1"> <thead> <tr> <th>ID</th> <th>ITEM CODE</th> <th>QUANTITY</th> </tr> </thead> <tbody> <c:forEach var="item" items="${sessionCart.cart.cartItems}" varStatus="rowStatus"> <tr> <td>${f:h(item.id)}</td> <td>${f:h(item.itemCode)}</td> <td> <form:input path="cartItems[${rowStatus.index}].quantity" /> <form:errors path="cartItems[${rowStatus.index}].quantity" /> </td> </tr> </c:forEach> </tbody> </table> <%-- (16) --%> <form:button name="edit">Save</form:button> </form:form> </c:otherwise> </c:choose> <c:if test="${ not empty sessionCart.cart.cartItems }"> <div> <%-- (17) --%> <a href="${pageContext.request.contextPath}/order">Go to Order</a> </div> </c:if> <div> <a href="${pageContext.request.contextPath}/item">Back to Item</a> </div> </body> </html>
項番 説明 (16) 数量を更新するためのボタン。 (17) 注文画面を表示するためのリンク。
<html> <head> <title>Order</title> </head> <body> <h1>Order</h1> <table border="1"> <thead> <tr> <th>ID</th> <th>ITEM CODE</th> <th>QUANTITY</th> </tr> </thead> <tbody> <c:forEach var="item" items="${sessionCart.cart.cartItems}" varStatus="rowStatus"> <tr> <td>${f:h(item.id)}</td> <td>${f:h(item.itemCode)}</td> <td>${f:h(item.quantity)}</td> </tr> </c:forEach> </tbody> </table> <form:form modelAttribute="orderForm"> <%-- (18) --%> <form:button>Order</form:button> </form:form> <div> <a href="${pageContext.request.contextPath}/cart">Back to Cart</a> </div> <div> <a href="${pageContext.request.contextPath}/item">Back to Item</a> </div> </body> </html>
項番 説明 (18) 注文するためのボタン。
<html> <head> <title>Order Complete</title> </head> <body> <h1>Order Complete</h1> ORDER ID : ${f:h(order.id)} <table border="1"> <thead> <tr> <th>ID</th> <th>ITEM CODE</th> <th>QUANTITY</th> </tr> </thead> <tbody> <c:forEach var="item" items="${order.orderItems}" varStatus="rowStatus"> <tr> <td>${f:h(item.id)}</td> <td>${f:h(item.itemCode)}</td> <td>${f:h(item.quantity)}</td> </tr> </c:forEach> </tbody> </table> <br> <div> <a href="${pageContext.request.contextPath}/item">Back to Item</a> </div> </body> </html>