2011年2月21日月曜日

Struts 2 インターセプターという仕組み

インターセプターとは

 アメリカンフットボールの用語に、「インターセプト」があります。パスしたボールが守備側に横取りされることをいうのですが、Struts 2のインターセプターも、アメフトと同じようなイメージです。Actionメソッドが呼び出される前に、複数のインターセプターが割り込み、制御を横取りして実行されます。

1 インターセプターのフロー図

図1 インターセプターのフロー図

 図中の、ActionProxyActionInvocationResultは、Struts 2の核となるオブジェクトです。Servletコンテナからのリクエストがあると、(図ではActionProxyが呼ばれる前の処理を省略しています)設定に従ってActionProxyがインターセプターを呼び出します。インターセプターは、ActionInvocationオブジェクトがインスタンスを保持しています。

 インターセプターは、その処理のなかで再帰的に次のインターセプターを呼び出します。最後のインターセプターの後は、Actionが実行され、Resultオブジェクトで結果画面の生成をします。それから再び呼び出したインターセプターに制御が戻ってきます。そのときの順番は、最初の逆準となり、図の例では、Interceptor3→Interceptor2→Interceptor1という具合になります。

 Struts 2では、その機能の多くがインターセプターとして提供されており、さまざまなインターセプターがあらかじめ用意されています。また、プラグインのように、必要に応じてインターセプターを利用することができます。

 このようなアプローチは、アスペクト指向と言えるものです。インターセプターにより、Actionクラスを横断するような汎用的な処理をモジュール化することができます。その結果、Actionクラスは固有の処理に特化できるようになり、Actionクラスとしての役割がシンプルに記述できるようになっています。

 定義済みのインターセプターは結構数がそろっているものの、当然Webアプリケーション独自のものを定義したい場合があるでしょう。そんな場合は、インターセプターを自分で記述することができます。本稿では、かんたんなインターセプターを作って、インターセプターの動作を明らかにしていきます。

インターセプター一覧

インターセプター名

定義名

概要

Alias インターセプター

alias

入力画面のフォームのパラメータ名に別名をつける

Chaining インターセプター

chain

プロパティの内容を次のActionに引き継ぐ

Checkbox インターセプター

checkbox

フォームのCheckboxで、checkされていなくても、値がセットされるようにする

Cookie インターセプター

cookie

cookieデータを管理する

Conversion Error インターセプター

conversionError

入力フォームの変換エラーを検出する

Create Session インターセプター

createSession

セッション情報を作成する

Debugging インターセプター

debugging

デバッグメッセージを表示する

Execute and Wait インターセプター

execAndWait

非同期でActionを実行する

Exception インターセプター

exception

例外がスローされたときの処理を設定する

File Upload インターセプター

fileUpload

ファイルのアップロードを支援する

I18n インターセプター

i18n

ロケール情報を保持する

Logger インターセプター

logger

ログ出力を行う

Message Store インターセプター

store

メッセージの保存と参照を行う

Model Driven インターセプター

modelDriven

Actionをモデルドリブンで操作する

Scoped Model Driven インターセプター

scopedModelDriven

Actionをスコープドモデルドリブンで操作する

Parameters インターセプター

params

リクエストパラメータを制御する

Prepare インターセプター

prepare

Actionクラスのexecute()メソッドが実行される前に呼び出したい処理を記述する

Scope インターセプター

scope

Actionクラスのプロパティをsessionapplicationスコープとして設定する

Servlet Config インターセプター

servletConfig

session等に情報を保存する

Static Parameters インターセプター

staticParams

Action単位で静的パラメータを定義する

Roles インターセプター

roles

JAAS認証時のみActionを実行するようにする

Timer インターセプター

timer

Actionクラスの実行時間を計測する

Token インターセプター

token

2重POSTの禁止処理(リクエスト単位)を行う

Token Session インターセプター

tokenSession

2重POSTの禁止処理(セッション単位)を行う

Validation インターセプター

validation

バリデーション処理を行う

Workflow インターセプター

workflow

validateメソッドの呼び出しを行う

Parameter Filter インターセプター

parameterFilter

パラメータの取り消しを行う

Profiling インターセプター

profiling

プロファイリング機能のON/OFF設定をする

