メソッドの呼び出し結果をAOPでキャッシュする実験

DI+AOPにはSeasar2を使用。
このサンプルの問題点などは後ろで。

ソースコード

まずキーに対して値をキャッシュするクラスを作る(今回は単純にHashMapで)

package sample.cache.cache;

import java.util.HashMap;
import java.util.Map;

public class Cache {
    private Map map = new HashMap();
    public boolean isCached(Object key) {
        return map.containsKey(key);
    }
    public void cacheObject(Object key, Object value) {
         map.put(key, value);
    }
    public Object getCachedObject(Object key) {
        return map.get(key);
    }

}


上のCacheオブジェクトをメソッド毎に管理するクラスを作る(今回はキャッシュクラスの型は決め打ちで)

package sample.cache.cache;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * メソッド毎にキャッシュを管理するクラス
 */
public class CacheManager {
    private Map<Method, Cache> caches = new HashMap<Method, Cache>();
    
    public synchronized Cache getCache(Method method) {
        Cache cache = this.caches.get(method);
        
        if (cache == null) {
            cache = new Cache();
            this.caches.put(method, cache);
        }
        
        return cache;
    }

}


上のCacheおよびCacheManagerを使ってメソッドの呼び出し結果をキャッシュするMethodInterceptorを実装する。

package sample.cache.interceptor;

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

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import sample.cache.cache.Cache;
import sample.cache.cache.CacheManager;

/** 引数をキーに戻り値をキャッシュするMethodInterceptor */
public class CacheInterceptor implements MethodInterceptor {
    private Log logger = LogFactory.getLog(getClass());
    /** メソッド毎にキャッシュを管理するオブジェクト */
    private CacheManager cacheManager;
    
    public CacheInterceptor(CacheManager cacheManager) {
        super();
        if (cacheManager == null) {
            throw new IllegalArgumentException("cacheManager is null.");
        }
        this.cacheManager = cacheManager;
    }

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

}

使用サンプル

Fileを渡して中身をbyte[]で受け取るメソッドを作成し、CacheInterceptorを設定して結果をキャッシュしてみる。


クラスFileReaderを作成してFileの中身を読み込むメソッドを実装する。このメソッド自身は、呼び出される度に指定されたFileの中身を読んでバイト列を生成して戻す。

package sample.cache;

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

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

    public byte[] getFileContents(File file) throws IOException {
        if (!file.isFile()) {
            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ファイルでaspectを設定する。

cache_aop.dicon:

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
    "http://www.seasar.org/dtd/components21.dtd">
<components>
    <component name="cacheManager" class="sample.cache.cache.CacheManager"/>

    <component name="cacheInterceptor" class="sample.cache.interceptor.CacheInterceptor">
        <arg>cacheManager</arg>
    </component>

    <component class="sample.cache.FileReader">
        <aspect pointcut="getFileContents">cacheInterceptor</aspect>
    </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] (CacheInterceptor.java:40) - miss.
DEBUG [main] (CacheInterceptor.java:37) - hit.
DEBUG [main] (CacheInterceptor.java:37) - hit.

環境

  • J2SE 5.0
  • Seasar 2.4.13 (2.3でも問題ないと思う)
    • 付属のlibの中身全部
  • log4j-1.2.9.jar

なぜAOPを使うのか

DBアクセスやファイル内容の読み込みのような時間がかかったり負荷が高かったりする処理は可能な限り結果をメモリ内にキャッシュしたい。その際、キャッシュ機構をモデル毎・ロジック毎に実装するのは面倒なのでやり方は共通化したい。メソッド呼び出し方を共通化(Commandパターンなど)すれば前面に共通のキャッシュを設置出来るだろうけど、前処理が面倒だったりプログラムの見通しが悪くなったりするので、そういう全体の構成を大きく変える手段は出来るだけ取りたくない。


キャッシュの目的はパフォーマンスの向上(負荷の低減やレスポンスの短縮)であって、本来のロジック(Core Concernな部分)とは無関係なものなのでAOPで実装するメリットが大きいと思う。


また、キャッシュをどのレイヤーで実現するかというようなことを考えた場合、レイヤー内部でというよりレイヤーの境界でおこないたいことが多いと思う。DIコンテナを使っていればその部分は粗結合にしているだろうから、DIコンテナの設定ファイル上で結合部位に設定してやるのが手軽で良いと思う。

このサンプルの問題点や課題

分かりやすくする為にいろいろ削ぎ落としてるんで実用性にはとても問題あり

  • スレッドセーフじゃない
    • 同時にアクセスが来た時にブロックした方が良いかとか場合によるか
  • キーを単純に引数のListにしている
    • 効率が引数の各クラスのhasCode()/equals()の書き方に依存する
      • equals()が書かれていない→ヒットしない
      • hasCode()が偏り過ぎ→パフォーマンスに影響
      • equals()が遅い→パフォーマンスに影響
    • エラーハンドラを渡したいとか、キャッシュに関係ない引数がある場合に問題
      • どの引数を使用するか指定出来るようにするとか
    • キーが変更不能でないのでそもそも危険
  • キャッシュの更新機構が無い
    • キャッシュに使用するメモリが無限に増大する
      • CacheをLRUに変えて件数を制限するなど
    • 例えばファイルが更新されたら読み直すなど
      • 結局キャッシュ対象ごとにタイムスタンプ保持して比較したり必要か
    • キャッシュ強制更新用のAPIを作る
      • よくやる方法はモデル毎に名前をつけておいてMulticastで消せるようにしておくとか
    • 更新とは別に中のクラスやCacheクラス自身のライフサイクルは考慮が必要か

Cache/CacheManagerをインタフェース化したりFactory使ったりして付け替えられるようにする必要はあるだろう。

その他

最初はDB検索の結果をキャッシュする為にDaoに対して使用することを想定していたんだけど、キャッシュってもっといろんな所で有効だと思う。メモリ内のシーケンシャルな検索をHashMapで高速化するとか。そんな時にも同じ方式が使える。


DB検索に関してはもう少し別のアプローチがあっても良いと思う。起動時にあらかじめ全件取得して問い合わせ時はメモリ内で絞り込み/ソートするとかも可能にしたい。EOQualifier/EOSortOrderingみたいに、DBに投げるのと同じ検索条件オブジェクトを使ってメモリ内でも絞り込み・ソート出来るようにする方がその辺の融通は利きやすいと思う。

  1. 毎回DBに問い合わせ
  2. DBに問い合わせるが結果をキャッシュ、
  3. 起動時に全件読み込んでメモリ内で絞り込み
  4. 起動時に全件読み込んでメモリ内で絞り込み+絞り込み結果をキャッシュ

などをエンティティやクエリパターン毎に個別に設定できるのが理想。問い合わせ条件が複雑になると実装するのも難しいのだけど。


JPA使えばこの辺は解消されるのだろうか。

追記

検索してみたらseasarのsandboxに入ってるS2Cachingというのが既にあったなあ。
s2caching - S2Cachingとは


仕方が無いので次はProxyを使ったサンプルを書こう。