クラスjava.io.ObjectInputStreamはどうやって実現されているんだ?

俺様デシリアライザを作るにはクラスjava.io.ObjectInputStreamをカスタマイズする必要があります。クラスjava.io.ObjectInputStreamの振る舞いを変えるには完全に再実装するしかありません(そのように無理やり制限されています)。ということは、俺様デシリアライザでクラスjava.io.ObjectInputStreamと同じような振る舞いを実現しなければいけないことになります。そこで、いろいろ考えたところ問題にぶち当たりました。

次のクラスPersonのインスタンスをシリアライズしてデシリアライズすると、フィールドmNameの内容はちゃんと復元されます。

import java.io.Serializable;
public class Person implements Serializable {
  private final String mName;
  public Person(String aName) {
    super();
    mName = aName;
  }
}

final修飾されたフィールドへの代入はJava言語の文法上許されていませんし、リフレクションを使ってもできません。それ以前に、どうやってインスタンスを生成し、初期化ているのでしょうか?仕様によると、デシリアライザは生成したインスタンスを最も具体的な引数なしコンストラクタで初期化します。具体的には次のように振舞うはずです。

  1. クラスPersonのインスタンスpを生成する。
  2. インスタンスpをクラスObjectの引数なしコンストラクタで初期化する。※クラスPersonには引数なしコンストラクタがないので直接のスーパークラスObjectの引数なしコンストラクタが最も具体的とみなされる。

インスタンスpはクラスPersonのインスタンスであるにも関わらず、クラスPersonのコンストラクタで初期化されていません。このようなインスタンスを生成することはJava言語の文法上許されていませんし、リフレクションを使ってもできません。

クラスsun.misc.Unsafeってなんだ?

Java言語の文法や標準のAPIでは実現できないライブラリを見つけると解析したくなるのが技術者の性というものです。
さっそく解析、クラスjava.io.ObjectInputStreamのメソッドreadObject()から処理を追ってゆくと、クラスjava.io.ObjectStreamClassの中でクラスsun.misc.Unsafeと出会いました。名前だけでも相当危険な香りがぷんぷんしてきます。さらに処理を追ってゆくと、このクラスのnative修飾されたメソッドputObject(Object, long, Object)を呼び出しています。呼び出しに使われた引数から推測すると「インスタンス(第1引数)が格納されているアドレスからオフセット(第2引数)されたメモリに値(第3引数)を書き込む」処理のようです。なるほど、native修飾されたメソッドを使ってJavaVMのヒープに直接アクセスできるなら可能です。

クラスsun.misc.Unsafeの使われ方を調べると、static修飾されたメソッドgetUnsafe()を呼び出してシングルトンインスタンスを手に入れることが分かります。さっそく、呼び出してみると例外が起こってシングルトンインスタンスを手に入れることができません。

仕方がないので、メソッドgetUnsafe()の実装(次)を調べてみました。実装によると、ブートクラスローダでロードされたクラスからの呼び出ししか受け付けないことが分かります。これはブートクラスパスに入っているクラスからの呼び出ししか受け付けないことを意味しています。普通はブートクラスパスに入っているクラスは標準ライブラリを構成するクラスだけです。つまり、自作のクラスから呼び出したければ自作のクラスをブートクラスパスに入れなければいけません。

 0  iconst_2
 1  invokestatic sun.reflect.Reflection.getCallerClass(int) : java.lang.Class [246]
 4  astore_0
 5  aload_0
 6  invokevirtual java.lang.Class.getClassLoader() : java.lang.ClassLoader [217]
 9  ifnull 22
12  new java.lang.SecurityException [127]
15  dup
16  ldc  [1]
18  invokespecial java.lang.SecurityException(java.lang.String) [220]
21  athrow
22  getstatic sun.misc.Unsafe.theUnsafe : sun.misc.Unsafe [216]
25  areturn

自作クラスをブートクラスパスに入れるなら、次のプログラムでやりたかったことができます。実行すると、初期化前のインスタンスを作り、final修飾されたフィールドを更新します。すごくないですか?

import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class Main {
  private static class Person {
    private final String mName;
    public Person(String aName) {
      super();
      mName = aName;
    }
    @Override
    public String toString() {
      return "Person(" + mName + ")";
    }
  }
  public static void main(String[] aArguments) throws InstantiationException, NoSuchFieldException {
    Unsafe tUnsafe = Unsafe.getUnsafe();
    Person tTarget = (Person) tUnsafe.allocateInstance(Person.class);
    System.out.println("create: " + tTarget);
    tUnsafe.putObject(tTarget, tUnsafe.objectFieldOffset(Person.class.getDeclaredField("mName")), "mel");
    System.out.println("update: " + tTarget);
  }
}

簡単に解説すると、クラスsun.misc.UnsafeのメソッドallocateInstance(Class)を使って初期化前のインスタンスを作り、メソッドputObject(Object, long, Object)を使ってfinal修飾されたフィールドを更新します。更新する際にフィールドのオフセットが必要なので、メソッドobjectFieldOffset(java.lang.reflect.Field)を使って手に入れます。

クラスsun.misc.Unsafeにはメモリを確保するメソッドとか面白いメソッドがたくさんあるので、かなり遊べそうです。

自作クラスをブートクラスパスに入れないなら、次のプログラムで無理やりできます。無理やりできるとはいえ、セキュリティポリシーによっては(アップレットの標準のセキュリティポリシーでは)動かないので、いつでも使える手ではありません。

import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class Main {
  private static class Person {
    private final String mName;
    public Person(String aName) {
      super();
      mName = aName;
    }
    @Override
    public String toString() {
      return "Person(" + mName + ")";
    }
  }
  private static Unsafe getUnsafe() throws NoSuchFieldException {
    Field tDeclaredField = Unsafe.class.getDeclaredField("theUnsafe");
    boolean tAccessible = tDeclaredField.isAccessible();
    tDeclaredField.setAccessible(true);
    try {
      try {
        return (Unsafe) tDeclaredField.get(null);
      } catch (IllegalAccessException aCause) {
        throw new RuntimeException("フィールド" + tDeclaredField + "にアクセスできません。");
      }
    } finally {
      tDeclaredField.setAccessible(tAccessible);
    }
  }
  public static void main(String[] aArguments) throws InstantiationException, NoSuchFieldException {
    Unsafe tUnsafe = getUnsafe();
    Person tTarget = (Person) tUnsafe.allocateInstance(Person.class);
    System.out.println("create: " + tTarget);
    tUnsafe.putObject(tTarget, tUnsafe.objectFieldOffset(Person.class.getDeclaredField("mName")), "mel");
    System.out.println("update: " + tTarget);
  }
}

簡単に解説すると、リフレクションでシングルトンインスタンスを無理やり取り出しているだけです。

まとめ

まとめると、クラスjava.io.ObjectInputStreamを実現するには少なくともクラスsun.misc.Unsafeの力が必要であることが分かりました。独自のデシリアライザを作る場合は注意した方がよいですよ。

カテゴリー: 技術情報