2008-04-30

BlazeDSとセッション変数、Springのsessionスコープ その2

前回からの続き

Flexアプリケーション向けのサービスをBlazeDSで構築するにあたり、認証をどのように扱うかという問題について、

a) FlexContext経由で HttpSessionを使ってセッション変数へアクセスできる
b) aを利用し、認証メソッドが一度呼び出され成功した際にセッション変数へ認証情報をセットする
c) aを利用し、各メソッドの先頭に認証状態のチェックを行う処理を入れる

というソリューションを解説した。しかし、この方法にはいくつかの問題点がある。最近の Java設計者であればすぐに下記の2点を解決したいと考えるはずだ。

α - ひとつは、FlexContextや HttpServletRequestといったコンテキスト依存の APIを使用することでコンポーネントの独立性が低下し(ひらたい言い方をすると、POJOでなくなる)、再利用の機会を喪失するうえユニットテストもやりにくくなること。

β - もうひとつは、全てのメソッドに同じ処理(この場合は認証状態のチェック処理)をコピー&ペーストで差し込む行為が一般的には忌避すべきものであるということ。

このような問題を解決するために、まさにDI/AOPコンテナが存在することは言うまでもない。
幸い、BlazeDSは Spring Frameworkと一緒に使うことが出来る(以前のブログエントリを参照)。

αの問題を解決するにあたっては、DIを用いる。
「臭いものに蓋をする」の考え方で、FlexContext, HttpServletRequestといったものを隠蔽し純粋に必要なデータのみの入出力を定義するためのインターフェイスを抽出する。このインターフェイスは BlazeDSや Servlet APIに依存しない。

interface SessionData {
public void setUserId(String userId);
public String getUserId();
}

ユニットテスト用の実装を、特別なAPIを一切用いないで作成する。

public class SessionDataMock implements SessionData {
private String userId;

public void setUserId(String userId)
{
this.userId = userId;
}
public String getUserId()
{
return this.userId;
}
}

同じインターフェイスでデプロイ用の実装を作成する。

public class SessionDataImpl implements SessionData {
private HttpSession getSession()
{
return FlexContext.getHttpRequest().getSession();
}

public void setUserId(String userId)
{
getSession().setAttribute("userId", userId);
}
public String getUserId()
{
return (String)getSession().getAttribute("userId");
}
}

SessionDataの実装は下記のように使われるだろう。

// 認証処理が成功したら、セッションにユーザIDをセットする
if (doAuth(userId,password)) sessionData.setUserId(userId);

// セッションからユーザIDを取得する。認証されていない場合は例外を送出する。
String userId = sessionData.getUserId();
if (userId == null) throw new AuthenticationRequiredException();

ユニットテストの際にはSessionDataMock、デプロイの際にはSessionDataImplをビジネスコンポーネントへ注入してやるような使い分けをすれば、「セッションデータへアクセスするためのAPIが実行環境に依存してしまうためビジネスコンポーネントのユニットテストが出来ない」という事態を避けることが出来る。

と、ここまでは Spring 1.xまでの場合である。
Spring 2.0以降では、Webアプリケーションの場合に限って利用可能な Beanのスコープとして "request" "session" が追加されている。
ここではセッションに対して値をバインドしたいので、

public class SessionData {
private String userId;

public void setUserId(String userId)
{
this.userId = userId;
}
public String getUserId()
{
return this.userId;
}
}

のようなバリューオブジェクトを作成して scope="session" のBeanとして定義し、必要なコンポーネントへ注入してやれば良い。例えば Bean定義ファイルは下記のようになる。

<bean id="sessionData" class="com.acme.SessionData" scope="session">
<aop:scoped-proxy/>
</bean>

<bean id="myService" class="com.acme.MyService">
<property name="sessionData" ref="sessionData"/>
</bean>

sessionスコープで定義された Beanのインスタンスは、HTTPセッション毎にそれぞれ1個生成される。つまりこの Beanのクライアント(利用側)である myServiceは、そのまま sessionDataの setter/getterを呼び出すことでセッションデータの読み書きと同等のことが出来ることになる。これならば FlexContextも HttpServletRequestも HttpSessionも全く自分のコードに登場させる必要がないうえ、この SessionDataクラスはコンテキストに依存しないため単純に new して使用することも可能であり、ユニットテストの際にも一切変更することなく使える(=デプロイ用とユニットテスト用に複数の実装を用意する必要がない)のである。

賢明なエンジニアであれば、ライフサイクルの異なる Bean同士をワイヤリングする、つまりsingleton(デフォルト)スコープのBeanへ sessionスコープの Beanを注入することに無理があると感じるかもしれないが、実は Spring 2以降では、<aop:scoped-proxy/>要素を Bean定義内に配置することによりライフサイクルの差異を吸収する Proxyオブジェクトを自動的に生成してくれるようになっておりコーディング時には意識する必要がない(もっとも、意識しはじめてしまうと気持ちの悪い動作に思えてくるかもしれないが・・・)。

なお Springで Webアプリケーション特有のスコープを用いる場合、web.xmlに下記のリスナーを登録する必要がある。

<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>

</listener>


以上のように BlazeDSと Springを一緒に使うことによって、”BlazeDS、サーブレットAPI、ましてやSpringのAPIをも一切使わずに" Flexアプリケーションの認証情報をセッションへバインドすることが出来るのである。

続く

ラベル: ,

0 件のコメント:

コメントを投稿

<< ホーム