いつのまにかSeasarユーザーのためのハブサイトが出来ている!

検索していたらたまたま見つけたサイト。

seasar-users.jp へようこそ!
このサイトでは Seasar を使って高品質な Web アプリケーションを開発するための情報を提供しています。 Seasar を既に利用されている方だけでなく、これから Seasar を利用してみようという方にも配慮し、Seasar 開発者のポータルサイトとなることを目指しています。

http://seasar-users.jp/

少し前、xxxxx-users.jpというドメインを使用したプログラミング言語ユーザーのハブサイトが雨後の筍のように増殖したことは記憶に新しいところですが、いつのまにかSeasarユーザーのハブサイトができていました。

検索しても情報が全然出てこないので、最近できたのでしょうか。

サイトはすっきり見やすい構成で、スクリーンショットを多用したチュートリアルなどは初心者の方は重宝するのでは。

今はSAStrutsのコンテンツの比率が多いようで、個人的にはSAStruts チートシートがイチオシです。

今後のコンテンツの充実に期待大ですね。

S2RequestProcessorとインターセプターは相性が悪い?

ActionMessagesExceptionをキャッチしたら前ページへ、@Execute(roles="xxx")で権限なし例外があったら指定されたページへ、それ以外の例外は指定したページへ・・・みたいな感じにしたかったんだけど、ちょっとうまくいかない。

というのも権限チェックはS2RequestProcessor#processRoles()でやっててそこからNoRoleRuntimeException が発生するため、costomizer.diconで指定するインターセプタには捕らえられない。

例外を処理するインターセプター - Ruby Study Go

引用した記事に書かれているようなことが、ちょうど自分にも発生しました。

会員系サイトによくある「ようこそxxxさん」のように全てのページでユーザー名が表示できるように、requestにユーザー名をセットする独自のインターセプターを作って、actionCustomizerに設定していたのですが、validatorでエラーになるとユーザー名が表示されない。

なぜかと思って調べていくと、エラーがあるとS2RequestProcessor(実際はRequestProcessor)#processValidate()で処理が打ち切られるようで、Actionに仕掛けたインターセプターまで到達しない。

結局、インターセプターに実装していた処理は、S2RequestProcessorクラスを継承させたクラスへ実装しました。


インターセプターが必ず動くことを前提にした作りにしてしまうと、結構ハマりそうな所なので覚えておこう。

こういったビューへ共通的な文言をセットする事って多々あると思うのだが、もっとスマートなやり方はあるのかなぁ?

実行メソッドのindexメソッドは必ず必要?

SAStrutsに限った話ではないですが、WEB-INF/web.xmlに以下のように404エラーのエラーページ設定を入れておけば、存在しないURLへアクセスすると指定したエラーページへ遷移させることが出来ます。

<error-page>
	<error-code>404</error-code>
	<location>/WEB-INF/view/error/404NotFound.jsp</location>
</error-page>

色々なURLを打って動作確認していたら、たまに真っ白いだけの画面になるので何でだろうと思い調べてみると、indexメソッドを用意していないActionで起こる模様。

indexメソッドなしのHogeActionの場合、indexメソッドにマッピングされる/hogeというURLにアクセスすると真っ白画面になる。

とりあえずの回避策として以下のようにindexメソッドを作って、HttpServletResponseで404エラーを送ることでちゃんとエラーページが表示されました。

public class HogeAction {

	@Resource
	protected HttpServletResponse response;

	@Execute(validator = false)
	public String index() throws IOException{
		response.sendError(HttpServletResponse.SC_NOT_FOUND);
		return null;
	}

	@Execute(validator = false)
	public String hoge(){
		return "hoge.jsp";
	}

}

これ以外にもindexメソッドで存在しないjsp名をreturnしても404エラーとなることが確認できました。

いずれにせよ必要の無いindexメソッドを用意する必要があるのがイマイチなので、フレームワーク側で対応してもらえるとありがたいですね。

もしかしてindexメソッドを必要としないActionを作る自分がおかしい??

ActionFormについて思うこと

先々週辺りに発覚したActionのpublicフィールドの予期せぬ自動バインドの問題。→参照:ぱる日記


