SAFlute の ActionCallback

SAFluteの特徴の一つです。

ActionCallbackのきっかけ

Interceptorの悩み

Interceptorによる横断処理には以下の二つの問題があると考えました。

バリデーションよりも後
バリデーションエラーになると実行されない
Interceptorだらけ
StackTraceの汚れ、追いかけづらく、親近感が湧かない

前後処理をAction自身に書く

ServletFilterでは、まだどのActionが実行されるかわからないので業務的な処理が書きづらく、そもそも扱いやすさでは Interceptor とあまり変わりがありません。

そこで、"Strutsを通過した後、バリデーションよりも前" のタイミングにて、Action自身に前後処理を書けるようにしています。 それを ActionCallback と呼んでいます。

それにより、すぐ近くにあって追いかけやすく親近感も湧き、Interceptorチェーンによる StackTrace も汚れもなく、そして、Actionクラスにてオーバーライドすることで簡単に挙動も変更することもできます。

godHandBefore(), godHandFinally()

SAFluteでは、全てのActionで共通のスーパークラスを継承することを推奨しています。 そのスーパークラスにて、godHandBefore(), godHandFinally()を実装すると、リクエスト時に前後処理として呼び出されます。

godHandBefore()
バリデーションよりも前のタイミング
godHandFinally()
正常終了でもバリデーションエラーでも最後に必ず呼ばれる
e.g. Actionのスーパークラスにて、共通の前後処理を実装 @Java
@Override
public String godHandBefore(BrActionExecuteMeta executeMeta) {
    final String path = super.godHandBefore(executeMeta);
    if (path != null) {
        return path;
    }
    final String sslRedirect = processSslRedirect(executeMeta);
    if (sslRedirect != null) {
        return sslRedirect;
    }
    processTrackingCode();
    processUserAgent();
    return null;
}

@Override
public void godHandFinally(BrActionExecuteMeta executeMeta) {
    if (executeMeta.isForwardToJsp()) { // JSPへフォワードするなら
        if (userWebBean == null) { // basically true, however just in case
            final MemberUserBean userBean = getUserBean();
            userWebBean = new MemberUserWebBean();
            userWebBean.initialize(userBean, convertCoinLogic);
        }
    }
    super.godHandFinally(executeMeta);
}

callbackBefore(), callbackFinally()

godHandXxx()は、スーパークラス用のメソッドとして用意しています(別にサブクラスでもオーバーライドできてしまいますが、やらないように)。 サブクラス、つまり、末端の具象クラスのためのメソッドとして、callbackBefore(), callbackFinally() を用意しています。

callbackBefore()
godHandBefore()と仕様は同じ
callbackFinally()
godHandFinally()と仕様は同じ

わざわざ分けているのは、画面開発者がオーバーライドするときに super 呼び忘れるトラブル(何も動かなくなる事件)が起きないようにするためで、super を呼び忘れても問題のないサブクラス専用のメソッドを提供しています。

わざと、サブクラス用の方が覚えやすいまともな名前にしています。 godHandなんてメソッド、通常のコード補完で見ることほぼないでしょうし、見つけてもきもちわるくてオーバーライドしようとは思わないでしょう。 (一方で、インパクトがありすぎてすぐに覚えちゃうという話もあるようですが...)

画面描画処理は callbackFinally() にて

サブクラスでの callback は、画面内リクエスト共通処理という扱いになります。 用途としては、例えば、画面描画のためのデータ準備処理を callbackFinally() で実装すると良いでしょう。

e.g. Actionのサブクラスにて、画面描画処理をcallbackFinally()で実装 @Java
@Override
public void callbackFinally(BrActionExecuteMeta executeMeta) {
    if (executeMeta.isForwardToJsp()) { // JSPへフォワードするなら
        ... // そのJSPを描画するための準備処理
    }
    super.godHandFinally(executeMeta);
}

こうすると、バリデーションエラーのときでも実行されます。@Execute の input 属性には、JSPをそのまま指定してOKです。 (描画用の@Executeメソッドを用意してフォワードするやり方は、その入り口は外からは本来不要ですし、Interceptor もまた動いてしまいますし、何かとスパゲッティになりがちなので推奨されません)

e.g. 画面描画処理は callbackFinally() でやるので、inputは普通にJSPを指定 @Java
// input属性には、JSPを指定するのが流儀
@Execute(validate = "doValidate", input = path_Member_MemberEditJsp)
public void submit() {
    ...;
}

認証チェックや業務例外ハンドリングなどなど

もろもろ ActionCallback で処理を行っています。(Interceptorを "あまり" 使わないがSAFluteの流儀)

SAFluteのフレームワーク(TypicalBaseAction)にて、既に以下の処理が ActionCallback に組み込まれています。

  • DBFluteの共通カラム自動設定のための AccessContext の初期化
  • DBFluteのSQL呼び出しActionのSQLコメントへの埋め込み CallbackContext の初期化
  • DBFluteのリクエスト内SQL発行階数のカウント CallbackContext の初期化とロギング処理
  • ログインチェック、自動ログインも含む
  • 業務例外のハンドリング
  • Responseに no cache を付与