2012年3月30日金曜日

ModelAttribute と SessionAttributes



Spring Framework には ModelFactory というクラスがあります。API ドキュメントを読むと、Model の初期化やアップデート(セッションとモデルアトリビュートの同期)を行うクラスである旨が書かれています。

そこで今回は、@ModelAttribute と @SessionAttributes の作用を調べてみます。

login.jsp(抜粋)
メールアドレスとパスワードを入力する単純なフォームです。<form:form>タグは Spring Framewok が提供するタグライブラリーで modelAttribute という属性で、フォームオブジェクトの共有に使うモデルアトリビュート名(idCard)を設定しています。
<body>
<h1>WriDer's Demo Site</h1>
<form:form action="login.html" method="POST" modelAttribute="idCard">
<label for="email">メールアドレス</label>
<input type="text" id="email" name="email" value="${idCard.email}"/><br />
<label for="password">パスワード</label>
<input type="password" id="password" name="password" value="${idCard.password}"/><br />
<input type="submit" value="決定"/>
<h3>POSTed email: ${idCard.email}</h3>
<h3>POSTed password: ${idCard.password}</h3>
</form:form>
</body>

IdCard.java
上記フォームの入力データを格納する IdCard クラスです。2つのプロパティ(email と password)と、それらのセッター/ゲッターのみです。
package wrider.model;

public class IdCard {

private String email;

public String getEmail() {
return this.email;
}

public void setEmail(String email) {
this.email = email;
}

private String password;

public String getPassword() {
return this.password;
}

public void setPassword(String password) {
this.password = password;
}

}

AccountController.java
フォームの表示と入力データの処理を行うコントローラークラスです。GET リクエストの時は /WEB-INF/jsp/account/login.jsp を表示するだけです。POST リクエストの時は、login メソッドの引数として Spring から受け取った BindingResult 中のモデルマップを getModel() で取り出し putAll()ModelAndView に格納しています(コード中の青字の部分)。

makeIdCard は、上記 IdCard クラスの新しいインスタンスを生成するメソッドで、@ModelAttribute アノテーションを付けています(赤字の部分)。

各メソッドが呼び出された順番、BindingResult を介して引き継がれたモデルマップの key と value のリストをコンソールに表示するようにしています(青字の部分)。
package wrider.controller;

import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import wrider.model.IdCard;

@Controller
public class AccountController {
 
 private int testCounter = 0;
 
 @ModelAttribute("idCard")
 public IdCard makeIdCard() {
  printOrder("makeIdCard");
  return new IdCard();
 }
 
 @RequestMapping(value="/account/login.html", method=RequestMethod.GET)
 public String login() {
  printOrder("login[GET]");
  return "account/login";
 }
 
 @RequestMapping(value="/account/login.html", method=RequestMethod.POST)
 public ModelAndView login(IdCard idCard, BindingResult br) {
  printOrder("login[POST]");
  
  System.out.println(idCard.getEmail());
  
  System.out.println("BindingResult has ..");
  for (Map.Entry<String, Object> entry : br.getModel().entrySet()) {
   System.out.println(" key: " + entry.getKey());
   System.out.println(" value: " + entry.getValue().toString());
   System.out.println(" -----------------------------------");
  }
  

  ModelAndView mav = new ModelAndView();
  mav.getModel().putAll(br.getModel());
  mav.setViewName("account/login");
  return mav;
 }
 
 
 private void printOrder(String methodName) {
  this.testCounter++;
  System.out.println(this.testCounter + ": " + methodName + " has been invoked!");
 }

}

@ModelAttribute の作用
ブラウザー(ブラウザーA)で [servlet-name]/account/login.html にアクセスすると、まず @ModelAttribute アノテーションが付いている makeIdCard メソッドが実行され、GET リクエスト用の login メソッドが実行されています(1~2)。

フォームにデータを入力して submit すると、再び makeIdCard メソッドが呼び出された後、今度は POST リクエスト用の login メソッドが実行されています(3~4)。

