Proxyを使ってメソッド呼び出しを記録/再生する実験

(前回からの続き)Proxyを使ってリモート呼び出しをおこなうという魂胆。ところでネットワークストリームを使ったoneway voidのリモート呼び出しは、ストリームをファイルに置き換えるだけで、メソッド呼び出しの記録/再生に使えるはず。ということでProxyを使ってインタフェースへのメソッド呼び出しをファイルに記録して、それを同じインタフェースを持つ別のオブジェクトに対して再生するプログラムを書いてみた。

ソースコード

まず、メソッド呼び出し時のMethodオブジェクトと引数をパックする値ホルダ

package sample.proxyrecord;

import java.lang.reflect.Method;

/**
 * メソッド呼び出し(Methodおよび引数)を表すクラス
 */
public class Invocation {
    private Method method;
    private Object[] arguments;
    
    public Invocation(Method method, Object[] arguments) {
        super();
        this.method = method;
        this.arguments = arguments;
    }
    /**
     * @return the method
     */
    public Method getMethod() {
        return method;
    }
    /**
     * @return the arguments
     */
    public Object[] getArguments() {
        return arguments;
    }
}


上記のInvocationオブジェクトをObjectInputStream/ObjectOutputStreamで読み書きするUtilsクラス

package sample.proxyrecord;

import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


@SuppressWarnings("unchecked")
public final class InvocationRecordingUtils {
    private static Log logger = LogFactory.getLog(InvocationRecordingUtils.class);
    private InvocationRecordingUtils() {
    }
    /**
     * メソッド呼び出しをストリームに記録する
     * @param outputStream ObjectOutputStream
     * @param invocation Invocation
     * @throws IOException 書き込み時の例外
     */
    public static void writeInvocation(ObjectOutputStream outputStream, Invocation invocation)
            throws IOException {
        Method method = invocation.getMethod();
        Object[] args = invocation.getArguments();
        Class declaringClass = method.getDeclaringClass();
        String methodName = method.getName();
        Class[] parameterTypes = method.getParameterTypes();
        logger.debug("write invocation: " + declaringClass.getName() + "#" + methodName);
        outputStream.writeObject(declaringClass);
        outputStream.writeObject(methodName);
        outputStream.writeObject(parameterTypes);
        outputStream.writeObject(args);
    }
    /**
     * 記録されたメソッド呼び出しをストリームから読み出す
     * @param inputStream ObjectInputStream
     * @return 読み出したメソッド呼び出しを戻す
     * @throws IOException ストリーム読み出し時の例外
     * @throws ClassNotFoundException 記録された呼び出しのクラスや引数のクラスが見つからなかった場合
     * @throws NoSuchMethodException 記録された呼び出しに対応するメソッドが見つからなかった場合
     */
    public static Invocation readInvocation(ObjectInputStream inputStream) 
            throws IOException, ClassNotFoundException, NoSuchMethodException  {
        Class declaringClass = null;
        try {
            declaringClass = (Class) inputStream.readObject();
        } catch (EOFException e) {
            
        }
        if (declaringClass == null) {
            return null;
        }
        String methodName = (String) inputStream.readObject();
        Class[] parameterTypes = (Class[]) inputStream.readObject();
        Method method = declaringClass.getMethod(methodName, parameterTypes);
        logger.debug("read invocation: " + declaringClass.getName() + "#" + methodName);
        
        Object[] args = (Object[]) inputStream.readObject();
        return new Invocation(method, args);
     }
}


記録用のProxyを作りメソッド呼び出しをストリームに記録するクラス

package sample.proxyrecord;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Proxyを利用し、設定されたインタフェースを経由したメソッド呼び出しをストリームに記録するクラス
 * @param <T> メソッド呼び出しを記録するインタフェースの型
 */
@SuppressWarnings("unchecked")
public class InvocationRecorder<T> {

    private Class<T> interfaze;
    private ObjectOutputStream outputStream;
    public InvocationRecorder(Class<T> interfaze, OutputStream outputStream) throws IOException {
        if (interfaze == null) {
            throw new IllegalArgumentException("targetClass is null.");
        }
        if (outputStream == null) {
            throw new IllegalArgumentException("outputStream is null.");
        }
        if (! interfaze.isInterface()) {
            throw new IllegalArgumentException("" + interfaze + " is not interface.");
        }
        this.interfaze = interfaze;
        this.outputStream = new ObjectOutputStream(outputStream);
        
    }

    /**
     * メソッドの呼び出しを記録しながらtargetに委譲するProxyを生成する
     * @param target 委譲するオブジェクト
     * @return Proxyを戻す
     */
    public T getProxy(T target) {
        Object proxy = Proxy.newProxyInstance(interfaze.getClassLoader(),
                new Class[] { interfaze },
                new RecordingInvocationHandler(target));
        return this.interfaze.cast(proxy);
    }

    /**
     * ストリームをcloseする
     * @throws IOException ストリームclose時の例外
     */
    public void close() throws IOException {
        if (this.outputStream != null) {
            this.outputStream.flush();
            this.outputStream.close();
        }
    }
    /**
     * メソッド呼び出しを記録するInvocationHandler
     * 記録用のOutputStreamはエンクロージングインスタンスのものを使用する
     */
    private class RecordingInvocationHandler implements InvocationHandler {

