次期 S2Wicket 仕様断念の原因となった動的オブジェクトの循環参照問題をおいらもみんなのために解いたった!?
少し前に S2Wicket のコミッタ,よういちろうさんのところであった話題.
この問題を id:t_yano さんが
だそうな.お見事.
自分は元ネタの方にもコメントしてるように,これを (よういちろうさんが呼ぶところの) 動的プロキシで実現するのが適当なのか,未だに疑問を持ってます.
もし自分が S2Wicket のコミッタだったら,ここで動的にクラスを生成するかは微妙.
あらかじめ (静的に) サブクラスを用意しておくことを選択するかも.
数が多いとのことだけど,コード生成すれば良いだけだし.
とか思ってたのだけど,矢野さんのエントリ見てたらこれはこれでパズル的というか目的とは関係なく面白いかもとか思ったり.
そんなわけで (どんなわけで?),矢野さんとは全然違う方法で,おいらもみんなのために一所懸命解いたった!? (ノッフ風って何?)
っていっても,矢野さんほどの力作ではなくて,元ネタのよういちろうさんとこで配布しているプロジェクトの SerializedProxy
を次のように変更するだけ (package
や import
は省略,例外の扱いは手抜き).
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
の値をフィールド値としています.ここはちょっと分かりにくいかも.
元ネタの画像を直リンさせていただくと...
これを上記のコードでデシリアライズすると,次のような動きをします.
FormProxy
のSerializedProxy
のreadObject()
が呼ばれる.- 1 の
target
に新しいFormProxy
のインスタンスを設定する. FormProxy
のフィールド値を復元するためMap
をデシリアライズする.LinkProxy
のSerializedProxy
のreadObject()
が呼ばれる.- 4 の
target
に新しいLinkProxy
のインスタンスを設定する. LinkProxy
のフィールド値を復元するためMap
をデシリアライズする.- 6 でデシリアライズされた
Map
からparent
を取得するとFormProxy
のSerializedProxy
が返ってくるので,そのreadResolve()
を呼ぶ (2 でtarget
に設定されたFormProxy
が返される). LinkProxy
のSerializedProxy
のreadResolve()
が呼ばれる (これは標準のデシリアライズ実装から呼ばれる).- 3 でデシリアライズされた
Map
からchild
を取得するとLinkProxy
のSerializedProxy
が返ってくるので,そのreadResolve()
を呼ぶ (5 でtarget
に設定されたLinkProxy
が返される). FormProxy
のSerializedProxy
のreadResolve()
が呼ばれる (これは標準のデシリアライズ実装から呼ばれる).
最初にデシリアライズが始まる 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
が取れるようになってます.
でもまぁ,それだけなんだけど.