アクションからアクションにフォワードした際のrequestスコープのActionFormについて

場合によってはアクションからアクションにフォワードしたい場合がある.で,この際にrequestスコープのActionFormがどうなるかというのを,はっきりさせてみた.なお,ここではStruts-1.1で実験した.Struts-1.0.2とは動きが割と異なるようなので注意.
問題
確認用サンプル
requestスコープの範囲
ActionFormの生成
ActionFormの初期化
異なるActionFormを持つActionへのフォワード
ActionFormの引渡し
まとめ

問題

struts-config.xmlのaction-mappingsが以下だったとする.
  <action-mappings>
    <action    path="/Action1"
               type="matsu.struts.sample.Action1"
	       scope="request"
	       name="Type1Form">
      <forward name="success"        path="/Out.do" />
    </action>
    <action    path="/Action2"
               type="matsu.struts.sample.Action2"
	       scope="request"
	       name="Type2Form">
      <forward name="success"              path="/Out.do" />
    </action>
    <action    path="/Action3"
               type="matsu.struts.sample.Action3"
	       scope="request"
	       name="Type2Form">
      <forward name="success"              path="/Out.do" />
    </action>
    <action    path="/Out"
               type="matsu.struts.sample.OutAction" 
	       scope="request"
	       name="Type1Form">
      <forward name="success"              path="/out.jsp" />
    </action>
</action-mappings>
4つのアクションがあり,Action1,2,3からOutへフォワードする.いずれのアクションもrequestスコープのActionFormである.これをもとにWebアプリケーションを作成して,以下を確認する.
requestスコープの範囲
JSPへフォワードする際と同様,HTTPセッションと同じ範囲と考えてよいと思うが,念のため.
ActionFormの生成
上と被るような気がするが,フォワード元とフォワード先が同じクラスだった場合に,ActionFormは同じインスタンスが使用されるのか?それともAction毎にnewされるのか?
ActionFormの初期化
HTTPリクエストの値をActionFormに詰め込む作業の確認.
異なるActionFormを持つActionへのフォワード
フォワード前のActionのActionFormインスタンスはフォワード後どうなる?
ActionFormの引渡し
フォワード前のActionでフォワード後のActionをnewして設定などしてフォワード後のActionに渡すことができるのか?

確認用サンプル

確認のためにサンプルを作成してみた.これ.ビルド方法はだいたい以下.
展開
ant init
libディレクトリの下にstrutsのjarファイルを置く.動作確認時は以下を置いた.
commons-beanutils.jar commons-lang.jar struts-legacy.jar
commons-collections.jar commons-logging.jar struts.jar
commons-digester.jar commons-validator.jar
commons-fileupload.jar jakarta-oro.jar
src/WEB-INFの下にstrutsのタグリブを置く.動作確認時は以下を置いた.
struts-form.tld struts-template.tld
struts-html.tld struts-tiles.tld
struts-bean.tld struts-logic.tld
struts.tld struts-nested.tld
ant warを実行すると,warファイルができる.

requestスコープの範囲

結論から言うと,servletと同様,HTTPセッションの範囲と同じである.すなわちHTTPリクエストを受けてからレスポンスを返すまでであり,HttpServletRequestの生存期間と一致する…というのは,ActionFormのインスタンスはrequestスコープならHttpServletRequest,それ以外ならHttpSessionにsetAttributeされる.以下はStrutsのソースから.
ActionForm instance = null;
HttpSession session = null;
if (“request”.equals(mapping.getScope())) {
instance = (ActionForm) request.getAttribute(attribute);
} else {
session = request.getSession();
instance = (ActionForm) session.getAttribute(attribute);
}

ActionFormの生成

結論から言うと,フォワード元とフォワード先が同じクラスだった場合に,ActionFormは同じインスタンスが使用される.ただし,各Actionのexecuteを実行する前に,毎回ActionFormを初期化する.サンプルでこれを確認できる.まずAction1.

package matsu.struts.sample;

import org.apache.struts.action.*;
import javax.servlet.http.*;