コンソールの出力(ブラウザーA)
1: makeIdCard has been invoked!
2: login[GET] has been invoked!
3: makeIdCard has been invoked!
4: login[POST] has been invoked!
wrider@abc.com
BindingResult has ..
key: idCard
value: wrider.model.IdCard@1160fa6
-----------------------------------
key: org.springframework.validation.BindingResult.idCard
value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
-----------------------------------

続いて、別のブラウザー(ブラウザーB)を立ち上げて同様にアクセスします。やはり、GET/POST に関係なく毎回 @ModelAttribute メソッドが呼び出され、その度に新しい IdCard インスタンスが生成されています。

コンソールの出力(ブラウザーB)
5: makeIdCard has been invoked!
6: login[GET] has been invoked!
7: makeIdCard has been invoked!
8: login[POST] has been invoked!
picboo@def.com
BindingResult has ..
key: idCard
value: wrider.model.IdCard@1acbf5c
-----------------------------------
key: org.springframework.validation.BindingResult.idCard
value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
-----------------------------------


@SessionAttributes の作用
では、コントローラークラスに以下のように @SessionAttributes アノテーションを付けてみます。

package wrider.controller;
  :
@Controller
@SessionAttributes("idCard")
public class AccountController {
  :
}

すると、@ModelAttribute メソッドが呼び出されるのは、ブラウザーとのセッションが確立した最初だけになります。また、ブラウザーの Cookie を見ると jsessionid が発行されていることが確認できます。

コンソールの出力(ブラウザーA)
1: makeIdCard has been invoked!
2: login[GET] has been invoked!
3: login[POST] has been invoked!
wrider@abc.com
BindingResult has ..
key: idCard
value: wrider.model.IdCard@1bb9805
-----------------------------------
key: org.springframework.validation.BindingResult.idCard
value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
-----------------------------------

コンソールの出力(ブラウザーB)
4: makeIdCard has been invoked!
5: login[GET] has been invoked!
6: login[POST] has been invoked!
picboo@def.com
BindingResult has ..
key: idCard
value: wrider.model.IdCard@ea7211
-----------------------------------
key: org.springframework.validation.BindingResult.idCard
value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
-----------------------------------

以降は、以下のコンソール出力が示す通り POST リクエストに応じて login メソッドが実行されるだけです。BindingResult から取り出した IdCard オブジェクト(key: idCard)の参照 ID は、ブラウザー A が“IdCard@1bb9805”、ブラウザー B が“IdCard@ea7211”と、それぞれ先のアクセス(上記コンソール出力の 3 及び 6)の際の参照 ID と同じです。

一方、@SessionAttributes アノテーションを付加しなかった最初のコードでは、リクエストの度にこの参照 ID が変化しました。

コンソールの出力(ブラウザーA, B)
7: login[POST] has been invoked!
hismail@cba.com
BindingResult has ..
key: idCard
value: wrider.model.IdCard@1bb9805
-----------------------------------
key: org.springframework.validation.BindingResult.idCard
value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
-----------------------------------
8: login[POST] has been invoked!
hermail@fed.com
BindingResult has ..
key: idCard
value: wrider.model.IdCard@ea7211
-----------------------------------
key: org.springframework.validation.BindingResult.idCard
value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
-----------------------------------

まとめ
オブジェクトの生成、初期化などを行うメソッドに @ModelAttribute アノテーションを付けておけば、コントローラーメソッドが呼び出される前に実行してくれます。

一方、コントローラーのクラス定義に @SessionAttributes アノテーションを付けることで、セッションに付随するアトリビュートとして、指定したモデルアトリビュート(のリスト)を引き継ぐことができます。

ただし、SessionAttributes のドキュメントには、「認証オブジェクトのようなパーマネントなセッションアトリビュートには、伝統的な session.setAttribute メソッドを使え」と注意書きが記されています。この session.setAttribute を使った方法は前回の『データ(オブジェクト)共有を伴うリダイレクト』で触れています。

0 件のコメント:

コメントを投稿