2012年12月10日月曜日

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

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

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

アノテーションを作成。名前が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を実装。

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);

}

}

インタフェースの方の名前を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.

0 件のコメント:

コメントを投稿