これを受けて次のリリースからはActionFormの使用を推奨するようで、それに伴ってか、ActionForm周りで色々と動きがありそうです。


私はActionFormとしてのDtoを必ず作成する方針とし、一律セッションスコープで使っているので、セッションスコープのActionFormの削除手段が用意されたのは嬉しいのですが、私の抱える問題を解決できる手段ではなさそうです。

私のActionFormの使い方と抱える問題

先に述べたとおり、私はActionFormを必ず作成し、一律セッションスコープで使っています。

また、1つのActionの中に検索処理や編集処理などを含めているので、ActionFormには検索系の項目と登録系の項目が混在しています。

セッションスコープで使っている理由としては、

      • 登録処理に確認画面を挟むため
      • 検索画面と一覧画面が別画面で、一覧画面ではページングの機能が必要なため

といった理由からです。id:cypher256さんのSAStruts関連のエントリーの影響も大きいのですが。。


ここで問題になるのは、検索系の項目と登録系の項目のリセットタイミングが違うことです。
私は、この対処として同じくcypher256さんの@Reset アノテーション - cypher256's blogで書かれている@Resetアノテーションを流用させて頂いて凌いでいます。


今回、SAStrutsで用意されるセッションスコープのActionFormの削除手段(@ExecuteアノテーションにremoveActionForm=trueを指定)は、実行メソッドの正常終了時にセッションからそのActionFormを削除するというものなので、検索系の項目だけ初期化、登録系の項目だけ初期化といったことができそうに無いです。

ActionとActionFormは1:1の関係が良いのか?

登録処理系と検索処理系をxxxActionとxxxSearchActionと分けてActionFormも別にしてしまえば良いのですが、それに伴ってURLもxxx/editとxxxSearch/searchと変わってしまうのも、なんか嫌ですよね。

現状、SAStrutsアーキテクチャとしてはActionとActionFormは1:1の関係となっていますが、個人的にはActionとActionFormは1:nの関係で、Actionの実行メソッドで使用するActionFormを選べたほうが柔軟で良いのではないかと最近考えるようになってきました。

@Executeアノテーションで使用するActionFormを選択できて、removeActionForm=true指定をしておけば実行メソッドの正常終了時にそのActionFormがセッションから削除されるという様になれば、検索系のActionFormと登録系のActionFormのリセットタイミングを変えられるので良いなぁ、と。


この辺りの問題は皆さんはどのように対処しているんでしょうか?

ActionFormセッションスコープ派な人や、別な解法をお持ちの方はご意見戴けるとありがたいです。

Entity単位のServiceに共通の親クラスを持たせる

元ネタは続・SAStruts + S2JDBCのアーキテクチャに載っていたServiceで共通的に使うメソッドを共通親クラスに抽象化したメソッドで持たせるという考え。

ジェネリクスを使ったクラス設計なぞしたことが無い自分には良い刺激を受けました。


元記事ではユースケース単位にServiceを用意するという考えでしたが、自分としてはServiceはEntity単位に作っておいてActionでは1つまたは複数のServiceを組み合わせて使うようにしたい。

ServiceをEntity単位とした場合に元記事のAbstractServiceのままでは、例えばEmployeeエンティティ用のEmployeeServiceクラスを使ってfindAll等を使おうとした時に

employeeService.findAll(Employee.class);

となり、なんかDRYじゃない感じになる。

ServiceをEntity単位に作成するという方針にするのであれば、

employeeService.findAll();

としたい。

あと、自分は必ずActionFormとしてのDTOを使う方針にしたので、CRUD処理の引数もEntityではなくDTOを渡してEntityへの詰め替えもやらせたい。


何とか出来ないものかと試行錯誤してAbstractServiceを改変した結果が以下の通り。

public abstract class AbstractService<E> {

 // JDBCマネージャー
 public JdbcManager jdbcManager;

 // Entityのクラス
 public Class<E> clazz;

 public AbstractService(Class<E> clazz){
  this.clazz = clazz;
 }

 public <D> int insert(D dto) {
  E x = Beans.createAndCopy(clazz, dto).execute();
  return jdbcManager.insert(x).execute();
 }

