2008-05-26

Flexと JRubyで作るリアルタイム投票システム

video
※音が出るので注意※

シンプルな人気投票のプログラムなんだけど、Flex/BlazeDSのメッセージング機能により他人の投票もリアルタイムに見ることが出来るのが特徴。

操作してみたい人はこちら(期間限定、飽きたら消えるので悪しからず)
微妙なリアルタイム投票システム
多人数で操作したほうが楽しいので皆様お誘い合わせの上どうぞ。
一人でもブラウザを複数枚開いて操作してみれば意味がわかるかも(うるさいけど)。
あと、右クリックで View Sourceも出来るようにしてみた。

技術的トピックなど


  • Flex + BlazeDS + Spring + JRuby + ActiveRecord で実現。Railsは使っていない。

  • BlazeDSの Pub/Subメッセージングも JRubyから使ってみた(参考記事: AIRとBlazeDSとRubyでメッセンジャーを作る)。参考記事では、メッセージングにのみ BlazeDSを使っており通常のリクエストは JRuby on Railsで受けているようだ。

  • データベースは H2を使ってみた。これについては特記事項なし

  • Migrationでのスキーマ管理は Railsがなくても出来るらしいけどなんだか大変そうなのであきらめて普通にDDLを書いた

  • パイチャートの表示には、Professional版のFlex Builder にしかついていないチャーティングコンポーネントを使用。Adobeもエンタープライズ向けの有料コンポーネントがこんな使われ方をするとは夢にも思っておるまい

  • サーバ側ソース


    Flexに対する公開インターフェース ( VoteService )

    package net.stbbs.jrubytest;

    import java.util.Map;

    public interface VoteService {
    public Map getProgress();
    public void doVote(String name, String comments);
    }

    VoteServiceの実装が下記。Springの Bean定義ファイル内にそのまま Rubyで書き込んでいる。なお、標準Rubyライブラリや RubyGemsを使うためにアプリケーションサーバに対して VM引数 jruby.homeの設定をしてやる必要がある。

    <lang:jruby id="voteService"
    script-interfaces=
    "net.stbbs.jrubytest.VoteService"
    init-method=
    "init">
    <lang:inline-script>
    <![CDATA[
    require 'rubygems'
    require 'active_record'
    include_class 'flex.messaging.MessageBroker'
    include_class 'flex.messaging.util.UUIDUtils'
    include_class 'flex.messaging.messages.AsyncMessage'

    class Nomination < ActiveRecord::Base
    has_many :comments, :order=>"id"
    end

    class Comment < ActiveRecord::Base
    belongs_to :nomination
    end

    class VoteService
    def setJndiName(jndiName)
    @jndiName = jndiName
    end
    def setDDL(ddl)
    @ddl = ddl
    end
    def init
    #logger = ActiveRecord::Base.logger = Logger.new(STDOUT)
    ActiveRecord::Base.allow_concurrency = true

    ActiveRecord::Base.establish_connection(
    :jndi=>@jndiName, :adapter => 'jdbch2')
    begin
    ActiveRecord::Base.connection.execute(@ddl)
    Nomination.transaction {
    Nomination.create(:name=>"konata", :full_name=>"泉 こなた")
    Nomination.create(:name=>"kagami", :full_name=>"柊 かがみ")
    Nomination.create(:name=>"tsukasa",:full_name=>"柊 つかさ")
    Nomination.create(:name=>"miyuki",:full_name=>"高良 みゆき")
    }
    rescue Exception=>e
    print e.backtrace
    raise e
    end
    end

    def getProgress
    progress = Hash.new
    Nomination.find(:all).each {|n|
    progress[n.name] = {
    :full_name=>n.full_name,
    :votes_obtained=>n.votes_obtained,
    :recent_comments=>n.comments.last(5).collect {|c|
    {:comment=>c.comment}
    }
    }
    }
    progress
    end

    def doVote(name, comment)
    Nomination.transaction {
    n = Nomination.find_by_name(name)
    return if n == nil
    if comment != nil && comment != "" then
    Comment.create(:nomination_id=>n.id, :comment=>comment)
    end
    n.votes_obtained += 1
    n.save
    }
    broker = MessageBroker.getMessageBroker(nil)
    msg = AsyncMessage.new
    msg.setDestination("vote")
    msg.setMessageId(UUIDUtils.createUUID)
    msg.setBody({:name=>name,:progress=>getProgress})
    broker.routeMessageToService(msg, nil)
    end
    end

    VoteService.new
    ]]>
    </lang:inline-script>
    <lang:property name="jndiName" value="java:/comp/env/jdbc/vote"/>
    <lang:property name="DDL">
    <value>

    <![CDATA[
    drop table if exists nominations;
    create table if not exists nominations (
    id integer IDENTITY primary key,
    name varchar not null,
    full_name varchar not null,
    votes_obtained integer default 0
    );
    create unique index nominations_idx on nominations(name);
    drop table if exists comments;
    create table if not exists comments (
    id integer IDENTITY primary key,
    nomination_id integer not null,
    comment varchar not null,
    created_at timestamp not null
    );
    ]]>
    </value>
    </lang:property>
    </lang:jruby>

    BlazeDSの設定


    BlazeDSの設定は WEB-INF/flex/services-config.xmlに書くのが通常だが、Springの applicationContext.xmlで設定してしまえたほうが個人的に気分が良かったので Spring用のカスタム ConfigurationManagerを書いた。これはそれを使った場合の設定。なので自分にしか役に立たない。ごめん。
    net.stbbs.blazeds.springパッケージは時間が出来たら githubあたりで公開したいと思うんだけど、あきらかに自分が使わない設定項目については対応していないし対応する気にもならないのでどうしたものか。

    <!--
    BlazeDS用コンフィギュレーション
    MessageBrokerServletの services.configuration.managerパラメータに
    net.stbbs.blazeds.spring.FlexSpringConfigurationManagerを与えると、
    WEB-INF/flex/services-config.xmlの代わりにこちらが使われる
    -->

    <bean class="net.stbbs.blazeds.spring.MessagingConfiguration">
    <property name="channelSettings">
    <list>
    <!-- 非ポーリングAMFチャンネル (RPC用) -->
    <bean class="net.stbbs.blazeds.spring.AMFChannelSettings">
    <constructor-arg value="my-amf"/>
    <!-- mx:RemoteObjectの endpointプロパティにセットするURL -->
    <property name="uri"
    value=
    "http://{server.name}:{server.port}/{context.root}/messagebroker/amf"/>
    </bean>
    <!-- ポーリングAMFチャンネル (Pub/Sub用) -->
    <bean class="net.stbbs.blazeds.spring.AMFPollingChannelSettings">
    <constructor-arg value="my-polling-amf"/>
    <!-- mx:AMFChannel の uri プロパティにセットするURL -->
    <property name="uri"
    value=
    "http://{server.name}:{server.port}/{context.root}/messagebroker/amfpolling"/>
    </bean>
    </list>
    </property>
    <property name="serviceSettings">
    <list>
    <!-- リモーティング(RPC)サービス -->
    <bean class="net.stbbs.blazeds.spring.RemotingServiceSettings">
    <constructor-arg value="remoting-service"/>
    <property name="adapterSettings">
    <list>
    <bean class="net.stbbs.blazeds.spring.JavaAdapterSettings">
    <constructor-arg value="java-object"/>
    <property name="default" value="true"/>
    </bean>
    </list>
    </property>
    <!-- RPCサービスでは非ポーリングAMFを使用 -->
    <property name="defaultChannels">
    <list>
    <value>my-amf</value>
    </list>
    </property>
    <!-- Destinationアノテーション付き、又は RemotingDestinationインターフェイスつきの
    Beanを自動的に destinationとして登録する -->

    <property name="destinationsByAnnotation" value="true"/>
    </bean>

    <!-- Pub/Subサービス -->
    <bean class="net.stbbs.blazeds.spring.MessageServiceSettings">
    <constructor-arg value="messaging-service"/>
    <property name="adapterSettings">
    <list>
    <bean class="net.stbbs.blazeds.spring.ActionScriptAdapterSettings">
    <constructor-arg value="actionscript"/>
    <property name="default" value="true"/>
    </bean>
    </list>
    </property>
    <!-- Pub/SubサービスではポーリングAMFを使用 -->
    <property name="defaultChannels">
    <list>
    <value>my-polling-amf</value>
    </list>
    </property>
    <!-- destination(JMSでいうところの Topic)を定義 -->
    <property name="destinationNames">
    <list>
    <value>vote</value>
    </list>
    </property>
    </bean>
    </list>
    </property>
    </bean>

    個人的なメモ


    activerecord-jdbc-adapterはバージョン 0.8から JRuby 1.1専用になってしまった。仕方ないので DBMS特有のモジュールも含めバージョン 0.7.2をインストールする必要がある。

    jruby -S gem install activesupport
    jruby -S gem install activerecord
    jruby -S gem install -r activerecord-jdbc-adapter -v 0.7.2
    jruby -S gem install -r activerecord-jdbch2-adapter -v 0.7.2

    Gentoo(Portage)の JRuby 1.0.3は Java 1.6.0で動かせないみたい。仕方ないのでデフォルトJVMを 1.5.0に切り替え。
    rubygemsをインストールしようとしたら OutOfMemoryErrorが出る。仕方ないので/usr/bin/jrubyを編集してVMオプションに -Xmx512mをつけた。そしたら 512でも足りなかった。64bitだからってそんなに使いますか。1024で解決。

    PortageのJRubyGemsから参照されているutf8procのネイティブ(utf8proc_native.so)部が怪しくてActiveSupportをロードしたときにエラーが出る。lddで見てみたが特に外のライブラリ(iconvとか)に依存してるわけではなさそうで、外的要因じゃなければと仕方なく /usr/share/jruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/multibyte/chars.rbの最後の方で「ネイティブ版のutf8procが使えたらそっちを使う」ってなってる所を手で削除した。

    関連記事



    Springのスクリプト言語サポートを使って BlazeDSと JRubyを統合する

    実際に BlazeDS経由で ActionScript3から呼び出せるように Rubyスクリプトを配置する方法

    なぜ Flex向けのサービスをRubyで記述することにこだわるのか

    こっちの記事は Flex - JRubyの実用的なシチュエーションについて

    ラベル: ,

    2008-05-22

    なぜ Flex向けのサービスをRubyで記述することにこだわるのか

    最近のエントリを見てわかるように、しばらく BlazeDS, Spring, JRubyのソースを追いかけたり書き換えたりしていた。なぜそうまでして Flexから JRubyを使いたいのか書いておくことにする。

    video
    ※音が出るので注意※

    この動画は「とあるサービスにおいてデータベースからメンバー検索を行う」機能をRIAとして実装することを想定した画面のサンプル(と思っていただきたい。実際に業務でこんなものを作ったわけではないので悪しからず)の作動する様子なのだが、このアプリケーション、実はモックアップである。サーバと通信はするものの、サーバサイドではデータベースへ問い合わせをせずにプリセットの固定データからレコードを検索して返すようになっている。

    顧客を交えてシステムの画面設計を進める際は、このような「実際に動作する」モックアップを素早く製作して提示し、食い違いがあればすぐに修正する(場合によっては、捨てて作り直す)というプロセスを繰り返すことで安心感を得るとともに食い違いのリスクを軽減することが出来る。

    このモックアップの特徴は、実際にサーバと通信を行うことである。すなわち、モックアップを製作する段階で画面デザインのみならず システムの入出力仕様(ユーザはシステムに何を入力して何を得るか)まで決まるということだ。単に画面デザインを提示して議論するよりも、システムの辻褄が合わない所やお互いの理解や考えが足りていない部分を早期に洗い出すことが出来る分そのほうが合理的だし、極端な話をすると全ての入出力について仕様が決まればそれより後ろ側のことはデータベース設計も含めて他の者へ任せてしまうことすら出来るので、設計の分業も可能になる。効果的な分業により(理論上は)開発組織全体のスループットを上げられる。

    さて、アジャイル式に則り、実際に顧客の目の前でモックアップを製作したり修正する課程を想像してみよう。

    まずは GUIのデザインである。テキストボックス、データグリッド、ボタンを配置していく様は、顧客の目から見てもわかりやすい作業であるため、彼らの興味を引き一緒に考えることが出来るだろう。Adobeの手先ではないが、幸いにして Flex Builderならば、VisualBasicやDelphiばりの早さで GUIをデザインできるため比較的このようなやり方に現実性がある。また、サーバと通信する部分はActionScriptによるコーディングが必要だが、ActionScriptからサーバ側の Javaメソッドを呼び出すには単純な関数呼び出しのように少ないコードを書くだけで済む。

    いっぽうサーバ側のコードはどうだろうか。データベースへアクセスしないモックアップとはいえ、これを長々と書いていると顧客が寝てしまうため迅速に書き下ろさなくてはならない。

    そう、きわめて迅速にである。

    ・・・つまりここで Rubyが登場する。

    UI 側に公開するインターフェイス


    このインターフェイスがすなわち、動画でお見せした画面からの入出力(呼び出し)仕様となる。
    BlazeDSの仲介により、公開メソッドは ActionScript3から直接呼び出せる。従来のWebアプリケーションでやったように、つまらないコントローラクラスやForm Beanを作る必要は無い。

    返値の型がMapだったりするのは仕様としてどうかという声もあると思うが、そのくらいなら自分のオフィスに戻ってから直しても遅くないだろう。
    package net.stbbs.jrubytest;

    import java.util.Collection;
    import java.util.Map;

    public interface MemberService {
    /**
    * メンバーを検索し、結果をコレクションで返す
    * @param nameOrProperty 名前又は属性名に対して有効な検索語。
    * nullの場合全てのレコードがマッチする
    * @return 検索にマッチしたレコードのコレクション
    */

    public Collection<Map> searchMember(String nameOrProperty);
    }

    公開インターフェイスが決まったら、それを実装する。Spring Frameworkのスクリプト言語サポートを用いれば、Javaで書いたインターフェイスを Rubyで実装することが可能だ。

    rubyで実装した場合

    class MemberService 
    def searchMember(nameOrProperty)
    [
    {:id=>'konata',
    :name=>'泉 こなた',:birthday=>'05月28日',:hometown=>'埼玉県',
    :bloodtype=>'A',:breasts_rank=>'極小',:height=>142,
    :properties=>{'オタク'=>5,'胸'=>1,'アホ毛'=>5,'運動神経'=>5,'需要'=>4}},
    {:id=>'tsukasa',
    :name=>'柊 つかさ',:birthday=>'07月07日',:hometown=>'埼玉県',
    :bloodtype=>'B',:breasts_rank=>'小',:height=>158,
    :properties=>{'バルサミコ酢'=>5,'巫女'=>3,'ドジっ娘'=>4,
    'どんだけ〜'=>5,'妹属性'=>4}},
    {:id=>'kagami',
    :name=>'柊 かがみ',:birthday=>'07月07日',:hometown=>'埼玉県',
    :bloodtype=>'B',:breasts_rank=>'中',:height=>159,
    :properties=>{'ツインテール'=>5,'お姉ちゃん'=>3,'ツンデレ'=>5,
    'お菓子好き'=>4,'ツリ目'=>5}},
    {:id=>'miyuki',
    :name=>'高良 みゆき',:birthday=>'10月25日',:hometown=>'東京都',
    :bloodtype=>'O',:breasts_rank=>'巨',:height=>166,
    :properties=>{'天然'=>5,'巨乳'=>5,'眼鏡っ娘'=>5,'委員長'=>5,'虫歯'=>5}}
    ].find_all {|m|
    nameOrProperty == nil \
    || m[:name].include?(nameOrProperty) \
    || m[:properties].any?{|k,v| k.include?(nameOrProperty) }
    }
    end
    end
    顧客が眠そうな顔をしていたら、もっと少ないレコード数でやめておくべきかもしれないし、逆に根気よく付き合ってもらえそうなら ActiveRecordを使って実際にデータベースへ入出力する所まで書けるかもしれない。みさおとあやのも入れてあげてと言われたら入れてさしあげるべきだろう。

    さて、これを Javaで実装したらどうなるだろう?

    Javaで実装した場合

    package net.stbbs.jrubytest;

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;

    public class MemberServiceImpl implements MemberService {

    protected Map createRecord(
    String id, String name, String birthday,
    String hometown, String bloodtype,
    String breasts_rank, int height, Map properties)
    {
    Map m = new HashMap();
    m.put("id", id);
    m.put("name", name);
    m.put("birthday", birthday);
    m.put("hometown", hometown);
    m.put("bloodtype", bloodtype);
    m.put("breasts_rank", breasts_rank);
    m.put("height", height);
    m.put("properties", properties);
    return m;
    }

    public Collection<Map> searchMember(String nameOrProperty) {
    Collection<Map> results = new ArrayList<Map>();
    Map p = new HashMap();
    p.put("オタク", 5);
    p.put("胸", 1);
    p.put("アホ毛", 5);
    p.put("運動神経", 5);
    p.put("需要", 4);
    results.add(createRecord(
    "konata", "泉 こなた", "05月28日", "埼玉県",
    "A","極小",142, p ));

    p = new HashMap();
    p.put("バルサミコ酢", 5);
    p.put("巫女", 3);
    p.put("ドジっ娘", 4);
    p.put("どんだけ〜", 5);
    p.put("妹属性", 4);
    results.add(createRecord(
    "tsukasa", "柊 つかさ", "07月07日", "埼玉県",
    "B", "小", 158, p ));

    p = new HashMap();
    p.put("ツインテール", 5);
    p.put("お姉ちゃん", 3);
    p.put("ツンデレ", 5);
    p.put("お菓子好き", 4);
    p.put("ツリ目", 5);
    results.add(createRecord(
    "kagami", "柊 かがみ", "07月07日", "埼玉県",
    "B", "中", 159, p ));

    p = new HashMap();
    p.put("天然", 5);
    p.put("巨乳", 5);
    p.put("眼鏡っ娘", 5);
    p.put("委員長", 5);
    p.put("虫歯", 5);
    results.add(createRecord(
    "miyuki", "高良 みゆき", "10月25日", "東京都",
    "O", "巨", 166, p ));

    if (nameOrProperty == null || "".equals(nameOrProperty)) return results;

    Collection<Map> searchResults = new ArrayList<Map>();
    for (Map m:results) {
    boolean hit = false;
    if ( ((String)m.get("name")).indexOf(nameOrProperty) >= 0) {
    hit = true;
    } else {
    for (Object property:((Map)m.get("properties")).keySet()) {
    if ( ((String)property).indexOf(nameOrProperty) >= 0) {
    hit = true;
    break;
    }
    }
    }
    if (hit) searchResults.add(m);

    }
    return searchResults;
    }
    }

    ・・・・・・・・・・・・・・寝る。確実に寝る。
    これを書いている途中で、1レコードの生成処理を別メソッドに分けるというリファクタリングまでしている。
    下手をすると顧客はその場を中座して煙草を吸いに行ったまま帰ってこないかもしれない。

    Ruby版のほうは、ほぼ本質的な内容(羅列したい固定レコードと簡単なマッチング条件)だけで占められているため非コーダーにとっても何となく分かるようなものだが、Java版はいかにもプログラムコードで、意味不明な単語の出現頻度があまりにも多い。

    誤解しないでいただきたいのは、Rubyの方が高い記述性のために優れており全てのシーンにおいて Rubyを選択するべきである、と言いたいわけではないことだ。

    はっきり言ってしまえば、よほどユニットテストのノウハウに自信があるのでなければ並のプログラマに Rubyを使わせるなどということはできない。静的言語のように制約を受けないから間違い放題だし、そもそも彼らは Rubyの力を使いこなすことが出来ないため Javaで書かせた場合と比べて記述が少なくもならない。つまり、良いことがまるでない。

    設計の局面で Rubyを使う理由としてここで語りたかったことをもう少し具体的に示すならば、アーキテクトが顧客との緊密な打ち合わせの上でモックアップを早く作ることによって設計を早期に固めてしまいさえすれば、後は並のプログラマに Javaで手堅く実際のロジックを書かせることで効果的に分業が出来るのではないかという話である。引数と返り値が決まっていて中身を実装するだけなら誰でも出来るのだから、アーキテクトはそんな誰でも出来る仕事を早く作って早く渡すのがその務めだろう(彼らの書いたコードの品質を制御する方法はまた別の問題だし、もしプロジェクトの規模が大きくドメインモデルのアーキテクチャを取ろうとするならばアーキテクトの仕事はもっと多くなるが)。

    少し無理な当てはめ方をするならば、アジャイル的方法と旧来のウォーターフォール的方法をハイブリッドにしたものと考えられなくもない。

    そのスタイルがうまく発展すれば、アーキテクトはギークなりスーパーコーダーとしての素養を発揮しつつも、外国の並以上かつ安価なプログラマを人材の補完に充てるという方法につなげることが出来るかもしれない。オフショア開発で起こる問題の原因は常に提示する仕様の曖昧さにあると考えられるが、仕様書の付録として「RPC込みで動作するモックアップ」があれば設計意図の伝わりやすさは格段に高いはずだし、設計自体の論理的誤りも少ないはずなので、きっとオフショアリスクはいくぶんかそこで軽減出来る。

    ついでだから Flexのソースも載せておく


    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
    backgroundColor=
    "white" verticalAlign="middle" horizontalAlign="center">
    <mx:Script>
    <![CDATA[
    import mx.collections.ArrayCollection;
    [Bindable] private var properties:ArrayCollection;
    private function doSearch():void
    {
    memberService.searchMember(
    nameOrProperty.text !=
    ""? nameOrProperty.text : null);
    }
    ]]>

    </mx:Script>
    <mx:RemoteObject id="memberService" destination="memberService"
    endpoint=
    "./messagebroker/amf"/>
    <mx:VBox>
    <mx:VBox width="100%" height="200"
    backgroundImage=
    "@Embed('assets/members.jpg')"
    verticalAlign=
    "middle" horizontalAlign="center">
    <mx:HBox verticalAlign="middle" horizontalAlign="center">
    <mx:Label text="名前又は属性で検索" color="#000000" fontSize="18"
    fontWeight=
    "bold"/>
    <mx:TextInput id="nameOrProperty" fontSize="18" enter="doSearch()"/>
    <mx:Button label="検索実行" fontSize="18" color="#000000" click="doSearch()"/>
    </mx:HBox>
    <mx:Label color="#000000" fontSize="16"
    text=
    "※空欄で検索すると全てのレコードが表示されます"/>
    </mx:VBox>
    <mx:HBox verticalAlign="middle">
    <mx:VBox>
    <mx:Label text="検索結果" fontSize="18"/>
    <mx:DataGrid id="results" height="250"
    dataProvider=
    "{memberService.searchMember.lastResult}">
    <mx:columns>
    <mx:DataGridColumn headerText="写真" dataField="id" width="50">
    <mx:itemRenderer>
    <mx:Component>
    <mx:Image source="images/{data.id}.png" height="48"/>
    </mx:Component>
    </mx:itemRenderer>
    </mx:DataGridColumn>
    <mx:DataGridColumn headerText="名前" dataField="name" width="90"/>
    <mx:DataGridColumn headerText="誕生日" dataField="birthday" width="70"/>
    <mx:DataGridColumn headerText="出身地" dataField="hometown" width="90"/>
    <mx:DataGridColumn headerText="血液型" dataField="bloodtype" width="50"/>
    <mx:DataGridColumn headerText="胸ランク" dataField="breasts_rank" width="50"/>
    <mx:DataGridColumn headerText="身長" dataField="height" width="60"/>
    </mx:columns>

    <mx:change>
    <![CDATA[
    if (results.selectedItem == null) {
    properties = null;
    return;
    }
    var snd:Sound = new Sound();
    var req:URLRequest =
    new URLRequest(
    "sounds/" + results.selectedItem.id + ".mp3");
    snd.load(req);

    properties = new ArrayCollection();
    var p:Object = results.selectedItem.properties;
    for(var i:Object in p) {
    properties.addItem({key:i,value:p[i]});
    }

    snd.play();
    ]]>

    </mx:change>
    <mx:valueCommit>
    <![CDATA[
    properties = null;
    ]]>

    </mx:valueCommit>
    </mx:DataGrid>
    </mx:VBox>
    <mx:Image source="@Embed('assets/rightarrow.png')"/>
    <mx:VBox>
    <mx:Label text="属性一覧" fontSize="18"/>
    <mx:DataGrid dataProvider="{properties}" selectable="false">
    <mx:columns>
    <mx:DataGridColumn headerText="属性名" dataField="key" width="80"/>
    <mx:DataGridColumn headerText="ランク" dataField="value" width="50"/>
    </mx:columns>
    </mx:DataGrid>
    </mx:VBox>
    </mx:HBox>
    </mx:VBox>
    </mx:Application>

    関連記事



    Adobe BlazeDSを自分のWebアプリケーションに組み込む

    Adobe BlazeDS(オープンソース)を使ってActionScript3からサーバ側の Javaメソッドをコールできるようにする設定方法

    An alternative of BlazeDS - Rails and WebORB

    ActionScript3からサーバ側の Rubyメソッドをコールできるようにする Railsプラグインの使い方

    More alternative of BlazeDS - PHP and WebORB

    ActionScript3からサーバ側の PHPメソッドをコールできるようにする PHPライブラリの使い方

    Springのスクリプト言語サポートを使って BlazeDSと JRubyを統合する

    実際に BlazeDS経由で ActionScript3から呼び出せるように Rubyスクリプトを配置する方法

    ラベル: ,

    2008-05-20

    Springのスクリプト言語サポートを使って BlazeDSと JRubyを統合する

    最初に言っておくが、Rubyといっても今回 Railsは使わない。

    BlazeDSのオブジェクトファクトリとして Springを用いる方法については過去のエントリで触れたとおり。

    これの延長で、Spring Frameworkがスクリプト言語の統合をサポートしている事を利用して Flex向けのサービスを Rubyで記述してしまおうという次第。

    Java VM上で Rubyスクリプトを動作させるには JRubyというインタプリタを使用する。Springは JRubyの他に Groovyと BeanShellをサポートしているが、日本人にとっては Rubyが最も馴染み深いだろう。
    (余談だが、宗教上の理由から国産のDIコンテナが Rubyをサポートすることは無さそうに見える。まったく不思議なことである)

    Rubyクラスを Springの Beanとして登録する方法は比較的簡単だ。

    • JRuby(Spring 2.5で使用できる最新のバージョンは現在のところ 1.0.3)、の配布から jruby.jarを探し出してクラスパスへ追加する

    • 公開インターフェイスを Javaで書く。面倒だがどうしても必要なようだ。

    • Bean定義ファイル(Webアプリケーションの場合たいがい WEB-INF/applicationContext.xmlだろう)へ、普通の bean 要素のかわりに lang:jruby 要素で Beanを定義することで Rubyスクリプトをロードし、Rubyクラスをインスタンス化させる。


    Rubyスクリプトを配置する方法は、Bean定義ファイルにインラインでそのまま書き込む方法と クラスパスの通った場所にスクリプトファイルを置く方法の二つある。後者の場合は変更を検出し自動でリロードさせることも可能なようだ。また、一貫して前者のスタイルでスクリプトを記述していけば「applicationContext.xmlの中に全てのビジネスロジックが集約されているアプリケーション」というおかしなものを作ることも可能である(有用性は特にない)。
    いずれの方法をとるにせよ、スクリプト内に日本語で文字列リテラルなどを書き入れると化けてしまう。これを回避するには Spring側にパッチを当てる必要がある

    具体例


    いつも通り適当で申し訳ないが、文字列を引数に取って文字列を返す sayHelloメソッドを持つサービス HelloServiceを例にとって具体例を挙げてみる。まずは HelloServiceの公開インターフェイスを Javaで作成する。

    package com.acme;

    public interface HelloService {
    public String sayHello(String yourName);
    }

    下記は、この Helloサービスを実装する Rubyクラスを "helloService" という名前のBeanとして定義ファイル内にインラインで記述する例である。

    <lang:jruby id="helloService" script-interfaces="com.acme.HelloService">
    <lang:inline-script>
    <![CDATA[
    class HelloService
    def sayHello(yourName)
    "Hello, " + yourName
    end
    end
    HelloService.new
    ]]>

    </lang:inline-script>
    </lang:jruby>

    最後に HelloServiceを newしているのは、このクラスのインスタンスが Springの管理すべき Beanのインスタンスであると明示するためである(スクリプト内には複数のクラス定義を書くことが出来るため)。

    さて、この helloServiceに対して sayHelloメソッドの呼び出しをしてやれば挨拶が返ってきそうであることは何となく想像できる。しかし、Javaで実装を1行も書いていないばかりか、Bean定義ファイル内にコードらしきものをさらっと書き込んだだけであるのにも関わらず本当にこんなものが Flexクライアントから呼び出せるのだろうか?

    答え:呼び出せる

    このサービスを BlazeDS経由で Flexから呼び出せるように公開するには、普通に Javaで実装されたサービスに対して行うのと同様 WEB-INF/flex/services-config.xmlの中に destinationを登録する。Springをオブジェクトファクトリとして使うので、factoryに springを指定している。BlazeDSに Springを統合する方法については、このエントリの冒頭でリンクしている過去エントリを参照のこと。

    <destination id="helloService">
    <properties>
    <factory>spring</factory>
    <source>helloService</source>
    </properties>
    </destination>

    このアプリケーションに jrubytestという名前を付けて実行させた。コンテキストルートは http://localhost:8080/jrubytest/ で、mx:RemoteObject向けの通信エンドポイントは http://localhost:8080/jrubytest/messagebroker/amf となる。これを起動したままにしてFlex側に移ろう。

    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:RemoteObject id="helloService" destination="helloService"
    endpoint=
    "http://localhost:8080/jrubytest/messagebroker/amf"/>
    <mx:VBox>
    <mx:HBox width="100%">
    <mx:Label text="お名前"/>
    <mx:TextInput id="yourName"/>
    <mx:Button label="送信">
    <mx:click>
    <![CDATA[
    helloService.sayHello(yourName.text);
    ]]>

    </mx:click>
    </mx:Button>
    </mx:HBox>
    <mx:Label width="100%" text="{helloService.sayHello.lastResult}"/>
    </mx:VBox>
    </mx:Application>

    Flex Builder上ではこのようなデザイン画面になる。


    アプリケーションを起動し、テキスト入力欄に文字列を入力して送信したところ、Rubyで実装したとおりの実行結果を正しく得ることが出来た。



    これで Flex - BlazeDS - Spring - JRubyの連携はとりあえず可能であるとわかった。
    つまり、Railsを使わずとも Rubyで FlexからRPCが可能なビジネスオブジェクトを実装し JavaVM上で作動させることが出来るということだ(ひとまず)。

    だが、文字列やプリミティブ型以外の引数や返り値を用いようとするといくらか JRubyや BlazeDSをハックする必要があった。詳細については下記エントリを参照されたし。
    JRuby for BlazeDS その1
    JRuby for BlazeDS その2

    時間があれば、さらに ActiveRecordを組み合わせる方法についても書くようにしたい。なにも ActiveRecordは Railsだけのものではない。

    追記


    Springの JRubyサポートを使って JSFとJRubyを組み合わせている人を発見したので参考までに。
    Creating JSF applications with JRuby and ActiveRecord-JDBC (Part 1)
    Creating JSF applications with JRuby and ActiveRecord-JDBC (Part 2)

    ラベル: ,

    JRuby for BlazeDS その2

    前回のエントリで、JRubyに対して Flex(BlazeDS)向けのハックをいくらか適用した。

    が、JRubyから Flexへ値を返す際にまだ下記の問題が残っている。

    1)ハッシュのキーを文字列でなくシンボルにすると、Flex側でプロパティが拾えない。
    2)ActiveRecordのエンティティをそのまま返すとFlex側でプロパティが拾えない。
    3)インスタンス変数持ちのオブジェクトを返しても、Flex側でプロパティが拾えない。

    これらはいずれも JRuby側のオブジェクトを ActionScriptの Object(動的プロパティ持ち)として返したい場合のものだ。
    ちなみに ActionScript3にはハッシュ型というものの用意がなく、Object型のオブジェクトに動的プロパティを与えることで代用するようになっている。

    BlazeDSでは、JavaBeansや Mapといったオブジェクトを AMFにシリアライズする際、それぞれの型にあった「プロパティプロキシ」のインスタンスを内部レジストリから取得し、それを経由して目的のオブジェクトから「キー:値」のエントリを取り出すようになっている。
    上に挙げた問題を解決するために、
    ・JRubyのハッシュ型(org.jruby.RubyHash)に対応するプロパティプロキシ RubyHashProxy
    ・JRubyのオブジェクト全般(org.jruby.RubyObject)に対応するプロパティプロキシ RubyObjectProxy
    を作成した。これらは Mapオブジェクトをシリアライズするためのプロキシである MapProxyがベースになっている。
    前者は1の問題、後者は2及び3の問題に対応する。

    これらの独自で作成したプロパティプロキシをBlazeDSの PropertyProxyRegistryに登録すれば、JRubyからの返り値が期待通りに ActionScript側へ プロパティのセットされた Objectとして届くようになる。特に ActiveRecordのオブジェクトをそのまま Flexへ返せるようになるのは威力絶大だ。

    しかし、PropertyProxyRegistryの設定は残念ながら設定ファイルで行うことが出来ない。
    仕方ないので Webアプリケーションの起動時に PropertyProxyRegistryへプロパティプロキシの登録を行うよう、web.xmlに登録するための簡単なリスナクラスも作成した。プロパティプロキシと合わせてソースを置いておく。

    jruby_property_proxies.zip

    ラベル: ,

    JRuby for BlazeDS その1

    JRubyで記述したサービスを BlazeDS経由で Flexから呼び出したくなった。
    実運用のためのサービスをJRubyで記述する気はないが、モックアップを手早く作り上げるのには適していると思ったからだ。

    Springが対応しているJRubyの最新版は現時点で 1.0.3である。
    そのまま使おうとすると下記の点で不便だ。

    Flex → JRuby


    ・ActionScriptの Date型が Rubyの Time型に自動変換されてくれない。
    java.util.Dateへの変換は BlazeDSが自動で行ってくれるが、JRuby1.0はそれを Rubyの Time型にまで変換してはくれない。

    ・ActionScriptの(動的プロパティ持ち)オブジェクトが文字列キーのハッシュになってしまう。
    今風に、ハッシュのキーはシンボルにしたい。person['name']じゃなくて person[:name]のほうがかっこいい。

    JRuby → Flex


    RubyのTime や Dateといったオブジェクトが ActionScriptのDate型に変換されてくれない。
    サーバ側で java.util.Dateにまで変換してやれば、ActionScript側には Date型で届いてくれるのだが、JRuby1.0はその変換をしてくれない。

    というわけでパッチ


    JRuby 1.0.3の org/jruby/javasupport/JavaUtil.java に対して下記のようなクイックハックを敢行した。

    38a39,41
    > import java.util.Calendar;
    > import java.util.Date;
    > import java.util.Map;
    43a47
    > import org.jruby.RubyHash;
    81c85,108
    <
    ---
    > } else if (javaClass == Date.class
    > || (javaClass == null && rubyObject.respondsTo("tv_sec"))
    > || (javaClass == null && rubyObject.respondsTo("yday")) ) {
    > // Timeオブジェクト用
    > if (rubyObject.respondsTo("tv_sec")) {
    > Long tv_sec =
    > ((RubyNumeric)rubyObject.callMethod(context, "tv_sec")).getLongValue();
    > return new Date(tv_sec * 1000);
    > }
    > // Dateオブジェクト用
    > if (rubyObject.respondsTo("yday")) {
    > long year =
    > ((RubyNumeric)rubyObject.callMethod(context, "year")).getLongValue();
    > long yday =
    > ((RubyNumeric)rubyObject.callMethod(context, "yday")).getLongValue();
    > Calendar cal = Calendar.getInstance();
    > cal.set(Calendar.YEAR, (int) year);
    > cal.set(Calendar.DAY_OF_YEAR, (int) yday);
    > cal.set(Calendar.MILLISECOND, 0);
    > cal.set(Calendar.SECOND, 0);
    > cal.set(Calendar.MINUTE, 0);
    > cal.set(Calendar.HOUR_OF_DAY, 0);
    > return cal.getTime();
    > }
    235c262,274
    <
    ---
    > } else if (javaClass == Date.class) {
    > // ActionScriptのDateオブジェクト用
    > return runtime.newTime(((Date)object).getTime());
    > } else if (javaClass == flex.messaging.io.amf.ASObject.class) {
    > // ActionScriptのオブジェクト用
    > RubyHash rh = new RubyHash(runtime);
    > for (Object e:((Map)object).entrySet()) {
    > Map.Entry entry = (Map.Entry)e;
    > rh.put(
    > runtime.newSymbol((String)entry.getKey()),
    > convertJavaToRuby(runtime, entry.getValue()));
    > }
    > return rh;

    以前のエントリに書いたが、今の Springは JRuby 1.1に対応していない。
    将来 Springが JRuby1.1に対応したらこのパッチは要らなくなるかもしれない。

    続く

    ラベル: ,

    2008-05-13

    WebORB PHPが呼び出し毎にやたら遅い時の対処

    そういう場合は、セキュリティ監査のために逆引きをしようとして遅いのかもしれない。
    とりあえず閉じたシステムだから WebORBに監査してもらわなくていいよという場合、
    Weborb/weborb-config.xml
    の中にある secure-resources, access-constraints の両要素を空にする(それぞれの要素自体は残すこと)と動作が速くなる。

    ラベル:

    2008-05-12

    JRuby 1.1が Spring Frameworkで使えない

    最新の JRubyを Springから使ってやろうとしたら、

    java.lang.NoSuchMethodError:
    org.jruby.Ruby.parse
    (Ljava/lang/String;Ljava/lang/String;Lorg/jruby/runtime/DynamicScope;I)Lorg/jruby/ast/Node;

    とか言われて使えなかった。どうやらJRuby側で 1.1からAPIが随分変わったそうだ。Springが JRuby1.1に対応するには 3.0まで待てだと。
    仕方ないので JRuby 1.0.3を使うことにした。

    そしたら今度は
    org.jruby.exceptions.RaiseException: superclass must be a Class (Module) given
    というエラー。勘弁してください。
    Springのフォーラムで同じ悩みを抱えている人が一人だけいた。
    JRuby 1.0以降を使う場合、ドキュメントに書いてある例どおりにRubyクラスにJava側のインターフェイスを継承させるとエラーになってしまうようだ。何も継承していないクラスを記述したところ解決。

    でもRubyで記述した文字列リテラルに日本語の文字が含まれていると化けて出てきてしまった。UTF-8以外の文字コードが介在していない環境で、この手の問題が起こるのはむしろ最近じゃ珍しいと思うのだが。

    というわけで、Springの lang:jrubyで日本語を通すに続く。

    ラベル: ,

    2008-05-11

    Flexアプリケーションのなんちゃってローカライゼーション

    インドの公用語はヒンドゥー語、補助的な公用語が英語とされているが、実はヒンドゥー語は日本における日本語のように全国的に通用するわけではなく、インドの各地域には20を超える言語が存在する。それらは日本でいうところの方言といったレベルと違い、それぞれ別の言語である。
    特に南部ではヒンドゥー語の話者がほとんどおらず、公用語をヒンドゥー語と定める政府の方針には反対意見が根深いという。
    そうなってくるとインド全国で通じる言葉といえば英語になるわけだが、皆が英語を話せるかというとそうでもなく、教育を受けた高いカーストの人か、観光客相手の仕事をしている人などに限られてくる。インドは難しい。

    以上、全て無駄話である。

    Flexアプリケーションの UIは XML(MXML) で記述されるため、日本語で記述したものを英語へ翻訳するにはXMLファイルを訳者へ渡して語の置き換えをしてもらうという単純なやりかたである程度事が済むのだが、「完成した」アプリケーションの翻訳ならそれでも良いかもしれないものの、いかんせんソフトウェアとは常に進化する(させられる)ものであり、毎日のように変化する翻訳元に翻訳先を追従させるのは難しい。というか無理。


    Flexでは Javaのようにリソースバンドル(propertiesファイル)を使えるので、L10NやらM17Nにはそれを使うことになっている。
    たとえば日本語用のリソースバンドル locale/ja_JP/resource.properties には
    messageToSend=送信するメッセージ

    英語用のリソースバンドル locale/en_US/resource.properties には
    messageToSend=Message to send
    と書いたうえで、

    <mx:Label text="@Resource(key='messageToSend', bundle='resource')"/>

    のように UIコンポーネントを記述すると、アプリケーションがコンパイルされた時のロケール指定に応じて自動的にラベルのテキストが "送信するメッセージ" "Message to send" に切り替わる。
    (Flex Builderではコンパイラ設定の「追加コンパイラ引数」欄でロケールを指定できる)

    ただし、これはやたらめんどくさい。ソース中に出現するあらゆるメッセージをキー文字列で表現しておいて言語環境ごとに適切な語へ置換するというのはわかるが、はっきり言ってキーで書いてあるとわかりにくい。だって、

    <mx:Label text="送信するメッセージ"/>

    <mx:Label text="@Resource(key='messageToSend', bundle='resource')"/>
    になったら本当に何がなんだかわからんよ。

    そんなわけで手抜きの「なんちゃってローカライゼーション」が発動する。



    まずは Flexのプロジェクト設定でビルドパスに locale/{locale}を追加する。これにより、locale/ja_JP/ や locale/en_US/ 内にある propertiesファイルを読み込めるようになる。

    次はMXMLの中に
    <mx:Metadata>
    [ResourceBundle("resource")]
    </mx:Metadata>
    を追加し、resource.propertiesを読み込ませる。もちろん resource.propertiesは ja_JP用と en_US用の2個を作るが、ja_JP用のほうは空の内容で良い。


    en_US用の resource.propertiesには必要なだけ
    日本語表現=英語表現

    の記述をする。日本語表現をキーにしてしまうのがコツ。
    (なおJavaと違って Flexの propertiesファイルには UTF-8を使うことが出来るので、native2asciiのようなうざったいツールを使う必要はない)

    そしてmx:Scriptブロックの中にこんな関数を書き入れた。

    private function LS(str:String):String
    {
    if (str == null) return null;
    var ls:String = resourceManager.getString("resource", str);
    return (ls == null) ? str : ls;
    }

    私の UIコンポーネントはこんな風に記述される。

    <mx:Label text="{LS('送信するメッセージ')}"/>
    <mx:TextInput id="message"/>
    <mx:Button label="{LS('送信')}"/>

    LS(Localized Stringのつもり) 関数を通すと、propertiesファイルに訳語が収録されていればそれに置換され、無ければそのままの文字列が返ってくる。このようにしておけば、翻訳者は propertiesファイルにひたすら「日本語表現=英語表現」の辞書を作っていくだけで英語版のメンテナンスが出来るわけだ。

    日本語でコンパイル(ロケール ja_JP)

    英語でコンパイル(ロケール en_US)

    実際には、日本語表現と英語表現で語順が変わったりするため propertiesファイル内では {0}{1}といったプレースホルダが使えるようになっているのだが、上のLS()関数はそれに対応していない。後は自身で工夫するべし。

    ラベル:

    BlazeDSをメッセージブローカにして FlexでPub/Subメッセージングを行う

    ※このエントリは Publish/Subscribeモデルのメッセージ交換についての知識を前提とする。

    Flexでは Publisherを Producer(プロデューサ)、Subscriberを Consumer(コンシューマ)、Topicを Destination(ディスティネーション)と呼ぶようである。

    BlazeDSの組み込まれたWebアプリケーションは、これらを取り扱うメッセージブローカとして動作することが出来る。
    しかしながら、Webアプリケーションは所詮 Webアプリケーションであり、通信プロトコルとしては HTTPしか使えない。Pub/SubメッセージングをHTTPで擬似的に実現する(よく疑似プッシュと呼ばれる)方法は基本的に Cometの考え方と根を同じにするが、Flex/BlazeDSでは利用形態や環境に応じていくつかのバリエーションが用意されている(その辺りについてはAkihiro Kamijo氏のブログが詳しい)。

    ここでは最もシンプルな Long-pollingを取り扱う。なのでひとまず「俺のサーバは64bitでメモリが湯水のように使えて好きなだけスレッドも立てられるからC10K問題について考える必要がない」と思いこむこと。同時接続数が多くなると fastcgiがおかしな動作をするなと思ってソースを追いかけたら一部で select() が使われたままになって放置されてることを発見して 1024本(-α)までですか!と絶叫した思い出などは封印すること。

    BlazeDSで最も簡単にLong-pollingなメッセージングを有効にするには下記のようにする。
    1) polling-enabled = true な AMFチャンネル(mx.messaging.channels.AMFChannel)を定義する
    2) 1を使って、メッセージング用のサービス(flex.messaging.services.MessageService)を定義する
    3) 2の中に必要なだけ destinationを定義する(サーバサイドで動的にdestinationを生成することも出来る)

    以前のエントリ BlazeDSを自分のWebアプリケーションに組み込む で最小限 RPCだけが可能なように作成した WEB-INF/flex/services-config.xmlをベースに、これらを追加分として例示してみることにする。

    channels要素内
    <!--
    チャンネルの定義。
    RPC用のチャンネルとは別のidとエンドポイントURLを与えている。
    また、polling-enabledプロパティに trueをセットしている。
    -->

    <channel-definition id="my-polling-amf"
    class=
    "mx.messaging.channels.AMFChannel">
    <endpoint
    url=
    "http://{server.name}:{server.port}/{context.root}/messagebroker/amfpolling"
    class=
    "flex.messaging.endpoints.AMFEndpoint"/>
    <properties>
    <polling-enabled>true</polling-enabled>
    <polling-interval-millis>0</polling-interval-millis>
    <wait-interval-millis>-1</wait-interval-millis>
    <max-waiting-poll-requests>300</max-waiting-poll-requests>
    </properties>
    </channel-definition>
    services要素内
    <!-- メッセージング用サービスと destinationの定義。-->
    <service id="messaging-service" class="flex.messaging.services.MessageService">
    <adapters>
    <adapter-definition id="actionscript"
    class=
    "flex.messaging.services.messaging.adapters.ActionScriptAdapter"
    default=
    "true"/>
    </adapters>
    <default-channels>
    <!-- channel-definitionで定義したidをここから参照 -->
    <channel ref="my-polling-amf"/>
    </default-channels>

    <!--
    destination(JMSで言うところのTopic)は必要なだけ作成しておく。
    また、動的に作成することもできる。動的生成については
    BlazeDSサンプルコード内 ChatRoomService.javaを参照
    -->

    <destination id="hoge"/>
    <destination id="honya"/>
    <destination id="foobar"/>
    </service>
    上記の設定により、http://host:port/contextroot/messagebroker/amfpolling を通信先として Flexから Publisher/Subscriberメッセージングを行うことができるようになる。
    Flexアプリケーション Publisher(Producer) 側

    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
    <![CDATA[
    import mx.messaging.messages.AsyncMessage;
    ]]>

    </mx:Script>
    <mx:ChannelSet id="channelSet">
    <mx:AMFChannel
    uri=
    "http://localhost:8080/demo/messagebroker/amfpolling"/>
    </mx:ChannelSet>
    <mx:Producer id="producer" channelSet="{channelSet}"
    destination=
    "hoge"/>

    <mx:HBox width="100%">
    <mx:Label text="送信するメッセージ"/>
    <mx:TextInput id="message"/>
    <mx:Button label="送信">
    <mx:click>
    <![CDATA[
    var msg:AsyncMessage = new AsyncMessage;
    msg.body = message.text;
    producer.send(msg);
    ]]>

    </mx:click>
    </mx:Button>
    </mx:HBox>
    </mx:Application>
    Flexアプリケーション Subscriber(Consumer) 側

    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
    <![CDATA[
    import mx.controls.Alert;
    ]]>

    </mx:Script>
    <mx:ChannelSet id="channelSet">
    <!--
    requestTimeout=-1 HTTPリクエストをタイムアウトさせずメッセージを待ち続ける
    pollingInterval=0 HTTPリクエストが終了したら即座に次のリクエストを発生させる
    -->

    <mx:AMFChannel
    uri=
    "http://localhost:8080/demo/messagebroker/amfpolling"
    requestTimeout=
    "-1" pollingInterval="0"/>
    </mx:ChannelSet>
    <mx:Consumer id="consumer"
    channelSet=
    "{channelSet}" destination="hoge">
    <mx:message>
    <![CDATA[
    // メッセージが飛んできた時の処理
    message.text =
    (event as MessageEvent).message.body.toString();
    ]]>

    </mx:message>
    </mx:Consumer>
    <mx:creationComplete>
    <![CDATA[
    // subscribeをしないとメッセージは飛んでこない
    consumer.subscribe(null);
    ]]>

    </mx:creationComplete>
    <mx:HBox width="100%">
    <mx:Label text="受信したメッセージ"/>
    <mx:Text id="message" width="100%"/>
    </mx:HBox>
    </mx:Application>


    補足事項

  • 実用的な用途のうえでは、Flexアプリケーション同士でダイレクトに Pub/Subメッセージ交換を行うケースは実際にはそれほどないと思われる。多くの場合 Producerとしての処理はサーバ側で行われ、Flexは Consumer専門になるのではないだろうか。

  • 設定によりJMSとの相互運用レイヤを有効に出来るらしい?ので、既存のESB (Enterprise Service Bus)に Flexアプリケーションを取って付けるといった用途も考えられる。日本の企業で ESBなんて格好いい物が整備されている所がどれほどあるか知らないが。

  • JMSにない概念として Subtopicというものがあるようだ。
  • ラベル:

    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" が追加されてい