読者です 読者をやめる 読者になる 読者になる

BlazeDSから送ったメッセージが受け取れない件

今朝、メガネと自転車の鍵が壊れました。でおなじみのカタヤマです。


さて、Seasarカンファレンス用に取り急ぎ作った「イーダ先生の俳句道場(http://t2haiku.appspot.com/)」はお楽しみいただけているでしょうか?
このプログラムは、サーバにT2、クライアントにFlexという構成で実行しています。


このプログラムが完成した段階で、よねむらさんことid:yone098に「Flexいいですよね!」と言ったら、「よね!」の部分が癪に障ったようで、


JavaFXでも出来るだろうが」


とキレた上、その勢いでJavaFXで接続できるクライアントプログラムを作成してくれました。
ありがとうございます。


がしかし、どうもうまく通信できません。


T2は、内部で持っているAMF実装とBlazeDSのAMF実装を切り替えられるのですが、どうやらサーバ側がT2のAMF実装だとエラーになるようでした。


「なんでT2だとダメなの?」とid:yone098が更にキレはじめたので、焦った僕は飯田橋から大至急駆けつけて、BlazeDSとT2のソースを追ってみました。
何かしらのオブジェクトをデシリアライズするときに、クラス名が取れなくてエラーになってる様子でした。

オブジェクト型のフォーマット仕様

AMF3の仕様で、オブジェクト型は

object-type = object-marker (U29O-ref | (U29O-traits-ext
              class-name *(U8)) | U29O-traits-ref | (U29O-traits
              class-name *(UTF-8-vr)))

というフォーマット定義になっています。(Adobeのドキュメントより抜粋)
object-marker(0x0A)の後ろに、オブジェクトの性質を示すtraitsが入っていて、その後にclass-nameとプロパティが入っています。


traitsからは、「このオブジェクトが参照なのかどうなのか、dynamicクラスなのかどうなのか、プロパティが何個あるのか」などの情報が取れます。
dynamicクラスの場合、クラス定義以外にそのインスタンスに動的にセットしたプロパティがある可能性があるため、key:valueの形で値を読み取る必要が出てきます。送信側がFlexのdynamicクラスなどの場合にこのマークがつくようです。


またクラス名は

class-name = UTF-8-vr ; メモ: 匿名クラスの場合は、
                        ; 空のストリングを使用。

という仕様になっています。


クラス名の後には、プロパティ名とプロパティの値が続きます。

原因追求

これを踏まえて、実際に調べていくとエラーになっているのはflex.messaging.messages.RemotingMessageの「headers」というプロパティでした。


このプロパティは、FlexではObject型で宣言されています。FlexのObject型は「public dynamic class Object」と宣言されているため、サーバに送られてくるときはdynamicクラスだよという情報がtraitsに入っています。
T2の内部AMF実装の場合、クライアントからdynamicだというマーカーが来ると、その後マーカーの後に続く値(プロパティ名と値)を、Amf3Objectクラスに入れる処理をします。
Amf3ObjectはHashMapのサブクラスなので、実質HashMapにkey=valueを入れるような処理になります。


またこのObject型がFlexクライアントから送られてくる場合、class-name部分には空文字が入っているのですが、dynamicクラスのデシリアライズ処理にはクラス名は不要(dynamicの時点でAmf3Objectを使うことが確定する)なので、正しく値をデシリアライズすることが出来ます。


一方、BlazeDSクライアントの場合、このheadersプロパティは「java.util.Map」で宣言されており、traitsには非dynamicクラス(つまり普通のクラスだよ)という情報が入っています。なので、class-nameに「java.util.HashMap」とか入ってると思いきや、そのMapをシリアライズするときに使う「flex.messaging.io.MapProxy」というクラスには、

    /**
     * Return the classname of the instance, including ASObject types.
     * If the instance is a Map and is in the java.util package, we return null.
     * @param instance the object to find the class name of
     * @return the class name of the object. 
     */
    protected String getClassName(Object instance)
    {
        if (instance != null && instance instanceof Map 
                && instance.getClass().getName().startsWith("java.util."))
        {
            return null;
        }
        return super.getClassName(instance);
    }

という実装がされており、Mapの場合はクラス名を返さないような実装になっています。
「we return null.」
ああ、そうですか。そういうことは事前に言って頂きたい。


従って、「非dynamicだけとクラス名送らないよ」というパターンがBlazeDSクライアントの場合発生します。

BlazeDSは、このパターンを想定していて、オブジェクトを読む部分(flex.messaging.io.amf.Amf3Input#readScriptObject)では、

if (className == null || className.length() == 0)
{
 object = new ASObject();//ASObjectはHashMap継承のクラス
}

という実装をしており、このためうまく通信できていました。

結論

T2側もこれと同等の処理を追加したところ、うまく動くようになりました。
一口にAMF実装と言っても若干解釈が違うところがあるので、なかなか奥深いなと思いました。
なお、BlazeDSのjarを使ったJavaクライアントからのAMF通信は、id:shot6の記事が参考になりますhttp://d.hatena.ne.jp/shot6/20090618#1245259170

追記

ちなみにid:yone098は今、python製のAMF実装が他と違うと言ってキレています。
http://d.hatena.ne.jp/yone098/20090626/1245974748