2012年3月26日月曜日

データ(オブジェクト)共有を伴うリダイレクト


他の Web アプリケーションと同様、Spring MVC Framework を使ったアプリケーションでも、フォームに入力されたデータをリダイレクト先に引き継がせたいときがあります。また、共通のアプリケーションを利用するブラウザー間でデータを共有したいときがあります。

そこで今回は、ServletContext および HttpSession、そして redirect: プリフィックを組み合わせて『データ共有を伴うリダイレクト』の仕掛けを考えてみようと思います。

リダイレクトなし
まずは実験の土台となる簡単なコントローラーを作ります。以下のコードには、2種類の index メソッドがあります。リクエストメソッド(GET/POST)に応じて処理を振り分けるよう @RequestMapping アノテーションで指示しています。

WelcomeController.java
package wrider;

import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class WelcomeController {

 @RequestMapping(value ="welcome/index", method = RequestMethod.GET)
 public ModelAndView index() {
  
  Map<String, Object> model = new HashMap<String, Object>();
  model.put("pageTitle", "Welcome to WriDer's Site");
  
  ModelAndView modelAndView = new ModelAndView();
  modelAndView.setViewName("welcome/index");
  modelAndView.addAllObjects(model);
  
  return modelAndView;
 }
 
 @RequestMapping(value ="welcome/index", method = RequestMethod.POST)
 public ModelAndView index(HttpServletRequest req, HttpServletResponse res) {
  
  this.reqParamList(req);
  
  Map<String, Object> model = new HashMap<String, Object>();
  model.put("pageTitle", "I'm Postman");
  
  ModelAndView modelAndView = new ModelAndView();
  modelAndView.setViewName("welcome/index");
  modelAndView.addAllObjects(model);
  
  return modelAndView;
 }
 
 private void reqParamList(HttpServletRequest req) {
  System.out.println("[" + req.getRequestURI() + "]Method: " + req.getMethod());
  
  String pname = null;
  for (Object obj : Collections.list(req.getParameterNames())) {
   pname = obj.toString();
   System.out.println(pname + ": " + req.getParameter(pname));
  }
 }
}

index.jsp (抜粋)
<body>
 <h1>WriDer's Demo Site</h1>
 <form:form action="index.html" method="POST">
  <input type="text" name="anyText"/>
  <input type="submit" value="決定"/>
 </form:form>
 <h2>POSTed data: <%= request.getParameter("anyText") %> </h2>
</body>

テキストボックスに適当な文字列を入力して決定ボタンを押します。

画面

コンソールの出力
[/ishtar/welcome/index.html]Method: POST
anyText: 難しいことは後回し

redirect: でリダイレクト
次に、POST リクエストで実行される index メソッドにある setViewName(..) の箇所を以下のように変更し、新たに catcher メソッドを追加します。redirect: プリフィックスは、InternalResourceViewResolver のスーパークラスである UrlBasedViewResolver でサポートされている機能です。

WelcomeController.java (抜粋)
 @RequestMapping(value ="welcome/index", method = RequestMethod.POST)
 public ModelAndView index(HttpServletRequest req, HttpServletResponse res) {
    :
  ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("redirect:catcher.html");
  modelAndView.addAllObjects(model);
  
  return modelAndView;
 }
 
 @RequestMapping("welcome/catcher")
 public ModelAndView catcher(HttpServletRequest req, HttpServletResponse res) {
  
  this.reqParamList(req);
  
  Map<String, Object> model = new HashMap<String, Object>();
  model.put("pageTitle", "I'm Catcher");
  
  ModelAndView modelAndView = new ModelAndView();
  modelAndView.setViewName("welcome/catcher");
  modelAndView.addAllObjects(model);
  
  return modelAndView;
 }

画面

コンソールの出力
[/ishtar/welcome/index.html]Method: POST
anyText: 難しいことは後回し
[/ishtar/welcome/catcher.html]Method: GET
pageTitle: I'm Postman

コンソールの出力が示す通り、まず index メソッドが POST で呼び出された後、catcher メソッドにリダイレクトされます。

この場合の特徴的な挙動として...
  • catcher メソッドは GET リクエストで呼び出される。
  • index メソッド内で modelAndView オブジェクトに addAllObjects() で追加したアトリビュート、この場合は model オブジェクトに put した pageTitle が、リダイレクト先である /welcome/catcher.html へのクエリーパラメーターとして渡されている。
  • catcher メソッドが受け取ったパラメーターの中に、最初に POST して anyText は含まれていない。
POST データを引き継ぐ処理を何も行っていないので当然の結果です。

