オレオレStruts拡張

S2Strutsをメンテナンスしているのですが、メンテナンスしてみて思ったのは「S2Strutsって意外と機能が多くて間単に使いたいだけの人にとっては全体を把握しにくいだろうなぁ」ということです。

もっとシンプルなのがいいな、ということでこんな感じのオレオレStruts拡張を作ってみました。

  • ActionやActionFormは通常のStrutsと同じように継承して使う。
  • Actionのメソッドの呼び出しはパラメータ名に基づいて行う。
  • アノテーションでActionFormのバインディングを行う。
  • アノテーションでHttpServletRequest/HttpSessionのgetAttribute/setAttributeを自動化する。
  • Strutsにだけ依存するようにする。
  • Seasar(2.3系/2.4系)やSpring(1系/2系)など任意のDIコンテナと連携可能にする。
  • Struts1.2でも1.3でも動作可能にする。

シンプルが売りなのでHOT deployとか無設定とかはなし。地道にstruts-config.xmlを書きます。action要素のpath属性の値とコンポーネント名をつき合わせてDIコンテナ管理下のコンポーネントを呼び出します。struts-config.xmlを書くのは明らかに面倒くさいですが、Strutsを知っている人にはわかりやすいと思う。
ActionやActionFormにはユーティリティ的なメソッドが用意されているのでこれをそのまま利用できるよう継承を前提とします(Actionについては必須ではないけれど)。POJOにすると、これらのメソッドに相当する機能や規約が別個に必要になっちゃうのがいやなのです。


以下、例です。

jspとActionFormとActionは1対1対1で作成します。S2Strutsとかで使うInitActionの役割はActionに持たせます。

今回作ったオレオレStruts拡張ではデフォルトで「*」で始まるパラメータをメソッド名だとみなします。従業員一覧のlist.jspから次のURLのリンクをクリックするとします。ここでは「/example」をコンテキストパスとします。

http://localhost:8080/example/emp/list.do?*edit&empno=7369

パラメータに「*」ではじまる「*edit」が存在するので、editメソッドを呼び出します。「/emp/list」がstruts-config.xmlでListActionにマッピングされているとすると、ListActionのeditメソッドを呼び出します。ListActionのコンポーネントは「/emp/list」という名前でSeasar2やSpringのコンポーネントとして登録されている必要があります。

ListAction
public class ListAction extends Action {
    @Form
    public ListForm listForm;

    public String edit() {
        return "edit";
    }
}

@FormはActionFormであることを示すアノテーションでActionに1つだけ指定できます(ActionFormがない場合は指定しないことも可)。editメソッドを呼び出す直前で設定され、呼び出された後は、デフォルトでフィールド名をキーとしてリクエストスコープに格納されます。
戻り値の「edit」という文字列はActionForwarの名前です。ここでは、struts-config.xmlでedit.jspフォワードするように指定されているとします。

フォワード先のjspでは、html:formタグの内側でglue:prepareタグを指定しておきます。役割はS2Strutsのinitタグとほとんど同じ。画面の表示に必要なデータを取得します。

edit.jsp
<html:form action="/emp/edit">
  <glue:prepare />
 ・・・
  <html:submit property="*confirm">確認</html:submit>
</html:form>

glue:prepareタグは、formタグのaction属性に対応するActionのprepareメソッドを呼び出します(任意の名前のメソッド名を指定することも可)。「/emp/edit」がEditActionにマッピングされているとすると、EditActionのprepareメソッドを呼び出します。

EditAction
public class EditAction extends Action {

    @Binding(bindingType = BindingType.MUST)
    protected EmpService empService;

    @Form
    protected EditForm editForm;

    @RequestAttribute
    protected ListForm listForm;

    public void prepare() {
        empno = Integer.valueOf(listForm.getEmpno());
        Emp emp = empService.getEmp(empno);
        BeanUtil.copyProperties(editForm, emp);
    }

    public String confirm() {
        return "confirm";
    }
}

@RequestAttributeは、フィールド名をキーとしてリクエストスコープの値をバインディングするアノテーションで、バインディングはprepareメソッドの呼び出し前に行われます。そのためprepareメソッドではフォワード元ですでに値が詰まっているActionFormの情報(listForm)を利用してこれから表示するデータを用意できます。@RequestAttributeがついたフィールドの値は、フィールド名をキーとして呼出し後にリクエストスコープに格納されます。

DIはDIコンテナまかせ。ここではempServiceをDIしています。どのDIコンテナでもsetterメソッドがあればDIされると思いますが、ここではSeasar2.4の例で@Bindingを使ってDIしています。

edit.jspから生成されたHTMLで確認ボタンを押すと、「*confirm」というパラメータが渡されEditActionのconfirmメソッドが呼び出されます。そして、「confirm」という名前のActionFowardが利用されてconfirm.jspフォワードし、confirm.jspに記述されたglue:prepareタグからConfirmActionのprepareメソッドが呼ばれ。。。という流れになります。