 public <D> int update(D dto) {
  E x = Beans.createAndCopy(clazz, dto).execute();
  return jdbcManager.update(x).execute();
 }

 public <D> int delete(D dto) {
  E x = Beans.createAndCopy(clazz, dto).execute();
  return jdbcManager.delete(x).ignoreVersion().execute();
 }

 public E find(Integer id) {
  return jdbcManager.from(clazz).id(id).getSingleResult();
 }

 public List<E> findAll() {
  return jdbcManager.from(clazz).getResultList();
 }

 public List<E> findAll(String orderBy) {
  return jdbcManager.from(clazz).orderBy(orderBy).getResultList();
 }

 public List<E> findAllByBeanMap(BeanMap conditions,
  String leftOuterJoin, String orderBy) {
  return jdbcManager.from(clazz)
     .leftOuterJoin(leftOuterJoin)
     .where(conditions)
     .orderBy(orderBy)
     .getResultList();
 }

 public List<E> findAllBySqlFile(String sqlPath, Object parameters) {
  return jdbcManager.selectBySqlFile(clazz, sqlPath, parameters).getResultList();
 }

 public long count(BeanMap conditions) {
  return jdbcManager.from(clazz).where(conditions).getCount();
 }

 public boolean exist(Integer id) {
  Object obj = jdbcManager.from(clazz).id(id).getSingleResult();
  return obj == null ? false : true;
 }

}

Employeeエンティティ用のEmployeeServiceクラスの実装は次のような感じ。

public class EmployeeService extends AbstractService<Employee>{

 public EmployeeService(){
  super(Employee.class);
 }

 /**
  * ここに個別のメソッドを定義
  */

}

型パラメータにEmployee、コンストラクタでもEmployee.classと指定しないといけないのが冗長な感じだけど、とりあえず

    • 引数にEntityのクラスを渡さない使い方(employeeService.findAll()といった感じ)
    • CRUD処理でDTOを引数にしてEntityへ詰め替える

という目的は達成できました。

どうにかして型パラメータからAbstractServiceのclazzへセットしたかったのですが、やり方わからず。。。

HOT deployとCOOL deployで挙動が違う?

少し前に各所で話題にのぼっていたid:cypher256さんの一連のSAStruts関連のエントリー

実際の案件に適用しての話なので、すごく参考になります。

このエントリーの中で@Reset アノテーション - cypher256's blogを試してみたのですが、なぜか動かない。


デバッグしてみたらRequestProcessorのサブクラスで取得した@Resetアノテーションがnullになっている。

しかも、初回のアクセスの場合だけはちゃんと取得できるという状況。

もしやと思ってCOOL deployにしてみたら、ちゃんと動いた。


うーん、この辺の挙動の違いって謎。

メッセージの渡し方

StrutsのActionクラスではsaveMessagesメソッドを使えば、ビューへメッセージを渡せますが、SAStrutsではどうやって渡すのか調べてみたら、ActionMessagesUtilというユーティリティクラスが用意されていました。

Actionクラスでの使い方は以下のような感じ。

public class HogeAction {

    public HttpServletRequest request;

    // 中略

    @Execute(input = "index.jsp")
    public String submit() {
        ActionMessages messages = new ActionMessages();
        messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("xxxxxx")); // xxxxxxはメッセージリソースに定義
        ActionMessagesUtil.addMessages(request, messages);
        return "index.jsp";
    }
}

jspでの出力はhtml:messagesタグを使う。

<html:messages id="msg" message="true">
  <bean:write name="msg" ignore="true"/>
</html:messages>

ActionMessagesUtil.addMessagesの引数にrequestを使った場合、遷移をリダイレクトにすると遷移先でメッセージが表示されないので、そのような場合にはsessionを使えば良さそう。

public class HogeAction {

    public HttpSession session;

    // 中略

    @Execute(input = "index.jsp")
    public String submit() {
        ActionMessages messages = new ActionMessages();
        messages.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("xxxxxx")); 
        ActionMessagesUtil.addMessages(session, messages); // 引数をsessionに
        return "index?redirect=true"; // リダイレクトな遷移
    }
}

こうすれば、メッセージがセッションスコープで登録されるようで、リダイレクトした後でもメッセージの表示が出来ました。