ServletContext と HttpSession
ServletContext にバインドされたアトリビュート(名前付きのオブジェクト)は、同じ Web アプリケーションに対するすべてのリクエスト間で共有できます。また、HttpSession にバインドされたアトリビュートは、同一ブラウザーのセッション間で共有できます。前者は一般に“Application Scope”、後者は“Session Scope”と呼ばれています。これらの仕組みを使って書き換えたのが以下のコードです。

WelcomeController.java (抜粋)
 @RequestMapping(value ="welcome/index", method = RequestMethod.POST)
 public ModelAndView index(HttpServletRequest req, HttpServletResponse res) {
  
  this.reqParamList(req);
  
  Map<String, Object> model = new HashMap<String, Object>();
  /*
   * ServletContext を介したデータの共有 - Applicationスコープ
   */
  req.getServletContext().setAttribute("appScopeText", req.getParameter("anyText"));
  /*
   * HttpSession を介したデータの共有 - Sessionスコープ
   */
  req.getSession().setAttribute("sessScopeText", req.getParameter("anyText"));
  
  ModelAndView modelAndView = new ModelAndView();
  modelAndView.setViewName("redirect:catcher.html");
  modelAndView.addAllObjects(model);
  
  return modelAndView;
 }
 
 @RequestMapping("welcome/catcher")
 public ModelAndView catcher(HttpServletRequest req, HttpServletResponse res) {
  
  this.reqParamList(req);
  
  Map<String, Object> model = new HashMap<String, Object>();
  model.put("pageTitle", "I'm Catcher");
  /*
   * ServletContext を介したデータの共有 - Applicationスコープ
   */
  model.put("appScopeText", req.getServletContext().getAttribute("appScopeText"));
  /*
   * HttpSession を介したデータの共有 - Sessionスコープ
   */
  model.put("sessScopeText", req.getSession().getAttribute("sessScopeText"));
  
  ModelAndView modelAndView = new ModelAndView();
  modelAndView.setViewName("welcome/catcher");
  modelAndView.addAllObjects(model);
  
  return modelAndView;
 }

index.jsp と catcher.jsp (抜粋)
  :
 <h2>POSTed data: <%= request.getParameter("anyText") %> </h2>
 <h2>Application Scope: ${appScopeText}</h2>
 <h2>HttpSession Scope: ${sessScopeText}</h2>
</body>

画面(Chrome)

コンソールの出力
[/ishtar/welcome/index.html]Method: POST
anyText: 難しいことは後回し
[/ishtar/welcome/catcher.html;jsessionid=2F3..]Method: GET

リダイレクト先である catcher メソッドが受け取ったリクエストパラメーターは null です。しかし、ServletContext と HttpSession(※HttpServletRequest.getSession() として取得)に setAttribute(..)でバインドしたオブジェクト――この場合は appScopeText, sessScopeText という名前のString データ――は、ちゃんとリダイレクト先と共有されています。

また、コンソールの出力、あるいは画面のアドレスバーを見ると jsessionid というセッション ID が発行されていることがわかります。

この状態で、異なるブラウザーを立ち上げて index.html にアクセスすると...

画面(Opera)

先のアクセスで ServletContext にバインドした appScopeText の内容が表示されています。一方、HttpSession には sessScopeText はバインドされていません。

このフォームに適当な文字列を入力し送信ボタンを押すと、catcher.html にリダイレクトされ、画面には「Application Scope: とにかく動かす」「HttpSession Scope: とにかく動かす」と表示されます。

コンソールの出力は以下の通り。このブラウザーとのセッション用にセッション ID(jsessionid)が発行されています。

コンソールの出力
[/ishtar/welcome/index.html]Method: POST
anyText: とにかく動かす
[/ishtar/welcome/catcher.html;jsessionid=701..]Method: GET


そして Chrome に戻り、リロードした際の画面が下図です。

画面(Chrome)

コンソールの出力
[/ishtar/welcome/catcher.html;jsessionid=2F3..]Method: GET

「HttpSession Scope: ..」は変わりませんが、「Application Scope: ..」の箇所は更新されました。これは、後のブラウザーからのリクエストに応じて ServletContext 内のアトリビュート(appScopeText)が setAttribute(..)で更新されたからです。

因みにアドレスバーにある jsessionid=.. は、Cookie としてブラウザーに一時保存されるため、下の「コンソールの出力」が示す通り、以降は無くてもかまいません。

コンソールの出力(Chrome)
[/ishtar/welcome/index.html]Method: POST
anyText: Chromeにポスト
[/ishtar/welcome/catcher.html]Method: GET

コンソールの出力(Opera)
[/ishtar/welcome/index.html]Method: POST
anyText: Operaにポスト
[/ishtar/welcome/catcher.html]Method: GET


Servlet の Application Scope と Session Scope、そして Spring が提供する redirect: プリフィックスを組み合わせると、色々と面白いことができそうです。

0 件のコメント:

コメントを投稿