2008-05-28

JRubyで DBIを使う ( DBI / Jdbc )

Javaでいうところの JDBCに相当するレイヤが Rubyにおける DBIである。

ActiveRecord用に最初から設計されたデータベースじゃないと ActiveRecordで使うのはとても厳しい。そういう場合に取り得る選択としてはひとまず ORMの使用をあきらめて SQLを用いたDBアクセスをするというのがあるが、そうするにしても JRubyから java.sql.* を使うようではなんだか負けた気分(っていうか最初から Javaで書けば?って話)なので、 ここはひとつ JRubyから DBIを使ってデータベースにアクセスすることにする。

以下、$JRUBY_HOME/binに PATHが通っている前提で。

JRubyに DBIをインストール


RubyForgeから "Ruby/DBI"を探し、dbi-*.tar.gz をダウンロード・展開したら、

jruby setup.rb config --without=dbd_sqlite,dbd_sybase
jruby setup.rb setup
jruby setup.rb install

のようにする。これで JRubyに DBIがインストールされたのだが、これとは他に DBIと JDBCをブリッジするドライバが必要となる。

JDBC用のDBIドライバをインストール


RubyForgfeで "Ruby DBI-JDBC driver" を探し、dbi-jdbc.tar.gzをダウンロード・展開する。
アーカイブの中に入っていた DBD ディレクトリを $JRUBY_HOME/lib/ruby/site_ruby/1.8/ に放り込む。
結果、
$JRUBY_HOME/lib/ruby/site_ruby/1.8/DBD/Jdbc/Jdbc.rb
$JRUBY_HOME/lib/ruby/site_ruby/1.8/DBD/Jdbc/JdbcTypeConversions.rb
が存在する状態になれば OK

DBI-JDBCの使い方例

require 'dbi'

url = 'DBI:jdbc:mysql://localhost/mydb'
username = 'username'
password = 'password'
driver = 'com.mysql.jdbc.Driver'
DBI.connect(url, username, password, 'driver'=>driver) do |dbh|
p dbh.select_all('show tables')
}

なお型変換やNULLの扱いのあたりで制限事項が色々あるため、dbi-jdbc.tar.gzに含まれているREADMEには一度目を通されたし。
JNDIからデータソースを取ってきてそこからgetConnection()する方法は用意されていないようだ。簡単なハックで出来るようになると思うけど。

DBIの API仕様については下記を参照のこと。
DBI Interface Specification Version 0.2.2 (Draft)

ラベル:

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

    Springの lang:jrubyで日本語を通す

    Spring Framerowkと JRubyを組み合わせると、Springの Beanを Rubyで実装することが出来る。
    が、Bean定義ファイルにインラインで記述するにしろソースファイルに記述するにしろ、スクリプト中の文字列リテラルに日本語が含まれていると化けてしまう。仕方ないので Springと JRubyのソースを追ってみた。

    まず結論として、Springの JRuby連携部分に対するその場しのぎのパッチを示す。
    org/springframework/scripting/jruby/JRubyScriptFactory.java をこんな風に書き換えると、とりあえず日本語が通る。

    96a97,98
    >String iso8859_1edString =
    > new String(scriptSource.getScriptAsString().getBytes("UTF-8"), "ISO-8859-1");
    98c100
    < scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader);
    ---
    > iso8859_1edString, actualInterfaces, this.beanClassLoader);

    デバッガを使って文字の化ける箇所を特定→charを惜しげもなく byte へキャストしている箇所発見(日本語を含む文字列リテラルが化ける直接の原因)→なぜそうなっているか大体想像がついたので JRubyの mainメソッドからスクリプトのロードが行われる箇所までを追跡→やはりコマンドラインからの通常起動では ISO-8859-1固定でスクリプトファイルを読み込んでいる

    つまりJRuby1.0 の Lexarは、スクリプトがどんなエンコーディングで記述されていようともそれを一旦 ISO-8859-1文字列としてパースすることを前提に実装されている。
    「コマンドラインや$KCODEで文字コードの指定が出来るじゃないか!」と思うかもしれないが、指定された文字コードによって文字列の扱い方が影響を受けるのは、構文解析が終わった後である。通常のI18Nアプローチでは考えにくいことだが、そうしないともともとI18Nに対応していない(文字列とバイトストリームの区別がない) MRI(CRuby) 1.8との互換性を取ることが困難なのだろう。

    上記の理由により、スクリプトを Correctly I18N'edな文字列として JRubyパーサに渡すと逆に文字化けが起こってしまう。だが、Springはそのようなことをお構いなしに「正しいエンコーディングで読み込まれた」スクリプト文字列を JRubyのパーサに渡してしまう。
    上のパッチは、一度正しく読み込まれたI18N'edな文字列を UTF-8表現のバイトストリームにバラし、それをわざわざISO8859-1文字列であると偽って Rubyパーサに渡すためのクイックハックである。

    ・・・っていうか、Springで JRubyが使えるようになってからしばらく経つと思うんだけど、この問題についての情報を検索で見つけることができなかった。日本人は誰もこの機能使ってないの?

    ラベル: ,

    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で日本語を通すに続く。

    ラベル: ,