シリアライズされたオブジェクトを別パッケージ・別クラスに復帰する

2018年3月7日



============================
この情報は古いので、シリアライズ・デシリアライズされるクラスを自由に変更するを見ること。
============================

Javaのシリアライゼーションは非常に便利で、ほぼSerializableをつけるだけで簡単に永続化してくれる。このため、この機能をあちこちで多用していたものだ。しかし、この方法の欠点としては、後からパッケージやクラス名を変更することができない点である。

現在の開発では、主にシリアライゼーションの方法としてJSONテキストとして永続化している。この方法では、パッケージ名・クラス名が永続化データの中に含まれないので、後からいくらでもパッケージ、クラス名を変更することができる。

※もちろん両者共に、フィールド名やそのタイプを変更してはならない(これを可にする方法は知らない)

さて、既存のコードでSerializableによるシリアライゼーションを多用しており、ユーザ側のデータベースに永続化データが格納されているものとする。これを最終的にはJSONによるシリアライゼーションに変えたいのだが、もちろんその過程でパッケージ名やクラス名を変更したい。これを行うにはどうしたら良いのか?

How can I change package for a bunch of java serializable classesを参考にして追求してみた。

以下を読む前に

この後のサンプルでは、クラス名を変更するために、

    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
      ObjectStreamClass read = super.readClassDescriptor();
      String className = read.getName();
      if (className.equals("foo.Foo")) {
        return ObjectStreamClass.lookup(bar.Bar.class);
      }
      return read;
    }

などとしているが、実際にやってみるとうまく行かないケースがあった。深くは追求していないが、どうもあるクラスのオブジェクトをシリアライズした後にクラスにフィールドを追加し、そクラス名を変更した場合にエラーになってしまうようだ(もちろん、serialVersionUIDは始めから設定してある)。

やはり、シリアライズされるクラス名を固定にすると同じように、ObjectStreamClassのnameフィールドを直接書き換えた方がよさそうだ。つまり、

    Field nameField = ObjectStreamClass.class.getDeclaredField("name");
    nameField.setAccessible(true);

としておき

    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
      ObjectStreamClass read = super.readClassDescriptor();
      String className = read.getName();
      if (className.equals("foo.Foo")) {
        nameField.set(read, "bar.Bar"); // 例外処理は省略
      }
      return read;
    }

である。

もっとも簡単な例

異なるパッケージ、異なるクラス名の一方をシリアライズし、他方のオブジェクトとしてデシリアライズする例を考えてみる。
つまり、foo.Fooオブジェクトをシリアライズし、bar.Barオブジェクトとしてデシリアライズする。

package foo;
import java.io.*;
public class Foo implements Serializable {
  private static final long serialVersionUID = 1L;  
  public int a = 123;
  public int b = 234;
  public String c = "string";
}
package bar;
import java.io.*;
public class Bar implements Serializable {
  private static final long serialVersionUID = 1L;
  public int a;
  public int b;
  public String c;
  @Override
  public String toString() {
    return a + "," + b + "," + c;
  }
}
import java.io.*;
import bar.*;
import foo.*;
public class SerializationTest {
  public static void main(String[]args) throws Exception {  
    Bar bar = bytesToObject(objectToBytes(new Foo()));
    System.out.println(bar);
  }
  public static byte[] objectToBytes(Object object) throws IOException {
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    try(ObjectOutputStream out = new ObjectOutputStream(bytes)) {      
      out.writeObject(object);
    }
    return bytes.toByteArray();
  }
  @SuppressWarnings("unchecked")
  public static <T> T bytesToObject(byte[]bytes)  throws Exception {
    try (ObjectInputStream in = new MyObjectInputStream(new ByteArrayInputStream(bytes))) {
      return (T)in.readObject();
    }
  } 
  public static class MyObjectInputStream extends ObjectInputStream {
    protected MyObjectInputStream(InputStream in) throws IOException {
      super(in);
    }    
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
      ObjectStreamClass read = super.readClassDescriptor();
      String className = read.getName();
      if (className.equals("foo.Foo")) {
        return ObjectStreamClass.lookup(bar.Bar.class);
      }
      return read;
    }
  }
}

実行結果は以下。これは全く問題無い。

123,234,string

少々複雑な例

もう少々複雑な例で試してみる。先の例に以下を追加する。

package foo;

import java.io.*;
import java.util.*;

public class FooHolder implements Serializable {
  private static final long serialVersionUID = 1L;
  public List<Foo>list = new ArrayList<>();
}
package bar;

import java.io.*;
import java.util.*;
import java.util.stream.*;

public class BarHolder implements Serializable {
  private static final long serialVersionUID = 1L;

  List<Bar>list;  

  @Override
  public String toString() {
    return list.stream().map(bar->bar.toString()).collect(Collectors.joining("\n"));
  }
}
import java.io.*;

import bar.*;
import foo.*;

public class SerializationTest {

  public static void main(String[]args) throws Exception {  
    FooHolder fooHolder = new FooHolder();
    Foo foo1 = new Foo();
    foo1.a = 1111;
    Foo foo2 = new Foo();
    foo2.a = 2222;
    fooHolder.list.add(foo1);
    fooHolder.list.add(foo1);
    fooHolder.list.add(foo2);
    BarHolder barHolder = bytesToObject(objectToBytes(fooHolder));
    System.out.println(barHolder);

  }

  public static byte[] objectToBytes(Object object) throws IOException {
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    try(ObjectOutputStream out = new ObjectOutputStream(bytes)) {      
      out.writeObject(object);
    }
    return bytes.toByteArray();
  }

  @SuppressWarnings("unchecked")
  public static <T> T bytesToObject(byte[]bytes)  throws Exception {
    try (ObjectInputStream in = new MyObjectInputStream(new ByteArrayInputStream(bytes))) {
      return (T)in.readObject();
    }
  }

  public static class MyObjectInputStream extends ObjectInputStream {
    protected MyObjectInputStream(InputStream in) throws IOException {
      super(in);
    }    
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
      ObjectStreamClass read = super.readClassDescriptor();
      String className = read.getName();
      if (className.equals("foo.FooHolder")) {
        return ObjectStreamClass.lookup(bar.BarHolder.class);
      }
      if (className.equals("foo.Foo")) {
        return ObjectStreamClass.lookup(bar.Bar.class);
      }
      return read;
    }
  }
}

実行結果は以下。これも問題無いようだ。

1111,234,string
1111,234,string
2222,234,string

Javaシリアライゼーションを検出したい場合

例えば、一つのデータベースフィールドにこれまではJavaのシリアライゼーションによるバイト配列を格納しており、それをJSONに変更したいものとする。この場合、どのように両者を区別したらよいか?

これはObject Serialization Stream Protocolに記述がある(日本語訳はオブジェクト直列化ストリームプロトコル )。シリアライゼーションストリームは以下の構成になっており、

stream:
  magic version contents

magicとversionは以下である(ビッグアンディアンで格納)。

    final static short STREAM_MAGIC = (short)0xaced;
    final static short STREAM_VERSION = 5;

例えば、先の例で以下のように中身を見ると、

    byte[]bytes = objectToBytes(new Foo());
    for (byte b: bytes) {
      System.out.println(String.format("0x%02x", b & 0xff));
    }

以下が表示される。

0xac
0xed
0x00
0x05
0x73
0x72
0x00
...