strutsの設計パターンについてまとめ。struts1の話。ASP.NETerなのでASP.NETとの比較が多め。
まず読んどけ
struts1の思想とか、基本的な流れとかわかりやすい。 Struts1日入門 株式会社ナレッジエックス
推薦図書
strutsの本はやっぱり テッド・ハスティッド の 'STRUTS・イン・アクション' が一番面白い。絶版なのが残念だが、今さら読む人も居ないだろうからしょうがない。1系なら今でも役に立つので、古本で見つけたら捕獲すべし。訳してくれた芦沢さんに感謝。
哲学
Action First
strutsはRailsのようなリソース指向でもないし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)とかぶってるけど気にしない。
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クラスを分けるには以下の手順が必要。
- 使用するActionFormを分ける(Java)
- Actionクラス自体を分ける(Java)
- strtus構成ファイルにActionFormとActionマッピングの定義を増やす(XML)
- jsp上のhmll:formを分ける(JSP)
4つのファイルを3種類のエディタで編集するのは骨が折れる。というかその前にhtml:formとactionと睨めっこして依存関係を確認して、分けれるかどうか判断するところのほうが大変。依存関係をわかりやすくするためにもActionを分けなくてはいけないと見事なデフレスパイラル。
ちなみに画面仕様書からactionを見分けるコツは「actionはとどのつまりsubmitしたときに呼ばれるメソッド」です。つまり、ボタンかリンクで呼び出される何らかの処理はすべてactionです。
formを分ける
actionにはformが必要。actionを分けたあとはformを分ける。この時に各actionで必要最小限な要素を持つFormに分けると以下の二点がよい。
- actionから制御コードを追い出せる
- レイアウト上の制約になるhtml:formの範囲を小さくできる
struts-configにvalidte=trueって書けると、actionからデータ検証のコードを省くことがで正常系だけの記述になる。クラスの責務が明確になりコードの見通しがよくなる。要するに、全部のactionで
ActionErrors err = form.validateHoge(); if (err.size() > 0) { saveErrors(request, err); return mapping.getInputForward(); }
を書いて気分が悪くなることを防げる。逆にActionFormを複数のactionで使いまわすと
- actionによって検証したい要素が異なる
- 検証メソッドが分かれる
- struts-configにvalidte=falseにする
- 各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向けの詳細設計書書くと要なのは以下の項目。
- 画面内のaction一覧
- actionが必要とする要素とそれを含むhtml:form
- html:fromに対応するActionForm
- JavaScriptで行う入力チェック
- ActionFormで行う入力チェック
- 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マッピングを二つ書く必要がなくなる。
JSPとJavaのバインド
DAO使ってDB用のDTOがある前提で、検索項目のバインドと明細のバインドについて
検索項目のバインド
検索項目というのは一般的なhtmlのformの使い方、要素を選んでsubmitする。その時初期値をActionFromからhmtl上のformへのバインドするには?方法は二つ
- ActionFormを初期化
- ActionForm(またはその他のbean)をセッションに入れて、bean:write
上には制限がある。呼び出し条件が関係ないならActionFormのコンストラクタで初期化すれば良い。前画面で選んだ項目をActionFormに入れる時は
- 初期化したいActionFormを使うloadActionを呼び出す
- loadActionで初期化したActionFormをセッションに保存
- 画面遷移
- 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でコピーするのがおすすめ。
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使うかカスタムタグ作るしかない。