デフォルトのインターセプター

 バリデーション処理では、実装さえすれば、特に呼び出し処理を記述する必要はありませんでした。それは、デフォルトで呼び出されるインターセプターがあり、バリデーションもその一つだからです。

 あらかじめ用意されているインターセプターの定義は、デフォルトの設定ファイルのstruts-default.xmlにあります。そこで定義されているインターセプターは30ほどあり、そのうちの半数ほどが、デフォルトのインターセプターとして実行されるようになっています。デフォルトのインターセプターは、多くのアプリケーションで必要となる機能を定義したものです。ちょっとしたアプリケーションなら、デフォルトのインターセプターを変更したり、別のインターセプターを追加したりする必要はないでしょう。

 なお、設定ファイルのstruts-default.xmlとは、struts.xmlで「extends="struts-default"」と指定して読み込んでいるデフォルトの設定ファイルのことです。

 以下が、struts-default.xmlで、デフォルトのインターセプターを定義している箇所です。

[リスト1struts-default.xmlの一部

<interceptor-stack name="defaultStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="alias"/>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="i18n"/>
    <interceptor-ref name="chain"/>
    <interceptor-ref name="debugging"/>
    <interceptor-ref name="profiling"/>
    <interceptor-ref name="scopedModelDriven"/>
    <interceptor-ref name="modelDriven"/>
    <interceptor-ref name="fileUpload"/>
    <interceptor-ref name="checkbox"/>
    <interceptor-ref name="staticParams"/>
    <interceptor-ref name="params">
      <param name="excludeParams">dojo\..*</param>
    </interceptor-ref>
    <interceptor-ref name="conversionError"/>
    <interceptor-ref name="validation">
      <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
    <interceptor-ref name="workflow">
      <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
</interceptor-stack>

 インターセプターに関する設定タグについては、あらためて後述しますが、<interceptor-stack>タグで、インターセプターをグルーピングしています。ここでは、defaultStackというインターセプターのグループ名を指定しています。また、<param name="excludeMethods">とは、指定されたメソッドの場合、そのインターセプターを実行しないという意味です。

アスペクト指向とは

 アスペクト指向(Aspect-oriented)とは、オブジェクト指向の問題点を補うために考案された概念です。オブジェクト指向ではうまく表現できない、クラス間を横断するような機能を「アスペクト(英単語としては、外観や様相といった意味)」とみなし、そのアスペクトをモジュール化するプログラミング技法を、アスペクト指向プログラミング(AOP)と呼びます。

 まさにインターセプターとは「アスペクト」であり、Struts 2では、インターセプターという手法でアスペクト指向プログラミングを実装していることになります。

独自のインターセプター

 では、かんたんなインターセプターを作ってみましょう。自作したインターセプターで動きを確かめてみます。

Interceptor本体

 インターセプター本体は、com.opensymphony.xwork2.interceptor.Interceptorインターフェイスを実装する必要があります。Interceptorインターフェイスには、下記のように3つのメソッドが定義してあり、すべて実装しなければなりません。

 ただし、com.opensymphony.xwork2.interceptor.AbstractInterceptorというクラスで、Interceptorインターフェイスのinitメソッドとdestroyメソッドが実装されています。内容のない空のメソッドが定義されているだけですが、これを継承してインターセプターを作ると、不要な実装を記述する手間が省けます。

[リスト2Interceptorインターフェイスの定義

public interface Interceptor extends Serializable {
    void destroy();
    void init();
    String intercept(ActionInvocation invocation) throws Exception;
}

void init()

 Servletコンテナ(Tomcat)を起動すると呼ばれます。

void destroy()

 Servletコンテナの終了時に呼ばれます。

String intercept(ActionInvocation invocation)

 このメソッドに、インターセプターの処理を記述します。パラメータのActionInvocationオブジェクトを使って、リクエストパラメータやセッションにアクセスします。

 ActionInvocationオブジェクトは、Actionクラスを実行したときのステータス情報を格納しており、Actionとインターセプターのインスタンスも保持しています。

 例えば、 次のようなコードでリクエストパラメータやセッションや取得することができます。

[リスト3interceptメソッド内に記述するコード

// セッションの取得
// Struts 2では、Mapオブジェクトとしてセッションが取得できる
Map<String, String> session = ActionContext.getContext().getSession();

// HttpServletRequestHttpSessionの取得
HttpServletRequest request = ServletActionContext.getRequest();
HttpSession session = ServletActionContext.getRequest().getSession();

 com.opensymphony.xwork2.ActionContextとは、セッション、リクエストパラメータ、ロケール情報などを保持するクラスで、org.apache.struts2.ServletActionContextは、ActionContextの派生クラスです。

 詳細は、オンラインのドキュメントを参照してください。これらのオブジェクトを利用して行えるのは、セッションやリクエストパラメータの取得、Actionオブジェクトの名前の参照、結果コードの参照や変更などです。

Interceptorのサンプルコード

 以下は、AbstractInterceptorを継承したインターセプターの例です。このクラスを、連載で使用しているサンプルプロジェクトに組み込んでみましょう。

[リスト4SimpleInterceptor.java

package part4;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

public class SimpleInterceptor extends AbstractInterceptor {

    private static final long serialVersionUID = 1L;

    // インターセプターの処理(コンソールに文字列を出力する)
    public String intercept(ActionInvocation invocation) throws Exception {

        System.out.println("intercept処理");

        // 次のインターセプター処理
        String result_code = invocation.invoke();

        // Result後のインターセプター処理
        System.out.println("intercept処理2");

        return result_code;
    }
}

 このインターセプターは、コンソールに文字列を出力するだけのものです。interceptメソッドの最後で、次のインターセプター処理を継続するために、invocation.invoke()メソッドを呼んでいます。そのメソッドを呼び出さず、Actionクラスと同様に、直接、"success"などの結果のコードを返すことができます。その場合は、直後のActionクラスの実行もスキップされることになります。

 なお、invocation.invoke()から戻ってきた後は、結果の処理も終わっています。この例では、結果の画面が表示されてから、二度目のSystem.out.printlnの出力処理を行っています。

 次に、設定ファイルのstruts.xmlを変更します。

[リスト5struts.xml(追加)

<interceptors>

    <!-- interceptorの定義 -->
    <interceptor name="simple" class="part4.SimpleInterceptor"/>

    <!-- interceptorをグルーピング -->
    <interceptor-stack name="simpleInterceptorStack" >
        <!-- 呼び出したい順にinterceptorを記述する -->
        <interceptor-ref name="simple" />
        <interceptor-ref name="defaultStack"/>
    </interceptor-stack>

</interceptors>

<action name="Hello" class="part1.Hello">

    <!-- interceptorの指定 -->
    <interceptor-ref name="simpleInterceptorStack"/>

    <result name="success">/index.jsp</result>
</action>

 <interceptors></interceptors>タグ間で、インターセプターの定義をします。そして、<interceptor>タグのname属性で名称、class属性で、インターセプター本体のクラスを指定します。

 前述したように、<interceptor-stack>タグはインターセプターのグルーピング指定です。複数のインターセプターをまとめて定義することができます。

 Actionクラスごとに指定するインターセプターは、デフォルトのインターセプターとの差分だけという指定ができないので、基本的には常にデフォルトのインターセプター(struts-default.xmlで指定されたdefaultStack)も指定する必要があります。通常、複数のインターセプターを指定することになりますので、グルーピングの設定をしておくと、Actionに適用するインターセプターを1つのグループ名で指定できるようになります。

 <action>タグ内の<interceptor-ref>タグで、そのActionに適用するインターセプターを指定します。この指定がない場合は、defaultStackが指定されたことになります。

 なお、すべてのActionに適用する場合は、defaultStack自体のグルーピングをstruts.xmlにて上書き設定するか、<default-interceptor-ref>タグを使って、デフォルトのインターセプターを再定義するとよいでしょう(<default-interceptor-ref>タグを使う例は、後述します)。

Actionが実行された後のインターセプター

 Actionが実行された後(Resultオブジェクトが処理をする前)にも、インターセプターのメソッドを呼び出すことができます。これは、Action結果のコードを判断して、何らかの処理を行いたい場合や、強制的に結果コードを変更したいときに利用できます。

 Actionの後に実行するメソッドは、次のように、ActionInvocationクラスのaddPreResultListenerメソッドを用いて設定します。

[リスト6]変更したinterceptメソッド

public String intercept(ActionInvocation invocation) throws Exception {

    System.out.println("intercept処理");

    invocation.addPreResultListener(
        new PreResultListener() {
            public void beforeResult(ActionInvocation actioninvocation, String resultCode) {
                System.out.println("Result:"+resultCode);
           }
       }
    );

    // 次のインターセプター処理
    String result_code = invocation.invoke();

    // Result後のインターセプター処理
    System.out.println("intercept処理2");

    return result_code;
}

 com.opensymphony.xwork2.interceptor.PreResultListenerは、beforeResultメソッドのみを定義したインターフェイスです。beforeResultメソッドが、コールバックとして実行されることになりますので、ここに処理を記述します。

 beforeResultメソッドには、Actionの結果コードがパラメータで渡されます。上記のコード例は、その結果コードをコンソールに出力するものです。

ログイン認証のインターセプター

 今度は、もう少し実用性のあるインターセプターを作ってみることにします。Webアプリケーションでおなじみの「ログイン認証」を実現するインタセプターです。

ログイン認証のフロー

 ログイン認証の処理を、Loginインターセプターとして実装します。Loginインターセプターの処理フローは、次の図のようになります。

4 Loginインターセプターの処理フロー図

図4 Loginインターセプターの処理フロー図

 サーバ上の、アプリケーション全体のファイル構成は次のようになります。part1part3のフォルダにあるファイルに変更はありません。

 <ContextRoot>

 ├ /WEB-INF

 │ ├ /classes

│ │ ├ /part4

 │ │ │ ├ SimpleInterceptor.class(追加)

 │ │ │ ├ LoginInterceptor.class(追加)

 │ │ │ └ Login.class(追加)

 │ │ └ struts.xml(更新)

 │ ├ /lib

 │ └ web.xml(変更なし)

 ├ /part4

 │ └ login.jsp(追加)

 index.jsp(変更なし)

 以下が、追加・変更するソース・設定ファイルです。順に説明していくことにしましょう。

§            struts.xml

§            LoginInterceptor.java(ログイン処理のインターセプター)

§            Login.java(ログインActionクラス)

§            login.jsp(ログイン画面)

 なお、ログインActionクラスのLogin.javaLogin.class)は図にないファイルですが、これは、ログイン画面からsubmitされるActionクラスです。ただ、ログイン処理は、すべてLoginインターセプターで行いますので、Loginインターセプターを起動するためだけの、ダミーのActionクラスです。

struts.xml

 struts.xmlには、次の定義を追加します。

[リスト7struts.xml(追加)

<interceptors>
    <!-- interceptorの定義 -->
    <interceptor name="simple" class="part4.SimpleInterceptor"/>

    <!-- interceptorの定義 -->
    <interceptor name="login" class="part4.LoginInterceptor" />

    <!-- interceptorをグルーピング -->
    <interceptor-stack name="myDefaultStack">
        <!-- 呼び出したい順にinterceptorを記述する -->
        <interceptor-ref name="simple" />
        <interceptor-ref name="login" />
        <interceptor-ref name="defaultStack" />
    </interceptor-stack>
</interceptors>

<!-- デフォルトinterceptorを再定義 -->
<default-interceptor-ref name="myDefaultStack"/>

<global-results>
    <result name="login">/part4/login.jsp</result>
    <result name="login-success">/index.jsp</result>
</global-results>

<action name="Login" class="part4.Login">
    <result name="success">/index.jsp</result>
</action>

 <interceptor>タグで、LoginInterceptorクラスをloginという名前で定義しています。そして、このインターセプターと、先ほどのSimpleInterceptor、およびdefaultStackをまとめて、myDefaultStackというインターセプターグループにします。

 <default-interceptor-ref>タグでは、上記のグループを、デフォルトのインターセプターとして再定義しています。

 <global-results>タグは、文字通り、すべてのActionクラスの結果を受けることができます。通常は、個別のActionごとに、結果コードの表示先を指定しますが、<global-results>タグにより、すべてのActionクラスの結果を共有したViewを設定できます。

 ここでは、未ログインのときの結果コード「login」でログイン画面の呼び出し、ログイン認証が完了したときの結果コード「login-success」で、index.jspを呼び出すようにしています。

LoginInterceptor.java

 LoginInterceptorinterceptメソッドは、次のようになります。

[リスト8LoginInterceptor.javaの一部

public String intercept(ActionInvocation invocation) throws Exception {

    // HttpServletRequestHttpSessionの取得
    HttpServletRequest request = ServletActionContext.getRequest();
    HttpSession session = ServletActionContext.getRequest().getSession();

    // (1) ログインしていれば、次のインターセプターへ
    if ( session.getAttribute("userid") != null &&
            session.getAttribute("userid").equals("part4") ){
        return invocation.invoke();
    }

    // (2) リクエストパラメーターのuseridとpasswordを取得
    String userid = request.getParameter("userid");
    String passwd = request.getParameter("password");

    System.out.println(userid);
    System.out.println(passwd);

    if ( userid != null && passwd != null &&
            userid.equals("part4")  && passwd.equals("wings") ) {

        // (3) 新たなセッションにuseridを設定する
        ServletActionContext.getRequest().getSession(true).invalidate();
        HttpSession newsession = ServletActionContext.getRequest().getSession(true);
        newsession.setAttribute("userid", userid );
        return "login-success";
    }
    return "login";
}

0 件のコメント:

コメントを投稿