@ledsun blog

無味の味は佳境に入らざればすなわち知れず

struts「設計パターン」についてまとめてみる

strutsの設計パターンについてまとめ。struts1の話。ASP.NETerなのでASP.NETとの比較が多め。

まず読んどけ

struts1の思想とか、基本的な流れとかわかりやすい。 Struts1日入門 株式会社ナレッジエックス

推薦図書

strutsの本はやっぱり テッド・ハスティッド の 'STRUTS・イン・アクション' が一番面白い。絶版なのが残念だが、今さら読む人も居ないだろうからしょうがない。1系なら今でも役に立つので、古本で見つけたら捕獲すべし。訳してくれた芦沢さんに感謝。

哲学

Action First

strutsRailsのようなリソース指向でもないしASP.NETのようなページ指向でもない。まず第一にaction。たとえばdo/loginのようなURLでログインを実行して、ログイン後のページを開く。ログインページを開くのではない。ASP.NETから入った人はURL設計でページ指向にしちゃうミスをして、なんだかしっくり来ないstruts構成ファイルに泣くことになるので注意が必要。

URLはアクション名、アクションぽくつけろ
*.doよりdo/*を使え

具体的にはstruts構成ファイルのaction定義につける名前のこと。フォームっぽい名前でもページっぽい名前でもダメです。do/addressFormではだめ

  • do/openAddressForme
  • do/conirfmAddressForm
  • do/registerAddressForm

とかにする。まあ、RESTful時代から見るとHTTPのメソッド(GET、POST)とかぶってるけど気にしない。

Action

actionはとどのつまりsubmitしたときに呼ばれるメソッド。ASP.NETでいうイベントハンドラstrutsではイベントハンドラ名の命名規則がないので自分で良い名前を考える。jspイベントハンドラをバインドしてくれないのでstruts構成ファイルを使って設定する。ここら辺をIDEがやってくれるというのはASP.NETの素晴らしい点。今でもMVVMのバインドをIDEでやろうとする当たり、MSのこだわりかな。ただし、ASP.NETには1画面1フォーム縛りと、ViewStateの呪縛があるため、少量のリクエストがたくさんというコンシューマ向けサイトに向かないのが欠点*1

考え方

strutsは非常に素直に便利なCGI*2を体現している。fromをsubmitしてサーバを呼び出すときに、fromの値をサーバで処理しやすい形で受け取る(ActionForm)、指定のActionで受け取ったデータを処理する。処理したデータをjspに当てはめて表示する。

URL設計

strutsではURLにjspって拡張子が含まれることがおススメされていない。ASP.NET出身者のはまりやすい罠。jspを直接開くとActionServletを通らないし、いかなるActionも呼び出されない。ASP.NET的にいうとPage_Loadを書けなくなる。そうするとページの初期化処理をスクリプトレットで書く必要がでてきてページのマークアップとロジックの分離ができなくなってしまう。簡単な処理ならいいんだけど、スクリプトレットだと

と共通処理の抽出が非常にやりづらい。
ページ指向でURL切っちゃったときは、do/OpenPageAなりdo/LoadPageAなりのaction名でForwardActionでよいので一枚かませておくと統一感が出てよいと思う。結局初期化処理を書くにはForwadActionでは足りないので自前でしこしこActionFormとActionを作ってやる必要が…。

画面設計

actionを分ける

画面設計をするときは最初にどうやってactionクラスを分けるかを考える。なぜか?後からactionクラスを分けるには以下の手順が必要。

  1. 使用するActionFormを分ける(Java
  2. Actionクラス自体を分ける(Java
  3. strtus構成ファイルにActionFormとActionマッピングの定義を増やす(XML
  4. jsp上のhmll:formを分ける(JSP

4つのファイルを3種類のエディタで編集するのは骨が折れる。というかその前にhtml:formとactionと睨めっこして依存関係を確認して、分けれるかどうか判断するところのほうが大変。依存関係をわかりやすくするためにもActionを分けなくてはいけないと見事なデフレスパイラル

ちなみに画面仕様書からactionを見分けるコツは「actionはとどのつまりsubmitしたときに呼ばれるメソッド」です。つまり、ボタンかリンクで呼び出される何らかの処理はすべてactionです。

formを分ける

actionにはformが必要。actionを分けたあとはformを分ける。この時に各actionで必要最小限な要素を持つFormに分けると以下の二点がよい。

  1. actionから制御コードを追い出せる
  2. レイアウト上の制約になるhtml:formの範囲を小さくできる

struts-configにvalidte=trueって書けると、actionからデータ検証のコードを省くことがで正常系だけの記述になる。クラスの責務が明確になりコードの見通しがよくなる。要するに、全部のactionで

ActionErrors err = form.validateHoge();
if (err.size() > 0) {
    saveErrors(request, err);
    return mapping.getInputForward();
}

を書いて気分が悪くなることを防げる。逆にActionFormを複数のactionで使いまわすと

  1. actionによって検証したい要素が異なる
  2. 検証メソッドが分かれる
  3. struts-configにvalidte=falseにする
  4. 各actionでデータ検証の異常ハンドリングを書く

となって上記呪文を各actionで書く羽目になる。actionからデータ検証のコードをなくせるのはASP.NETにはない素晴らしい点。ぜひ使いこなそう。

fomrはレイアウト上の制約になります。
html:formはブロック要素なので、行の途中に書くときは明示的にfloat:leftと指定する必要がありレイアウト指定が微妙に面倒。また、formを入れ子にした場合は内側のformは上手くバインドされない。このような面倒に出くわさないように、formはなるべく小さい範囲で指定する。formの入力要素とボタンを離してレイアウトしたい場合は、ボタンのonclickイベントでform内の隠しボタンを押すことをおすすめ。隠しボタンにstyleId要素を指定しておけばIDを振れるのでjavascriptの記述は簡単。

<html:button
    property="hogeButton"
    onclick="document.getElementById('trueHogeButton');">
    <bean:message key="button.hoge" />
</html:button>
画面設計書

以上の思想からstruts向けの詳細設計書書くと要なのは以下の項目。

  1. 画面内のaction一覧
  2. actionが必要とする要素とそれを含むhtml:form
  3. html:fromに対応するActionForm
  4. JavaScriptで行う入力チェック
  5. ActionFormで行う入力チェック
  6. Actionで行う詳細な処理(ビジネスロジック

strutsは作りながら考えるより、事前にクラス単位まで分割してから実装したほうが効率的。

formは絶対必要

ページ遷移のためのactionなどWebアプリにはformが必要ないactionもある。が、strutsでは必ず必要。struts構成ファイルでactionを定義するのに使用するformが必要。そういう時にはDynaActionFormで空のFormをBlankFormやDummyFormという名前で定義しておいて使いまわす。

EventDispatchActionよりただのAction

actionを分けるにはActionのクラスを分けるのとEventDispatchActionクラスを使ってメソッドを分ける方法がある。1ファイルが大きくなるのでActionクラスを分けるのがおススメ。ただ、まったく同じformに対する処理(入力確認と保存など)はEventDispatchActionを使うのが吉。struts構成ファイルにtypeだけが違うactionマッピングを二つ書く必要がなくなる。

JSPJavaのバインド

DAO使ってDB用のDTOがある前提で、検索項目のバインドと明細のバインドについて

検索項目のバインド

検索項目というのは一般的なhtmlのformの使い方、要素を選んでsubmitする。その時初期値をActionFromからhmtl上のformへのバインドするには?方法は二つ

  • ActionFormを初期化
  • ActionForm(またはその他のbean)をセッションに入れて、bean:write

上には制限がある。呼び出し条件が関係ないならActionFormのコンストラクタで初期化すれば良い。前画面で選んだ項目をActionFormに入れる時は

  1. 初期化したいActionFormを使うloadActionを呼び出す
  2. loadActionで初期化したActionFormをセッションに保存
  3. 画面遷移
  4. ActionFormのresetメソッドでセッションから値をコピー

するとうまいことJSPにバインドしてくれる。初期化したいformが二つある場合や、jsp呼び出しの場合は自動的に下を使う*3。下の場合は、bean:wirteだけだとバインドされない。バインド用にhiddenも用意しておいてそれにもbean:writeすること。
ちなみに検索用のDTOがある場合、DTOをそのままActionFormのプロパティ(dataとか)に定義すると楽ちん。data.nameとかでJSPからマッピングできるし、検索するときはそのままDTO(もしくはコピー)を投げればよい。

public class InputForm extends ActionForm {
	private DtoForSearch data = new DtoForSearch();

	public DtoForSearch getData() {
		return data;
	}

	public void setData(DtoForSearch data) {
		this.data = data;
	}
}
明細のバインド

業務アプリでよくあるのがDBからの検索結果を表に表示すること。DTO(DBの検索結果)から表に入れるときはDTOのサブクラスを作って、BeanUtils.copyPropertiesでコピーするのがおすすめ。

  • DBが変わってDTOが変わった時に表示用サブクラスの変更がいらない。
  • 表示形式を変えたい(たとえば「true/false」の表示を「○/×」に変えたい)時にDTOにビュー用の処理を入れないで済む。
public class ViewDto extends Dto {
	public string getFlagDisplay() {
		return flag ? "○" : "×";
	}
}
明細からのバインド

表の表示内容を編集して保存する場合。表からDTOへgetDataとかする定石がある。strutsで配列型のフォームオブジェクトを使う方法(2)。面倒くさいけどこれ通りにやるしかない。やらないとこんな【Struts】ServletException beanutils.populate @ Advanced Pro Developer BlogBeanUtils.populateの限界。この方法ではJSPでindexed=trueにした要素だけがマッピングされる。それ以外の要素はサーバ側で行番号なんかで突き合わせて取得すれば良いです。

ActionFormって名前かっこいいけど、実態はただの(http側の)DTOだよなぁ。

Tipsリンク

ログインチェック

Filterを使うとサーブレットコンテナへの全リクエストをチェックできる。

public class LoginFilter implements Filter {
	public void init(FilterConfig arg0) throws ServletException {
	}

	public void destroy() {
	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) request;
		String uri = req.getRequestURI();
		if (uri.contains(".do") || uri.contains(".jsp")) {
			if (!uri.contains("openLogin.do") && !uri.contains("Login.jsp")) {
				if ( !isLogin(req) ) {
					req.getRequestDispatcher("/openLogin.do").forward(req, response);
					return;
				}
			}
		}
		chain.doFilter(request, response);
	}
}
CSVファイルダウンロード

@IT:Java TIPS -- データベースの内容をCSV形式でダウンロードするちなみにContentTypeでcharsetを指定すれば、PrintWiterにcharsetを指定しなくてもよいのでresponse.getWriterが使える。

fileName = new String(fileName.getBytes("Windows-31J"), "ISO-8859-1");

// HTTPヘッダの出力
response.setContentType("application/octet-stream;charset=Windows-31J");
response.setHeader("Content-disposition", "attachment; filename=\"" + fileName + "\"");
response.flushBuffer();

// CSV出力
CSVWriter writer = new CSVWriter(response.getWriter());
writer.writeAll(data);
writer.flush();
writer.close();
traceログを入れる

FilterではRequestURIしか取れない。ActionMapping.findFowardメソッドだと、ファイルダウンロードのActionではreturn nullするので取れないことがある。RequestProcessor.processActionPerformにログ出力足してあげると、全action呼出しにログ入れられる。Strutsメモ8にRequestProcessorの拡張方法が書いてある。

public class MyRequestProcessor extends RequestProcessor {
	static protected Logger log = Logger.getLogger(MyRequestProcessor.class);

	protected ActionForward processActionPerform(HttpServletRequest request,
			HttpServletResponse response, Action action, ActionForm form,
			ActionMapping mapping) throws IOException, ServletException {
		try {
			String logStr = request.getRequestURI() + " - " + action.getClass().getName();
			log.info("action 開始 : " + logStr);
			ActionForward  forward = action.execute(mapping, form, request, response);
			log.info("action 完了 : " + logStr);
			return forward;
		} catch (Exception e) {
			return (processException(request, response, e, form, mapping));
		}
	}
}
例外処理

Strutsエラーメモ(Hishidama's struts error Memo)例外キャッチしてエラーメッセージ出すだけならstruts.configで書ける。前処理が必要ならExceptionHandlerを入れるといい。例外処理を設定ファイルに追い出してくと、Actionには正常系だけ書けばよくなる。

ActionMesseges.addの引数のプロパティの使いわけ

Strutsリファレンス<html:messages>画面の特定の要素に出したければプロパティで絞りこむ。どこか一か所にまとめて出すならActionErrors.GLOBAL_MESSAGEでOK。

自戒

Tiles

Tilesにはどの本にも解説が出てくるから早めに取り組んでおけ。JSPの重複を解消するにはTiles使うかカスタムタグ作るしかない。

*1:ASP.NET4ではかなり解消されてるんだけど、jQueryでバリバリ画面つくるとサーバコントロールがうまみがイマイチ

*2:というかServlet

*3:こういうことがあるので常にAction経由でページ遷移すること