public class Action1 extends Action{
    public ActionForward execute(ActionMapping mapping,
				 ActionForm form,
				 HttpServletRequest request,
				 HttpServletResponse response)
	throws Exception
    {
	System.out.println("===== Action1 execute START =====");
	Type1Form type1Form = (Type1Form)form;
	System.out.println("GET Type1Form message : \"" + type1Form.getMessage() + "\"");
	type1Form.setMessage("Action1 checked. "+type1Form.getMessage());
	type1Form.setMessage2("Not over write");
	System.out.println ("from request : " +
			    request.getAttribute("Type1Form"));
	System.out.println ("parameter form : " + form);
	System.out.println("===== Action1 execute END =====");
        return (mapping.findForward("success"));
    }
}

そしてOut.

package matsu.struts.sample;

import org.apache.struts.action.*;
import javax.servlet.http.*;
import java.util.*;

public class OutAction extends Action{
    public ActionForward execute(ActionMapping mapping,
				 ActionForm form,
				 HttpServletRequest request,
				 HttpServletResponse response)
	throws Exception
    {
	System.out.println("===== OutAction execute START =====");
	Type1Form type1Form = (Type1Form)form;
	System.out.println("GET type1Form message : \"" + type1Form.getMessage() + "\"");
	type1Form.setMessage("OutAction checked. "+type1Form.getMessage());
	System.out.println ("parameter form : " + form);

	Enumeration enum = request.getSession().getAttributeNames();
	while (enum.hasMoreElements()) {
	    System.out.println("Session Attribute Names : " + enum.nextElement());
	}

	enum = request.getAttributeNames();
	while (enum.hasMoreElements()) {
	    String name = enum.nextElement().toString();
	    System.out.println("Request Attribute Names  : " + name);
	    System.out.println("                  Values : " + request.getAttribute(name));
	}

	System.out.println("===== OutAction execute END =====");
        return (mapping.findForward("success"));
    }

}

さらにType1Form.

package matsu.struts.sample;

import org.apache.struts.action.*;

/**
 * ActionFormBean.
 * このクラスはBeanであり,すべてのフィールドに対する
 * アクセサ(getterとsetter)を実装する.
 */
public class Type1Form extends ActionForm{
    private static int no = 0;
    private String message;
    private String message2;
    private int myno;

    public Type1Form () {
	super();
	myno = ++no;
	System.out.println("Type1Form created : " + myno);
    }

    public void setMessage(String msg){
	System.out.println("Type1Form " + myno + " setMessage \"" + msg + "\"");
	message = msg;
    }

    public String getMessage(){
	return message;
    }

    public void setMessage2(String msg){
	System.out.println("Type1Form " + myno + " setMessage2 \"" + msg + "\"");
	message2 = msg;
    }

    public String getMessage2(){
	return message2;
    }

    public void setSubmit(String msg){
    }

    public String getSubmit(){
	return "";
    }

}

Action1,2,3へのPOSTメソッドを投げるためのページindex.jsp.

<%@page contentType="text/html; charset=EUC_JP" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>

<html:html>
<head>
<title>Struts sample</title>
</head>

<body>
<h1>Struts sample</h1>

<h2>Action1</h2>
Action1.do�إꥯ�����Ȥ���������.
<html:form action="/Action1" method="POST">
<html:text property="message" size="20" maxlength="20" /><br />
<html:submit property="submit" value="����" />
<html:reset value="�ꥻ�å�" />
</html:form>

<h2>Action2</h2>
Action2.do�إꥯ�����Ȥ���������.
<html:form action="/Action2" method="POST">
<html:text property="message" size="20" maxlength="20" /><br />
<html:submit property="submit" value="����" />
<html:reset value="�ꥻ�å�" />
</html:form>

<h2>Action3</h2>
Action3.do�إꥯ�����Ȥ���������.
<html:form action="/Action3" method="POST">
<html:text property="message" size="20" maxlength="20" /><br />
<html:submit property="submit" value="����" />
<html:reset value="�ꥻ�å�" />
</html:form>

</body>
</html:html>


最後に最終的なフォワード先out.jsp.

