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の実用的なシチュエーションについて

    ラベル: ,

    0 件のコメント:

    コメントを投稿

    << ホーム