Spring 3.1 の Cache Abstraction(キャッシュの抽象化)

しばらくコード付きのエントリ書いてないなぁ、と思ったので。Springの新機能についてひとつ。

Spring3.1は、まだリリース版は出ていないのだけど、RC1が出ている。(参考 Spring 3.1 RC1リリース

その新機能にCache Abstraction(キャッシュの抽象化)ってのがあって、色々調べてみた。例えばWebAPIなんかを叩いて情報を取ってくるようなメソッドは、情報があまり変化しないものであればキャッシュしちゃえばいいよね。例えば Amazon API で、ASINから商品名やら何やかんやを取ってくるメソッドとか。

下準備

package jp.xet.sample;

public interface EntityRepository {
    
  String get(int id);
    
  void put(int id, String value);
    
}

例えばこんな(↑)インターフェイスがあって、このgetのコストが高いとしましょう。で、今回使うサンプルの実装がコレ(↓)。コストの高さをThread.sleepで表現してみました。実際は単なるMapストレージなんだけども。

package jp.xet.sample;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Repository;

@Repository
public class EntityRepositoryImpl implements EntityRepository {
    
  private Map<Integer, String> storage = new HashMap<Integer, String>();
    
    
  @Override
  public String get(int id) {
      
    // simulate slow operation
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      throw new AssertionError(e);
    }
      
    return storage.get(id);
  }
    
  @Override
  public void put(int id, String value) {
    storage.put(id, value);
  }
}

で、こいつ(↑)には@Repositoryアノテーションがついている。まぁ@Componentと全く一緒*1らしい。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd">

 <context:component-scan base-package="jp.xet.sample"/>

</beans>

で、jp.xet.sampleパッケージ以下のアノテーション付きのコンポーネントをcontext.xmlで読んでもらう、と。まぁ、EntityRepositoryImplしかないんですが。そんなわけで、Mainクラスいきましょう。

package jp.xet.sample;

import org.apache.commons.lang.time.StopWatch;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    
    
  public static void main(String[] args) {
    ApplicationContext ctx =
      new ClassPathXmlApplicationContext("/context.xml");
    EntityRepository repos = ctx.getBean(EntityRepository.class);

こうして出来たコンテナからは、EntityRepositoryが取り出せる。あとは、適当にデータを置いて、時間をはかってみる。

    repos.put(1, "one");
    repos.put(2, "two");
    repos.put(3, "three");
    
    StopWatch sw = new StopWatch();
    sw.start();
    
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(3), sw.toString());
    
    repos.put(1, "壱");
    repos.put(3, "参");
    
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(3), sw.toString());
  }
}
time=0:00:01.002, value=one
time=0:00:02.012, value=two
time=0:00:03.014, value=one
time=0:00:04.014, value=two
time=0:00:05.015, value=three
time=0:00:06.016, value=壱
time=0:00:07.018, value=two
time=0:00:08.018, value=壱
time=0:00:09.019, value=two
time=0:00:10.020, value=参

だいたい、1getにつき1秒掛かってますね。

このget処理をキャッシングして、高速化しよう

まずcontext.xmlに手をいれましょう。キャッシュの設定。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:cache="http://www.springframework.org/schema/cache"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">

 <cache:annotation-driven />
 <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
  <property name="caches">
   <set>
    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
     <property name="name" value="default"/>
    </bean>
   </set>
  </property>
 </bean>

 <context:component-scan base-package="jp.xet.sample"/>

</beans>

この設定は、裏ではシンプルに普通のConcurrentHashMapに値をキャッシュする設定です。この他に、springではehcacheにも対応したりしてるらしい。で、ここでは「default」という名前のキャッシュを1つ作りました、ということになります。次にEntityRepositoryImplに @Cacheable アノテーションをつける。

  @Cacheable(value = "default")
  public String get(int id) {
    // ...
  }

こんな感じ。このメソッドの結果はキャッシュして、キャッシュヒットした場合は中身は実際には呼ばずに済ますよ、という意味です。この状態でもっかいMainを実行してみよう。

time=0:00:01.004, value=one
time=0:00:02.016, value=two
time=0:00:02.017, value=one
time=0:00:02.017, value=two
time=0:00:03.018, value=three
time=0:00:03.018, value=one
time=0:00:03.019, value=two
time=0:00:03.019, value=one
time=0:00:03.019, value=two
time=0:00:03.019, value=three

最初のone,twoには1秒ずつ掛かっているけど、その次のone,twoは一瞬で終わっている。threeは初アクセスなので再び1秒掛かってる。で、残りはもう全部キャッシュに載っているのでずばーーーっと。計約3秒。すばらしい。

漢数字はどうした

    repos.put(1, "壱");
    repos.put(3, "参");

途中でoneとthreeの値を書き換えたのだが、無視してキャッシュを返しちゃってますね。これを何とかしたい。ならば@CacheEvictアノテーションだ。

  @CacheEvict(value = "default", key = "#id")
  public void put(int id, String value) {
    // …
  }

このアノテーションがついたメソッドが呼ばれた時は、特定のキャッシュエントリを無効にします、っていうアノテーションだ。で、どのキャッシュエントリを無効にするの? ってのが key パラメータ。ここでは「idって名前の引数を利用します」ってことです。この表記法はSpEL(Spring Expression Langage)参照。

ちなみに、@CacheEvict(value = "default", allEntries = true) とすると、全キャッシュエントリをクリアしてくれる。

という対策を施し、再びMainを回す。

time=0:00:01.001, value=one
time=0:00:02.011, value=two
time=0:00:02.012, value=one
time=0:00:02.012, value=two
time=0:00:03.013, value=three
time=0:00:04.014, value=壱
time=0:00:04.014, value=two
time=0:00:04.015, value=壱
time=0:00:04.015, value=two
time=0:00:05.015, value=参

キャッシュのevictが上手く動いていることが分かります。

*1:ざっくりとしたコンポーネントには@Componentを付ければいいんだけど「こいつはコンポーネントだけど、特にリポジトリなんだよ」っていう気分で@Repositoryってのが用意されている。あとは@Serviceなんてのもある。気分で使い分ければよろし。