index.jspのからmessageの値を"ACION1"と設定して投げてみた.以下catalina.out.
Type1Form created : 3
Type1Form 3 setMessage "ACTION1"
===== Action1 execute START =====
GET Type1Form message : "ACTION1"
Type1Form 3 setMessage "Action1 checked. ACTION1"
Type1Form 3 setMessage2 "Not over write"
from request : matsu.struts.sample.Type1Form@1c4a5ec
parameter form : matsu.struts.sample.Type1Form@1c4a5ec
===== Action1 execute END =====
Type1Form 3 setMessage "ACTION1"
===== OutAction execute START =====
GET type1Form message : "ACTION1"
Type1Form 3 setMessage "OutAction checked. ACTION1"
parameter form : matsu.struts.sample.Type1Form@1c4a5ec
Session Attribute Names : org.apache.struts.action.LOCALE
Request Attribute Names  : org.apache.struts.action.MESSAGE
                  Values : org.apache.struts.util.PropertyMessageResources@178aae1
Request Attribute Names  : org.apache.struts.action.mapping.instance
                  Values : ActionConfig[path=/Out,name=Type1Form,scope=request,type=matsu.struts.sample.OutAction
Request Attribute Names  : Type1Form
                  Values : matsu.struts.sample.Type1Form@1c4a5ec
Request Attribute Names  : org.apache.struts.action.MODULE
                  Values : org.apache.struts.config.impl.ModuleConfigImpl@11abd68
===== OutAction execute END =====
"matsu.struts.sample.Type1Form@1c4a5ec"というのはオブジェクトを一意に示す文字列であるが,Action1とOutのexecuteの引数formでこれが一致し,さらにAction1とOutにおいてrequestからキー"Type1Form"でgetAttributeしたものも一致する.さらにType1Formのコンストラスタで出力する"Type1Form created : n"という文字列も1HTTP要求で一回しか出て来ない.ソース(主にorg.apache.struts.util.RequestUtils.java)の雰囲気からするとActionServletのActionForm作成判断動作は以下である(DynaFormBeanっぽい処理はここでは無視).
HttpServletRequest(requestスコープでなければHttpSession)にstruts-config.xmlのactionタグのname属性をキーにgetAttribute.
あればそのクラスをチェックしstruts-config.xmlと矛盾しなければ,それを使用.
なければnew.

ActionFormの初期化

ブラウザでは以下が出た.

����
message = “OutAction checked. ACTION1”
message2 = “Not over write”

messageの値を見ると,Action1におけるmessageに対する処理が入っていない.もう一度catalina.outを見ると,
Type1Form 3 setMessage "ACTION1"
===== Action1 execute START =====
...
===== Action1 execute END =====
Type1Form 3 setMessage "ACTION1"
===== OutAction execute START =====
となっており,各アクションのexecute呼び出しの前にType1FormのsetMessageメソッドが呼ばれていることが分かる.ここで,先のサンプルでのmessage2の値に注目してみる.これはAction1で設定し,out.jspでも出力された.HTTP要求のパラメータはmessageの値だけでmessage2の値はなかったので,Action1での設定が上書きされることなくOutまで届き,そしてout.jspでも出力された.

異なるActionFormを持つActionへのフォワード

サンプルではAction2はActionFormにType2Formを持ち,Type1Formを持つアクションOutにフォワードする.Action2.javaは以下である.

package matsu.struts.sample;

import org.apache.struts.action.*;
import javax.servlet.http.*;

public class Action2 extends Action{
    public ActionForward execute(ActionMapping mapping,
				 ActionForm form,
				 HttpServletRequest request,
				 HttpServletResponse response)
	throws Exception
    {
	System.out.println("===== Action2 execute START =====");
	Type2Form type2Form = (Type2Form)form;
	System.out.println("GET type2Form message : \"" + type2Form.getMessage() + "\"");
	type2Form.setMessage("Action2 checked. "+type2Form.getMessage());
	System.out.println ("from request : " +
			    request.getAttribute("Type2Form"));
	System.out.println ("parameter form : " + form);
	System.out.println("===== Action2 execute END =====");
        return (mapping.findForward("success"));
    }

}

Action2のActionFormであるType2Formは以下である.

package matsu.struts.sample;

import org.apache.struts.action.*;

/**
 * ActionFormBean.
 * このクラスはBeanであり,すべてのフィールドに対する
 * アクセサ(getterとsetter)を実装する.
 */
public class Type2Form extends ActionForm{
    private static int no = 0;
    private String message;
    private int myno;

    public Type2Form () {
	super();
	myno = ++no;
	System.out.println("Type2Form created : " + myno);
    }

    public void setMessage(String msg){
	System.out.println("Type2Form " + myno + " setMessage \"" + msg + "\"");
	message = msg;
    }

    public String getMessage(){
	return message;
    }

    public void setSubmit(String msg) {
    }

    public String getSubmit() {
	return "";
    }

}
index.jspを使用してAction2に要求してみた.以下はcatalina.out.
Type2Form created : 2
Type2Form 2 setMessage "ACTION2"
===== Action2 execute START =====
GET type2Form message : "ACTION2"
Type2Form 2 setMessage "Action2 checked. ACTION2"
from request : matsu.struts.sample.Type2Form@3eec1a
parameter form : matsu.struts.sample.Type2Form@3eec1a
===== Action2 execute END =====
Type1Form created : 4
Type1Form 4 setMessage "ACTION2"
===== OutAction execute START =====
GET type1Form message : "ACTION2"
Type1Form 4 setMessage "OutAction checked. ACTION2"
parameter form : matsu.struts.sample.Type1Form@fedfb6
Session Attribute Names : org.apache.struts.action.LOCALE
Request Attribute Names  : org.apache.struts.action.MESSAGE
                  Values : org.apache.struts.util.PropertyMessageResources@178aae1
Request Attribute Names  : org.apache.struts.action.mapping.instance
                  Values : ActionConfig[path=/Out,name=Type1Form,scope=request,type=matsu.struts.sample.OutAction
Request Attribute Names  : Type2Form
                  Values : matsu.struts.sample.Type2Form@3eec1a
Request Attribute Names  : Type1Form
                  Values : matsu.struts.sample.Type1Form@fedfb6
Request Attribute Names  : org.apache.struts.action.MODULE
                  Values : org.apache.struts.config.impl.ModuleConfigImpl@11abd68
===== OutAction execute END =====
そしてブラウザ出力.

����
message = “OutAction checked. ACTION2”
message2 = “”

意外にも(?)Type2Formに格納された値がType1Formにも設定されている.catalina.outをもう一度みてみると,要求のmessageの値がAction2のexecute前にType2Formに設定され,
Type2Form created : 2
Type2Form 2 setMessage "ACTION2"
===== Action2 execute START =====
さらにOutのexecute前にType1Formに設定されている.
Type1Form created : 4
Type1Form 4 setMessage "ACTION2"
===== OutAction execute START =====
すなわち,HTTP要求のパラメータは全て,各Actionのexecute前にActionFormに設定される.一つ目のActionのActinFormにアクセサがあっても,二つ目のActionのActionFromになければ,HTTP要求のパラメータに含めることができない(やるとこける).

ActionFormの引渡し

前節のcatalina.outの出力で,フォワード前のActionFormがフォワード後のActionのexecute時にも参照できることが確認できる.
Request Attribute Names  : Type2Form
                  Values : matsu.struts.sample.Type2Form@3eec1a
Request Attribute Names  : Type1Form
                  Values : matsu.struts.sample.Type1Form@fedfb6
ということで,フォワード前Actionのexecuteでフォワード後ActionのActionFormを作成し,突っ込むことはできそうである.すなわちキーをstruts-config.xmlのactionタグのname属性の値として,HttpSevletRequestにsetAttributeする.サンプルではAction3でこれを行っている.
package matsu.struts.sample;

import org.apache.struts.action.*;
import javax.servlet.http.*;

public class Action3 extends Action{
    public ActionForward execute(ActionMapping mapping,
				 ActionForm form,
				 HttpServletRequest request,
				 HttpServletResponse response)
	throws Exception
    {
	System.out.println("===== Action3 execute START =====");
	Type1Form type1Form = new Type1Form();
	type1Form.setMessage("Action3 checked.");
	type1Form.setMessage2("Action3 checked.");
	request.setAttribute("Type1Form", type1Form);
	System.out.println ("from request : " +
			    request.getAttribute("Type1Form"));
	System.out.println("===== Action3 execute END =====");
        return (mapping.findForward("success"));
    }

}
index.jspからAcion3.doに要求してみた.以下はcatalina.out.
Type2Form created : 2
Type2Form 2 setMessage "ACTION3"
===== Action3 execute START =====
Type1Form created : 2
Type1Form 2 setMessage "Action3 checked."
Type1Form 2 setMessage2 "Action3 checked."
from request : matsu.struts.sample.Type1Form@300429
===== Action3 execute END =====
Type1Form 2 setMessage "ACTION3"
===== OutAction execute START =====
GET type1Form message : "ACTION3"
Type1Form 2 setMessage "OutAction checked. ACTION3"
parameter form : matsu.struts.sample.Type1Form@300429
Session Attribute Names : org.apache.struts.action.LOCALE
Request Attribute Names  : org.apache.struts.action.MESSAGE
                  Values : org.apache.struts.util.PropertyMessageResources@1e2481b
Request Attribute Names  : org.apache.struts.action.mapping.instance
                  Values : ActionConfig[path=/Out,name=Type1Form,scope=request,type=matsu.struts.sample.OutAction
Request Attribute Names  : Type2Form
                  Values : matsu.struts.sample.Type2Form@ebf3f0
Request Attribute Names  : Type1Form
                  Values : matsu.struts.sample.Type1Form@300429
Request Attribute Names  : org.apache.struts.action.MODULE
                  Values : org.apache.struts.config.impl.ModuleConfigImpl@dc4c81
===== OutAction execute END =====
そしてブラウザ出力.

����
message = “OutAction checked. ACTION3”
message2 = “Action3 checked.”

Action3はType2FormをActionFormとし,execute内で
	Type1Form type1Form = new Type1Form();
	type1Form.setMessage("Action3 checked.");
	type1Form.setMessage2("Action3 checked.");
	request.setAttribute("Type1Form", type1Form);
等として,フォワード後のOutのためのActionFormをnewしてrequestに格納している.これは例によって,Outのexecute前に値を設定されているが,
Type1Form 2 setMessage "ACTION3"
===== OutAction execute START =====
それに注意すれば,ActionFormは無事フォワード後のActionに渡っている.
from request : matsu.struts.sample.Type1Form@300429
===== Action3 execute END =====
...
Request Attribute Names  : Type1Form
                  Values : matsu.struts.sample.Type1Form@300429

まとめ

以上をまとめると,ActionからActionへフォワードする際のrequestスコープのActinFormについて,以下が言える.
requestスコープのActionFormはHTTP要求受信時に作成され,HTTPServletRequestにて保持される.
キーはactionタグの属性nameで指定した値である.
フォワード前後で異なるActionFormにした場合,フォワード後のActionFormでは両方のActionFormが参照できる.
明示的に消すという操作はないらしく(HttpServletRequestが消える時にActionFormも消える),フォワード前にフォワード後のActionFormをsetAttributeすることができる.
requestスコープのActionFormは各アクションのexecute前に毎回HTTP要求のパラメータで初期化される.
フォワード前のexecuteでHTTP要求のパラメータにあったActionFormのフィールドを上書きしても,フォワード後のexecuteまでに再びHTTP要求のパラメータで上書きされてしまう.
HTTP要求のパラメータにないものは,フォワード前後のActionで保持される(execute前に上書きされない).
上はフォワード前後のActionFormのクラスの違いに関係なく行われるので注意が必要(例えばフォワード前のActionFormにはアクセサがあるが,フォワード後のActionFormにアクセサがないパラメータがHTTP要求にあるとこける).

This article was written by Fujiko