動的プロキシをシリアライズする具体的方法

Javassistを使った動的プロキシの作成」エントリと「 替わりのオブジェクトをシリアライズするwriteReplace()/readResolve()」エントリを組み合わせると,実行時にその場で作った動的プロキシクラスを元にnewしたインスタンスをシリアライズし,異なるJavaVMプロセスにてデシリアライズできるようになる。今回はその具体的な方法を紹介しよう。

まずは,動的プロキシの対象となる抽象クラスを以下とする。これは「 Javassistを使った動的プロキシの作成」で取り上げたものと同一である。

package test; public abstract class Component { private int cnt; public abstract void onClick(); public void output() { System.out.println(cnt++); } }

このクラスはabstractなため,当然そのままではnewできない。そこで,動的プロキシクラスの登場である。onClick()メソッドの実装として,output()メソッドを呼び出すだけの実装コードを持つ動的プロキシクラスの生成処理が以下である。

package test; public class ProxyFactory { public static Class createClass(String proxyClassName) { ClassPool cp = ClassPool.getDefault(); CtClass componentCC = cp.get(“test.Component”); CtClass proxyCC = cp.makeClass(proxyClassName, componentCC); CtClass serializableCC = cp.get(“java.io.Serializable”); proxyCC.addInterface(serializableCC); CtMethod onClickMethod = CtMethod.make( “public void onClick() {“

  • ” output();”
  • ”}”, proxyCC); proxyCC.addMethod(onClickMethod); CtMethod writeReplaceMethod = CtMethod.make( “public Object writeReplace() throws ObjectStreamException {“
  • ” java.util.Map fieldMap = new java.util.HashMap();”
  • ” java.lang.reflect.Field[] fields = “
  • ” test.Component.class.getDeclaredFields();”
  • ” for (int i = 0; i < fields.length; i++) {“
  • ” f.setAccessible(true);”
  • ” if (!java.lang.Modifier.isTransient(f.getModifiers())) {“
  • ” fieldMap.put(f.getName(), f.get(this));”
  • ” }”
  • ” }”
  • ” return new test.SerializedObject("” + proxyClassName + “", fieldMap);”
  • ”}”, proxyCC); proxyCC.addMethod(writeReplaceMethod); Class proxyClazz = proxyCC.toClass(); return proxyClazz; } }

動的プロキシクラスの生成処理を,ProxyFactory#createClass()メソッドに定義しているコードである。createClass()メソッドには,動的プロキシのクラス名を指定できるようにしている。onClick()メソッドのオーバーライドだけでなく,writeReplace()メソッドに関しても,動的プロキシクラスに追加しているのが見てとれるだろう。writeReplace()メソッド内では,シリアライズ対象となる自分自身が持つフィールドの値を(transient修飾子が付けられたもの以外に対して)取得し,フィールド名と値の組み合わせを持つコレクションを作成している。これと,createClass()メソッドの引数で受け取った動的プロキシのクラス名を引数に渡して,SeriarlizedObjectオブジェクトを生成し,そのオブジェクトを替わりのシリアライズ対象として返却している。

SerializedObjectクラスは,コレクションとクラス名を持つ単純なクラスではあるが,readResolve()メソッドを持っていることが特徴的である。

package test; import java.util.Map; public class SerializedObject implements java.io.Serializable { private String className; private Map fieldMap; public SerializedObject(String className, Map fieldMap) { this.className = className; this.fieldMap = fieldMap; } public String getClassName() { return className; } public Map getFieldMap() { return fieldMap; } public Object readResolve() throws ObjectStreamException { Class clazz = ProxyFactory.createClass(className); Object result = clazz.newInstance(); Set entrySet = fieldMap.entrySet(); for (java.util.Iterator i = entrySet.iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry)i.next(); java.lang.reflect.Field field = Component.class.getDeclaredField((String)entry.getKey()); field.setAccessible(true); field.set(result.entry.getValue()); } return result; } }

readResolve()メソッドでは,保持しておいたクラス名を元に動的プロキシクラスを生成し,そのインスタンスも生成する。そして保持しておいたフィールドの値を1つずつ生成したインスタンスにセットし,そのインスタンスをデシリアライズ結果として返却している。

ほとんどの例外処理をすっとばしたコードなので,そのままではコンパイルエラーとなるが,本質的な処理は以上である。あとは,以下のように,

Component c = (Component)(ProxyFactory.createClass(“Proxy”).newInstance()); c.onClick(); FileOutputStream fos = new FileOutputStream(“./test.ser”); ObjectOutputStream out = new ObjectOutputStream(fos); out.writeObject(c);

として実行結果「0」を得てJavaVMを一旦終了し,次に,

FileInputStream fis = new FileInputStream(“./test.ser”); ObjectInputStream in = new ObjectInputStream(fis); Component c = (Component)in.readObject(); c.onClick();

としてあげれば,実行結果「1」を得ることができるだろう。Javassistによって生成した動的プロキシに関しても,writeReplace()/readResolve()メソッドの機構をうまく利用することで,シリアライズが実現可能になることを上記の例は示してくれている。

DIコンテナの普及のおかげで,動的プロキシの生成を自前で行う機会はそう多くないかもしれない。しかし,動的プロキシによって実現できることは多岐に渡るため,知ってるのと知らないのとでは雲泥の差がある。もちろん「ご利用は計画的に」なのは言うまでもないが。。

このエントリーをはてなブックマークに追加

関連記事

2023年のRemap

Remapにファームウェアビルド機能を追加しました

Google I/O 2023でのウェブ関連のトピック

2022年を振り返って

現在のRemapと今後のRemapについて