Ruby/JRuby の上で Java オブジェクトを Ruby String にシリアライズ

hfu2007-12-07

Java の Serializable オブジェクトを、簡単に Ruby String にシリアライズできたらどうだろうと思いましたので、実際にやってみました。

目標

こんなことをやってみたいなとおもいました:

pojo = Currency.getInstance('JPY')  # 任意の Serializable な Java オブジェクトを作成

p pojo.toString # もとの Java オブジェクトを表示
serialized = PojoSerializer::serialize(pojo) # Java オブジェクトを Ruby String にシリアライズ
p serialized.hex # シリアライズした Ruby String を 16進表記でダンプ
deserialized_pojo = PojoSerializer::deserialize(serialized) # その Ruby String をデシリアライズ
p deserialized_pojo.toString # デシリアライズして得た Java オブジェクトを表示

実装方針

いつもの pagan poetry pattern でいきます*1Java の複雑な手続きを吸収して Ruby らしくする、グッドラッパーを目指すスクリプトを作ります。
今回は、CRuby でも JRuby でも同じように動かせるように留意しました。

実装後のプログラム実行結果

後述のとおり実装したスクリプトで上のコードを実行すると、以下のような結果が得られました:

$ ruby pojo_serializer.rb
"JPY"
"ac ed 00 05 73 72 00 12 6a 61 76 61 2e 75 74 69 6c 2e 43 75 72 72 65 6e 63 79 fd cd 93 4a 59 11 a9 1f 02 00 01 4c 00 0c 63 75 72 72 65 6e 63 79 43 6f 64 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 78 70 74 00 03 4a 50 59"
"JPY"
$ jruby pojo_serializer.rb
"JPY"
"ac ed 00 05 73 72 00 12 6a 61 76 61 2e 75 74 69 6c 2e 43 75 72 72 65 6e 63 79 fd cd 93 4a 59 11 a9 1f 02 00 01 4c 00 0c 63 75 72 72 65 6e 63 79 43 6f 64 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 78 70 74 00 03 4a 50 59"
"JPY"

JRuby と CRuby の間で、バイトレベルで一致した結果が間違いなく得られていることが確認できます。

実装

PojoSerializer の実装は、下のようになっています。

module PojoSerializer
  QNS = %w{java.io.ByteArrayOutputStream java.io.ObjectOutputStream java.io.ByteArrayInputStream java.io.ObjectInputStream}
  begin
    require 'java'
    QNS.each do |qn|
      include_class qn
    end
    BRIDGE = :jruby
  rescue LoadError
    require 'rjb'
    QNS.each do |qn|
      module_eval "#{qn.split('.').last} = Rjb.import('#{qn}')"
    end
    BRIDGE = :rjb
  end

  def PojoSerializer::serialize(pojo)
    baos = ByteArrayOutputStream.new
    oos = ObjectOutputStream.new(baos)
    oos.writeObject(pojo)
    oos.flush
    byte_array = baos.toByteArray
    if PojoSerializer::BRIDGE == :jruby 
      return byte_array.to_ary.pack('C*')
    else
      return byte_array
    end
  end

  def PojoSerializer::deserialize(s)
    byte_array = ''
    if PojoSerializer::BRIDGE == :jruby
      byte_array = s.unpack('C*').to_java(:byte)
    else
      byte_array = s.unpack('C*')
    end
    bais = ByteArrayInputStream.new(byte_array)
    ois = ObjectInputStream.new(bais)
    ois.readObject
  end
end

class String
  def hex
    s = ''
    each_byte do |b|
      s << (b < 16 ? '0' : '') + b.to_s(16) + ' '
    end
    s.chop
  end
end

if __FILE__ == $0
  begin
    require 'java'
    import java.util.Currency
  rescue LoadError
    require 'rjb'
    Currency = Rjb.import('java.util.Currency')
  end
  pojo = Currency.getInstance('JPY')

  p pojo.toString
  serialized = PojoSerializer::serialize(pojo)
  p serialized.hex
  deserialized_pojo = PojoSerializer::deserialize(serialized)
  p deserialized_pojo.toString
end

Java API の細々としたさわり方だとか、Ruby での String の扱い方*2だとかに関する知見をラッパーの中に固定化し、これらの知識を忘れられるようにするためのラッパーです。

JTS の Point をシリアライズするとどうなるか?

GeoTools が使っている com.vividsolutions.jts.geom.Point のインスタンスシリアライズしてみました:

require 'pojo_serializer' # これが上掲のスクリプト
require 'geotools'

p PojoSerializer::serialize(Geo::import_array_geometry([0, 0])).size
$ ruby geom_serialize.rb
DEBUG: rjb primitive_conversion mode

1152

地理情報処理の中で、Point はかなり基礎的なデータ単位なのですが、それをシリアライズすると1KB以上と、かなり大きくなってしまうことに驚きました*3。PojoSerializer、Java オブジェクトがどのくらいの大きさなのかを簡単に体感するのにいいかもしれません。

このエントリの品質について

POJO という言葉の使い方が間違っていたらすみません。

*1:ただし、後述する今回の実装では、入れ子にする第二のモジュールを省略して、Java から導入するクラスとラッパークラスがフラットに配置されています。Java から取り込むクラス数が少ないのでそうしてしまっています。

*2:Array#pack, String#unpack とか。

*3:Geometry をシリアライズするなら、WKTWKB を使うほうが良さそうです。