次期 S2Wicket 仕様断念の原因となった動的オブジェクトの循環参照問題をおいらもみんなのために解いたった!?

少し前に S2Wicket のコミッタ,よういちろうさんのところであった話題.

この問題を id:t_yano さんが

だそうな.お見事.


自分は元ネタの方にもコメントしてるように,これを (よういちろうさんが呼ぶところの) 動的プロキシで実現するのが適当なのか,未だに疑問を持ってます.
もし自分が S2Wicket のコミッタだったら,ここで動的にクラスを生成するかは微妙.
あらかじめ (静的に) サブクラスを用意しておくことを選択するかも.
数が多いとのことだけど,コード生成すれば良いだけだし.


とか思ってたのだけど,矢野さんのエントリ見てたらこれはこれでパズル的というか目的とは関係なく面白いかもとか思ったり.
そんなわけで (どんなわけで?),矢野さんとは全然違う方法で,おいらもみんなのために一所懸命解いたった!? (ノッフ風って何?)


っていっても,矢野さんほどの力作ではなくて,元ネタのよういちろうさんとこで配布しているプロジェクトの SerializedProxy を次のように変更するだけ (packageimport は省略,例外の扱いは手抜き).

public class SerializedProxy extends Component implements Serializable {
    private static final long serialVersionUID = 1L;
    private String className;
    private Object target;

    public static SerializedProxy create(String className, Object target) {
        return new SerializedProxy(className, target);
    }

    public SerializedProxy(String className, Object target) {
        this.className = className;
        this.target = target;
    }

    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.writeObject(className);
        stream.writeObject(createFieldMap());
    }

    @SuppressWarnings("unchecked")
    private void readObject(ObjectInputStream stream) throws IOException,
            ClassNotFoundException {
        try {
            className = String.class.cast(stream.readObject());
            Class sourceClazz = Class.forName(className);
            Class clazz = ProxyFactory.createProxy(sourceClazz);
            target = clazz.newInstance(); // ポイント!!
            Map<String, Object> fieldMap = Map.class.cast(stream.readObject());
            restoreFields(fieldMap);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Object readResolve() throws ObjectStreamException {
        return target;
    }

    private Map createFieldMap() {
        try {
            Map<String, Object> fieldMap = new HashMap<String, Object>();
            Class<? extends Object> clazz = target.getClass();
            while (clazz != Object.class) {
                Field[] fields = clazz.getDeclaredFields();
                for (int i = 0; i < fields.length; i++) {
                    if (!fields[i].isAccessible()) {
                        fields[i].setAccessible(true);
                    }
                    Object value = fields[i].get(target);
                    if (value != null) {
                        fieldMap.put(fields[i].getName(), value);
                    }
                }
                clazz = clazz.getSuperclass();
            }
            return fieldMap;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void restoreFields(Map<String, Object> fieldMap) {
        try {
            Class clazz = target.getClass();
            while (clazz != Object.class) {
                Field[] fields = clazz.getDeclaredFields();
                for (int i = 0; i < fields.length; i++) {
                    if (!fields[i].isAccessible()) {
                        fields[i].setAccessible(true);
                    }
                    Object value = fieldMap.get(fields[i].getName());
                    if (value instanceof SerializedProxy) { // ポイント!!
                        value = SerializedProxy.class.cast(value).readResolve();
                    }
                    fields[i].set(target, value);
                }
                clazz = clazz.getSuperclass();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

ポイントは writeObject()/readObject() を使ってることです.
特に readObject() が重要.
オブジェクトの復元が完全に完了する前に target フィールドにデシリアライズしつつあるオブジェクトのインスタンスを設定しちゃってます.ここが一番のポイント.
そして restoreFields() では保存していたフィールドの値が SerializableObject の場合はその target の値をフィールド値としています.ここはちょっと分かりにくいかも.


元ネタの画像を直リンさせていただくと...

これを上記のコードでデシリアライズすると,次のような動きをします.

  1. FormProxySerializedProxyreadObject() が呼ばれる.
  2. 1 の target に新しい FormProxyインスタンスを設定する.
  3. FormProxy のフィールド値を復元するため Map をデシリアライズする.
  4. LinkProxySerializedProxyreadObject() が呼ばれる.
  5. 4 の target に新しい LinkProxyインスタンスを設定する.
  6. LinkProxy のフィールド値を復元するため Map をデシリアライズする.
  7. 6 でデシリアライズされた Map から parent を取得すると FormProxySerializedProxy が返ってくるので,その readResolve() を呼ぶ (2 で target に設定された FormProxy が返される).
  8. LinkProxySerializedProxyreadResolve() が呼ばれる (これは標準のデシリアライズ実装から呼ばれる).
  9. 3 でデシリアライズされた Map から child を取得すると LinkProxySerializedProxy が返ってくるので,その readResolve() を呼ぶ (5 で target に設定された LinkProxy が返される).
  10. FormProxySerializedProxyreadResolve() が呼ばれる (これは標準のデシリアライズ実装から呼ばれる).

最初にデシリアライズが始まる FromProxy (の SerializedProxy) の readResolve() が一番最後に呼び出されるのが頭の痛いところだったわけですが,readObject()インスタンスの参照を確定させてしまっているのでもう怖くないよ.


ちなみに,restoreFields() で保存していたフィールドの値が SerializableProxy の場合ってのは,元のソースだと

                    Object value = fieldMap.get(fields[i].getName());
                    //
                    // ここでSerializedProxyなvalueが得られるが,
                    // 中身がなにも入っていない。。。
                    if (value instanceof SerializedProxy)
                        System.out.println(value.toString());
                    //
                    fields[i].set(target, value);

となっていたところ.
このタイミングでちゃんと target が取れるようになってます.
でもまぁ,それだけなんだけど.