        private T target;
        
        private RecordingInvocationHandler(T target) {
            super();
            this.target = target;
        }
        
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            InvocationRecordingUtils.writeInvocation(outputStream, 
                    new Invocation(method, args));
            return method.invoke(target, args);
        }
        
    }

}


ストリームの内容を再生(メソッド呼び出し)するクラス

package sample.proxyrecord;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.InvocationTargetException;

/**
 * ストリームに記録されたメソッド呼び出しを読み出して再生するクラス
 * @param <T> メソッド呼び出しを記録したインタフェースの型
 */
public class InvocationPlayer<T> {

    private ObjectInputStream inputStream;

    public InvocationPlayer(InputStream inputStream) 
            throws IOException {
        if (inputStream == null) {
            throw new IllegalArgumentException("inputStream is null.");
        }
        this.inputStream = new ObjectInputStream(inputStream);
        
    }

    /**
     * ストリームに記録されたメソッド呼び出しをtargetに対して再生する(呼び出す)
     * @param target メソッドを呼び出すオブジェクト
     * @throws IOException ストリーム読み出し時の例外
     * @throws ClassNotFoundException 記録された呼び出しのクラスや引数のクラスが見つからなかった場合
     * @throws NoSuchMethodException 記録された呼び出しに対応するメソッドが見つからなかった場合
     * @throws IllegalAccessException アクセス制御によりメソッドが呼び出せない場合
     * @throws InvocationTargetException メソッドが例外をスローする場合
     */
    public void play(T target) throws IOException, ClassNotFoundException, 
            NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Invocation invocation = null;
        while((invocation = InvocationRecordingUtils.readInvocation(inputStream)) != null) {
            invocation.getMethod().invoke(target, invocation.getArguments());
        }
    }

    /**
     * ストリームをcloseする
     * @throws IOException ストリームclose時の例外
     */
    public void close() throws IOException {
        if (this.inputStream != null) {
            this.inputStream.close();
        }
    }

}

使用サンプル

Mapインタフェース経由で呼び出されたメソッドを記録/再生する。例として、HTMLページのタイトルと背景色をMapに設定する部分を記録するイメージ


記録側

        // タイトルと背景色の情報
        String pageTitle = "MySite";
        Map colorInfo = new HashMap();
        colorInfo.put("bgcolor", "#a1fe9f");
        colorInfo.put("text", "#000000");
        colorInfo.put("link", "#0000ff");
        colorInfo.put("vlink", "#ff0000");
        
        // 呼び出しを記録するファイル
        String recordPath = "/tmp/record.dat";

        InvocationRecorder<Map> recorder = null;
        Map configuration = new HashMap();
        
        try {
            // ファイルへの記録用のオブジェクトを準備する
            recorder = new InvocationRecorder(Map.class, new FileOutputStream(recordPath));
            // Proxyを生成する
            Map recordingMap = recorder.getProxy(configuration);
            // Proxy経由でメソッドを呼び出し(記録される)
            recordingMap.clear();
            recordingMap.put("pageTitle", pageTitle);
            recordingMap.put("colorInfo", colorInfo);
        } finally {
            // ストリームをクローズ
            if (recorder != null) {
                recorder.close();
            }
        }
        System.out.println("[recorded] configuration = " + configuration);

再生側

        String recordPath = "/tmp/record.dat";

        InvocationPlayer<Map> player = null;
        Map configuration = new HashMap();
        Map configuration2 = new HashMap();
       
        try {
            // ファイルの内容を元に再生用のオブジェクトを準備する
            player = new InvocationPlayer<Map>(new FileInputStream(recordPath));
            // 再生
            player.play(configuration);
            
        } finally {
            if (player != null) {
                player.close();
            }
        }
        System.out.println("[played] configuration = " + configuration);

出力結果

----記録----
DEBUG [main] (InvocationRecordingUtils.java:31) - write invocation: java.util.Map#clear
DEBUG [main] (InvocationRecordingUtils.java:31) - write invocation: java.util.Map#put
DEBUG [main] (InvocationRecordingUtils.java:31) - write invocation: java.util.Map#put
[recorded] configuration = {pageTitle=MySite, colorInfo={vlink=#ff0000, text=#000000, link=#0000ff, bgcolor=#a1fe9f}}
----再生----
DEBUG [main] (InvocationRecordingUtils.java:59) - read invocation: java.util.Map#clear
DEBUG [main] (InvocationRecordingUtils.java:59) - read invocation: java.util.Map#put
DEBUG [main] (InvocationRecordingUtils.java:59) - read invocation: java.util.Map#put
[played] configuration = {pageTitle=MySite, colorInfo={vlink=#ff0000, text=#000000, bgcolor=#a1fe9f, link=#0000ff}}

環境

なにが嬉しいの

……嬉しい事は特に無いです。引数もSerializableじゃないとダメだし。


本番環境の使用パターン記録して、テスト環境で再現したりとかに使えるかも?