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>
<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;
}
}
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!");
}
}
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
-----------------------------------
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
-----------------------------------
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 {
:
}
:
@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
-----------------------------------
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
-----------------------------------
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
-----------------------------------
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 件のコメント:
コメントを投稿