2008-04-30

ApacheのDEFLATEフィルタで Flexアプリケーションの通信量を減らす

Flexアプリケーションがサーバ側のオブジェクトとHTTPでバイナリ通信する時の Content-typeは application/x-amf となっている。
この Content-typeに対し、Apacheの DEFLATEフィルタを適用してやれば、データ圧縮により Flexアプリケーションの通信量を減らすことが出来る。

AddOutputFilterByType DEFLATE application/x-amf

データの内容によるが、これだけで通信量がだいたい 1/4 から 1/5程度になる。

ラベル:

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アプリケーションの認証情報をセッションへバインドすることが出来るのである。

続く

ラベル: ,

2008-04-27

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

一般的に、RIAではその設計上サーバサイドでユーザの状態を持つ必要がほとんどない。例えば、Flexなら、ショッピングカートを実装するためにわざわざサーバ側でセッション変数を使わなくとも、Flexアプリケーションの内部変数、つまり ActionScriptの変数でカートの中身を保持すれば良い。

しかしながら、RIAであってもセッション変数を使ってサーバ側で保持せざるを得ないデータがひとつだけある。それは認証に関するものだ。認証はユーザがアプリケーションを起動した際に一度だけ行われ、それ以降はIDやパスワードのやりとりをせずとも済むのが好ましい。そのためには、認証情報をリモートオブジェクトのコンテナたるサーバ側でセッションに関連づけて保持する必要がある。

もし認証情報をサーバ側で保持しないとなれば、ユーザの特定や正当性検証を行うためリモートオブジェクトのあらゆるメソッドに引数としてID/パスワードが必要となってしまうことになり大変不経済であるのは想像に難くないだろう。

ここでは、BlazeDSを使って Flex向けのサービスを構築する際にセッション変数の扱いをどのように行うことが出来るかをまず説明する。

BlazeDS経由で呼び出されるリモートメソッドの中では、FlexContextクラスのstaticメソッド getHttpRequest() で HttpServletRequestのインスタンスを得ることが出来る。ここからさらに getSession()をコールすることによりセッションオブジェクトへアクセス出来るため、通常のサーブレットプログラミングと同様のやり方でセッション変数を保持したり読み出したりすることが可能だ。

HttpSession session = FlexContext.getHttpRequest().getSession();
session.setAttribute("mysessionobject", value);

クライアント)
ID/パスワードを引数とし、サーバ側の認証メソッドをコールする

サーバ)
ID/パスワードの検証を行い、正当であればセッション変数にユーザIDを保持しtrueを返す

クライアント)
認証メソッドがtrueを返してきたら、ステートを切り替えてユーザの操作を可能にする。

クライアントはサーバ側の様々なメソッドを必要に応じて呼び出す
サーバは「セッション変数にユーザIDが入っているか確認し、もし未認証状態であれば例外をスローする(この場合、Flex側ではFaultイベントが発生することになる)」という処理をあらゆるメソッドの先頭で行う。

この方法で、認証処理をアプリケーション起動時の1回だけに抑えつつも、認証を通過していないセッションからの不正なメソッド呼び出しを防ぐことが出来る。・・・・ひとまずは。

しかし、この方法はいくつかの問題を抱えている。
続く。

ラベル: ,

2008-04-21

More alternative of BlazeDS - PHP and WebORB

PHP(5以降) に WebORBを適用すると、Flexの mx:RemoteObject から呼び出し可能なサービスを PHPのクラスとして記述することが出来る。

Flex(クライアントアプリケーション)側は、通信クライアントとなるオブジェクトとして mx:RemoteObject を使用する。

Webサーバ側は、任意の場所に配置された weborb.phpを通信のエンドポイントとし、クライアントからの呼び出しを受け付ける。このweborb.phpスクリプトが、呼び出し対象となるサービスへFlexアプリケーションからのリクエストをディスパッチする。ここでいうサービスとは、Servicesディレクトリに配置された(クラスを含む)PHPスクリプト。

Webサーバ側では下記の準備をする。

・エンドポイントとなるスクリプト weborb.phpを配置する
・同じ階層に Weborb フォルダをコピーする。この中身はさしあたり変更しなくても良い
・同じ階層に Services フォルダを作成する。この中に自分のクラスを作成する
・(必要に応じて) WeborbフォルダやServicesフォルダへのHTTP経由での直接アクセスを禁止する

Servicesフォルダの中に作成するスクリプトはこのようにする。
(この例ではファイル名を MyService.php とする)

<?php
class MyService
{
public function sayHello()
{
return "Hello, World!";
}
}
?>

Flex側
<mx:RemoteObject id="srv" destination="MyService"
endpoint=
"http://localhost/~shimarin/weborb.php"/>
<mx:TextInput text="{srv.sayHello.lastResult}"/>
<mx:Button label="Say Hello" click="srv.sayHello()"/>

ラベル:

2008-04-08

Carbon Emacsで ruby-modeと日本語

2008年4月5日版の Carbon Emacsでは、ruby-modeで日本語含みのスクリプトをsaveしようとすると

ruby-mode-set-encoding: Symbol's function definition is void: coding-system-to-mime-charset

といわれてしまって出来ない。

/Applications/Emacs.app/Contents/Resources/site-lisp/ruby-mode.el を開き、
add-hook 'before-save-hook と書いてある行をコメントアウトして保存し、
M-x byte-compile-file で変更した ruby-mode.elをコンパイルする(ruby-mode.elcが出来る)ことでとりあえずsaveは出来るようになる。