メソッドの呼び出し結果をProxyを使用してキャッシュする実験

インタフェース経由でのメソッド呼び出しにおいて、間にProxyを挟んでキャッシュするサンプル。
DIコンテナは不要だけど、DIコンテナを使用して粗結合にしている場合に適用が簡単という例示の為に使ってます。

ソースコード

CacheおよびCacheManagerクラスについては、d:id:terazzo:20080106:1199646229のものを流用。


キャッシュを実施するメソッドを指定する為に今回はアノテーションを使用する。


アノテーションを作成。名前がCacheクラスと被るとややこしいのでDoCacheとする

package sample.cache.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value=ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DoCache {

}


Proxyでキャッシュをおこなう為のInvocationHandlerを実装。
中身はd:id:terazzo:20080106:1199646229のCacheInterceptorととても似ている

package sample.cache.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;

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

import sample.cache.annotation.DoCache;
import sample.cache.cache.Cache;
import sample.cache.cache.CacheManager;

/** 引数をキーに戻り値をキャッシュするInvocationHandler */
public class CacheInvocationHandler implements InvocationHandler {

    private Log logger = LogFactory.getLog(getClass());
    /** メソッド毎にキャッシュを管理するオブジェクト */
    private CacheManager cacheManager;
    /** 委譲先のオブジェクト */
    private Object target;

    public CacheInvocationHandler(Object target, CacheManager cacheManager) {
        super();
        if (target == null) {
            throw new IllegalArgumentException("target is null.");
        }
        if (cacheManager == null) {
            throw new IllegalArgumentException("cacheManager is null.");
        }
        this.target = target;
        this.cacheManager = cacheManager;
    }

    /**
     * メソッド呼び出しの際に全引数をキーとして戻り値をキャッシュする
     */
    public Object invoke(Object obj, Method method, Object[] arguments) throws Throwable {
        // アノテーションが無ければキャッシュ機能を使用しない
        if (!doesCache(method)) {
            return method.invoke(target, arguments);
        }
        // このメソッド用のキャッシュを取得
        Cache cache = this.cacheManager.getCache(method);
        // 引数からキーを作成
        Object key = Arrays.asList(arguments);
        // 既にキャッシュされた結果があればそれを戻す
        if (cache.isCached(key)) {
            logger.debug("hit.");
            return cache.getCachedObject(key);
        }
        logger.debug("miss.");
        // メソッドを呼び出して結果を得る
        Object result = method.invoke(target, arguments);
        //結果をキャッシュ
        cache.cacheObject(key, result);
        // 結果を戻す
        return result;
     }

    /**
     * キャッシュを適用するかどうかを判定
     * @param method 対象となるMethod
     * @return 適用する場合にtrueを戻す。
     * この実装では@DoCacheアノテーションを指定されたメソッドの場合trueを戻す。
     */
    protected boolean doesCache(Method method) {
        return method.isAnnotationPresent(DoCache.class);
    }

}


Proxyを生成しやすいようにUtilityクラスを作成する

package sample.cache.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public final class ProxyUtils {
    private ProxyUtils() {
    }

    /** invocationHandlerをInvocationHandlerに指定しproxyClassのProxyを作成する */
    public static Object createProxy(Class proxyClass, InvocationHandler invocationHandler) {
        return Proxy.newProxyInstance(proxyClass.getClassLoader(),
                new Class[] { proxyClass },
                invocationHandler);

    }

}

使用サンプル

d:id:terazzo:20080106:1199646229で使用したFileReaderクラスを実装とインタフェースに分ける。


インタフェースの方の名前をFileReaderにする。
今回キャッシュするgetFileContents()にはアノテーション@DoCacheを追加。

package sample.cache;

import java.io.File;
import java.io.IOException;

import sample.cache.annotation.DoCache;

public interface FileReader {

    /**
     * ファイルの内容を取得する
     * @param file 対象ファイル
     * @return ファイルの内容を表すバイト配列
     * @throws IOException ファイル読み込み時の例外
     */
    @DoCache
    byte[] getFileContents(File file) throws IOException;

}


実装の方(クラス名をFileReaderImplに変更)

package sample.cache;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class FileReaderImpl implements FileReader {
    
    private static final int BUFFER_LENGTH = 1024;

    /**
     * @see sample.cache.FileReader#getFileContents(java.io.File)
     */
    public byte[] getFileContents(File file) throws IOException {
        if (!file.isFile() || !file.canRead()) {
            return null;
        }
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(file);
            byte[] buffer = new byte[BUFFER_LENGTH];
            int len;
            while ((len = fin.read(buffer, 0, BUFFER_LENGTH)) != -1) {
                bout.write(buffer, 0, len);
            }
            return bout.toByteArray();
        } finally {
            if (fin != null) {
                fin.close();
            }
        }
    }
}


diconファイルでProxyを使用する
cache_proxy.dicon:

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
    "http://www.seasar.org/dtd/components21.dtd">
<components>
<!-- 元は次のようにFileReaderImplが定義されていたとする-->
<!--
    <component name="fileReader" class="sample.cache.FileReaderImpl" />
-->
<!--これを次のように置き換える-->

    <component name="cacheManager" class="sample.cache.cache.CacheManager"/>

    <component name="handler" class="sample.cache.proxy.CacheInvocationHandler">
        <arg>
            <component class="sample.cache.FileReaderImpl" />
        </arg>
        <arg>cacheManager</arg>
    </component>
    
    <component name="fileReader" class="sample.cache.FileReader">
        @sample.cache.proxy.ProxyUtils@createProxy(
            @sample.cache.FileReader@class, handler)
    </component>    

</components>


呼び出してみる

    private FileReader fileReader; // setter などでDI
    ...
    public void getFileContentsSample() {
        String path = "/Users/terazzo/sample.txt";
        File file = new File(path);
        try {
            // 一回目
            byte[] contents1 = this.fileReader.getFileContents(new File(path));
            // 二回目
            byte[] contents2 = this.fileReader.getFileContents(new File(path));
            // 三回目
            byte[] contents3 = this.fileReader.getFileContents(new File(path));
        } catch (IOException e) {
            e.printStackTrace();
        }


実行結果

DEBUG [main] (CacheInvocationHandler.java:46) - miss.
DEBUG [main] (CacheInvocationHandler.java:43) - hit.
DEBUG [main] (CacheInvocationHandler.java:43) - hit.

補足

AOPの時と同様、毎回プログラムを書かなくても境界に設定可能なのがメリット。但しあらかじめインタフェース化されている必要があるし、アノテーションをインタフェースに追加する必要がある。


スレッド非対応とかCacheクラスあたりの問題点はd:id:terazzo:20080106:1199646229と同じ。


今回は明示的に実装クラスがある場合だけど、当然S2DaoのDaoインタフェースなどに対しても使用可能。


しかしアノテーションを使わずにCacheInvocationHandler生成時にリストでメソッド名を指定した方が良いかも。付けたり外したりするもんじゃあないってどこかで読んだ。


というかAOP使う方が良そう。