Spring BootでSpring Cache(Cache Abstraction)を試す

Spring BootでSpring Cache(Cache Abstraction)を試したメモです

Spring BootでSpring Cache(Cache Abstraction)のAOPを試してみました。

Cache Abstraction

Cache Abstractionはキャッシュを抽象化する仕組みです。
実際のキャッシュの実装に依存せずにキャッシュを操作するインターフェースを提供します。

下記の実装がサポートされていて、CacheManagerのBean定義をしていない場合、
下記の順番でキャッシュの順番を検出していくようです。

 ・Generic
 ・JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, etc)
 ・EhCache 2.x
 ・Hazelcast
 ・Infinispan
 ・Couchbase
 ・Redis
 ・Caffeine
 ・Guava (deprecated)
 ・Simple

今回は何も指定しないので、Simple(ConcurrentHashMap)が使われるはずです。

Maven Dependency

mavendependencyには下記を追記しました。

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@EnableCaching

キャッシュを有効にするには@EnableCachingを指定します。
@Configurationクラスに指定する必要があります。

@SpringBootApplicationのクラスに指定しました。

@EnableCaching
@SpringBootApplication
public class SpringCacheExamplesApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCacheExamplesApplication.class, args);
    }
}
@Cacheable

@Cacheableをメソッドに付与すると、結果をキャッシュします。
@Cacheable("xxx")でキャッシュに名前をつけることができます。

引数なしの場合

引数がないメソッドの場合、キャッシュのキーはSimpleKey.EMPTYになります。

@Cacheable("myCache")
public String getString() {
    heavyTask();
    return "Hello!!";
}

重い処理を表現するために下記のダミーメソッドを用意。

private void heavyTask() {
    try {
        Thread.sleep(2_000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

テスト。
時間計測用のメソッドを用意。

private void time(Supplier supplier) {
    long start = System.currentTimeMillis();
    System.out.print(supplier.get());
    long end = System.currentTimeMillis();
    System.out.println(" [" + (end - start) + "msec]");
}

キャッシュされて高速化されていることを確認。

@Test
public void myCacheTest() throws Exception {
    // 1回目キャッシュなし
    time(() -> cacheService.getString());
    Thread.sleep(3_000);

    // 2回目キャッシュヒット
    time(() -> cacheService.getString());
}

2回目は高速化されていることが分かります。

Hello!! [2009msec]
Hello!! [1msec]

getString()でキャッシュ後に、同じキャッシュで別の引数なしのメソッドgetAnotherString()を呼び出すと、
キャッシュのキーが同じためキャッシュが返される。

@Cacheable("myCache")
public String getString() {
    heavyTask();
    return "Hello!!";
}

@Cacheable("myCache")
public String getAnotherString() {
    heavyTask();
    return "Another!!";
}

テスト。

@Test
public void anotherCacheTest() throws Exception {
    // getString 1回目キャッシュなし
    time(() -> cacheService.getString());
    Thread.sleep(3_000);

    // getString 2回目キャッシュヒット
    time(() -> cacheService.getString());
    Thread.sleep(3_000);

    // AnotherString 1回目キャッシュヒット
    time(() -> cacheService.getAnotherString());
}

"Another!!"ではなく"Hello!"が返される。

Hello!! [2014msec]
Hello!! [1msec]
Hello!! [0msec]

引数が1つの場合

引数のあるメソッドの場合、 キャッシュのキーはメソッドの引数になります。

@Cacheable("cacheWithArg")
public String getStringWithArg(String str) {
    heavyTask();
    return "Arg:" + str;
}

テスト。

@Test
public void cacheWithArgTest() throws Exception {
    // 1回目キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    Thread.sleep(3_000);

    // 1回目キャッシュなし
    time(() -> cacheService.getStringWithArg("bbb"));
    Thread.sleep(3_000);

    // 2回目キャッシュヒット
    time(() -> cacheService.getStringWithArg("aaa"));
}

引数ごとにキャッシュしているのがわかる。

Arg:aaa [2009msec]
Arg:bbb [2000msec]
Arg:aaa [2msec]

引数が複数の場合

複数の引数を取るメソッドの場合、引数の組み合わせがキャッシュキーとなります。

@Cacheable("cacheWithArgs")
public String getStringWithArgs(String str, int num, boolean isActive) {
    heavyTask();
    return "Args:" + str + num + isActive;
}

テスト。

@Test
public void cacheWithArgsTest() throws Exception {
    // キャッシュなし
    time(() -> cacheService.getStringWithArgs("aaa", 111, true));
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheService.getStringWithArgs("aaa", 111, false));
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheService.getStringWithArgs("bbb", 111, true));
    Thread.sleep(3_000);

    // キャッシュヒット
    time(() -> cacheService.getStringWithArgs("aaa", 111, true));
}

引数の組み合わせごとにキャッシュしているのが分かる。

Args:aaa111true [2009msec]
Args:aaa111false [2000msec]
Args:bbb111true [2000msec]
Args:aaa111true [2msec]

引数が複数の場合(key指定)

複数の引数を取るメソッドで、特定の引数をキャッシュキーにしたくない場合、
key属性を指定して、キャッシュキーとなる引数を指定できます。

下記のメソッドでnumとisActiveがキャッシュのキーとして不要な情報な場合、strをキーに指定します。

@Cacheable(value = "cacheWithArgsAndKey", key = "#str")
public String getStringWithArgsAndKey(String str, int num, boolean isActive) {
    heavyTask();
    return "Args:" + str + num + isActive;
}

テスト。

@Test
public void cacheWithArgsAndKeyTest() throws Exception {
    // キャッシュなし
    time(() -> cacheService.getStringWithArgsAndKey("aaa", 111, true));
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheService.getStringWithArgsAndKey("bbb", 111, true));
    Thread.sleep(3_000);

    // キャッシュヒット
    time(() -> cacheService.getStringWithArgsAndKey("aaa", 222, false));
}

key属性がキャッシュキーとなっているのが分かる。

Args:aaa111true [2050msec]
Args:bbb111true [2000msec]
Args:aaa111true [2msec]
@CachePut

@CachePutでキャッシュの値を更新できます。
@CachePutを付与したメソッドの結果でキャッシュが更新されます。

下記では、newStrでキャッシュを更新しています。キャッシュキーをkey属性で指定しています。

@CachePut(cacheNames = "cacheWithArg", key = "#str")
public String put(String str, String newStr) {
    heavyTask();
    return "Arg:" + newStr;
}

テスト。

@Test
public void putTest() throws Exception {
    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    Thread.sleep(3_000);

    // キャッシュ更新
    time(() -> cacheService.put("aaa", "newVal"));
    Thread.sleep(3_000);

    // キャッシュヒット
    time(() -> cacheService.getStringWithArg("aaa"));
}

キャッシュが更新されているのが分かる。

Arg:aaa [2012msec]
Arg:newVal [2037msec]
Arg:newVal [1msec]
@CacheEvict

@CacheEvictでキャッシュの値を削除できます。
@CacheEvictを付与したメソッドで引数をキーとしてキャッシュを削除します。

戻り値はvoidを指定しています。戻り値があっても無視されます。

@CacheEvict(cacheNames = "cacheWithArg")
public void evict(String str) {
}

テスト。

public void evictTest() throws Exception {
    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    time(() -> cacheService.getStringWithArg("bbb"));
    Thread.sleep(3_000);

    // キャッシュ削除
    cacheService.evict("aaa");
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    // キャッシュヒット
    time(() -> cacheService.getStringWithArg("bbb"));
}

指定したキャッシュが削除されていることが分かる。

Arg:aaa [2014msec]
Arg:bbb [2000msec]
Arg:aaa [2000msec]
Arg:bbb [1msec]

特定のキーではなくキャッシュ全体を削除したい場合、allEntries属性を指定します。

@CacheEvict(cacheNames = "cacheWithArg", allEntries = true)
public void evictAll(String str) {
}

テスト。

@Test
public void evictAllTest() throws Exception {
    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    time(() -> cacheService.getStringWithArg("bbb"));
    Thread.sleep(3_000);

    // キャッシュ削除
    cacheService.evictAll("aaa");
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    time(() -> cacheService.getStringWithArg("bbb"));
}

すべてのキャッシュが削除されていることが分かる。

Arg:aaa [2010msec]
Arg:bbb [2000msec]
Arg:aaa [2001msec]
Arg:bbb [2000msec]
@Caching

@Cachingを付与すると@CachePutや@CacheEvictを複数指定できます。
異なるキャッシュに対して操作をすることが出来ます。

@Caching(evict = {@CacheEvict("cacheWithArg"), @CacheEvict(cacheNames = "anotherCacheWithArg")})
public void caching(String str) {
}

テスト。

@Test
public void cachingTest() throws Exception {
    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    time(() -> cacheService.getAnotherStringWithArg("aaa"));
    Thread.sleep(3_000);

    // キャッシュ削除
    cacheService.caching("aaa");
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheService.getStringWithArg("aaa"));
    time(() -> cacheService.getAnotherStringWithArg("aaa"));
}

異なるキャッシュから削除できていることが分かる。

Arg:aaa [2013msec]
Name:aaa [2000msec]
Arg:aaa [2001msec]
Name:aaa [2001msec]
@CacheConfig

@CacheConfigを指定すると、メソッド単位ではなくクラス単位でキャッシュの設定ができます。

下記のように@CacheConfigでキャッシュ名を指定します。

@Service
@CacheConfig(cacheNames = "configCache")
public class CacheConfigService {
    @Cacheable
    public String get(String str) {
        heavyTask();
        return "get:" + str;
    }

    @CachePut
    public String put(String str) {
        heavyTask();
        return "put:" + str;
    }

    @CacheEvict
    public void delete(String str) {
    }

    private void heavyTask() {
        try {
            Thread.sleep(2_000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

テスト。

@Test
public void cacheConfigTest() throws Exception {
    // キャッシュなし
    time(() -> cacheConfigService.get("aaa"));
    Thread.sleep(3_000);

    // キャッシュ更新
    time(() -> cacheConfigService.put("aaa"));
    Thread.sleep(3_000);

    // キャッシュヒット
    time(() -> cacheConfigService.get("aaa"));
    Thread.sleep(3_000);

    // キャッシュ削除
    cacheConfigService.delete("aaa");
    Thread.sleep(3_000);

    // キャッシュミス
    time(() -> cacheConfigService.get("aaa"));
    Thread.sleep(3_000);
}

キャッシュの取得、更新、削除が出来ているのが分かる。

get:aaa [2008msec]
put:aaa [2000msec]
put:aaa [2msec]
get:aaa [2001msec]

JSR-107(JCache)

JSR-107(JCache)のアノテーションもサポートしています。

SpringとJSR-107の対応表と違いは下記に書いています。
36. Cache Abstraction

Maven Dependency

maven dependencyには下記を追記しました。

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.0.0</version>
</dependency>
@CacheResult

@CacheResultをメソッドに付与すると、結果をキャッシュします。
@CacheResult("xxx")でキャッシュに名前をつけることができます。

springの@Cacheableと似た機能です。

@CacheResult(cacheName = "jsr107Cache")
public String get(String str) {
    heavyTask();
    return "Arg:" + str;
}

テスト。

@Test
public void getTest() throws Exception {
    // 1回目キャッシュなし
    time(() -> cacheJsr107Service.get("aaa"));
    Thread.sleep(3_000);

    // 2回目キャッシュヒット
    time(() -> cacheJsr107Service.get("aaa"));
}

キャッシュされて高速化されているのが分かる。

Arg:aaa [2008msec]
Arg:aaa [1msec]
@CachePut

@CachePutでキャッシュの値を更新できます。
@CachePutを付与したメソッドの結果でキャッシュが更新されます。

springの@CachePutと同じ名前です。

@CachePut(cacheName = "jsr107Cache")
public String put(String str, @CacheValue String newStr) {
    heavyTask();
    return "Arg:" + newStr;
}

テスト。

@Test
public void putTest() throws Exception {
    // キャッシュなし
    time(() -> cacheJsr107Service.get("aaa"));
    Thread.sleep(3_000);

    // キャッシュ更新
    time(() -> cacheJsr107Service.put("aaa", "newVal"));
    Thread.sleep(3_000);

    // キャッシュヒット
    time(() -> cacheJsr107Service.get("aaa"));
}

キャッシュが更新されていることが分かる。

Arg:aaa [2010msec]
Arg:newVal [2001msec]
newVal [1msec]
@CacheRemove

@CacheRemoveでキャッシュの値を削除できます。
@CacheRemoveを付与したメソッドで引数をキーとしてキャッシュを削除します。

springの@CacheEvictと似た機能です。

@CacheRemove(cacheName = "jsr107Cache")
public void remove(String str) {
}

テスト。

@Test
public void removeTest() throws Exception {
    // キャッシュなし
    time(() -> cacheJsr107Service.get("aaa"));
    time(() -> cacheJsr107Service.get("bbb"));
    Thread.sleep(3_000);

    // キャッシュ削除
    cacheJsr107Service.remove("aaa");
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheJsr107Service.get("aaa"));
    // キャッシュヒット
    time(() -> cacheJsr107Service.get("bbb"));
}

キャッシュが削除されていることが分かる。

Arg:aaa [2008msec]
Arg:bbb [2000msec]
Arg:aaa [2000msec]
Arg:bbb [2msec]
@CacheRemoveAll

@CacheRemoveAllを指定するとキャッシュ全体を削除できます。

springでは@CacheEvict(allEntries=true)と同等の機能です。

@CacheRemoveAll(cacheName = "jsr107Cache")
public void removeAll() {
}

テスト。

@Test
public void removeAllTest() throws Exception {
    // キャッシュなし
    time(() -> cacheJsr107Service.get("aaa"));
    time(() -> cacheJsr107Service.get("bbb"));
    Thread.sleep(3_000);

    // キャッシュ削除
    cacheJsr107Service.removeAll();
    Thread.sleep(3_000);

    // キャッシュなし
    time(() -> cacheJsr107Service.get("aaa"));
    time(() -> cacheJsr107Service.get("bbb"));
}

キャッシュがすべて削除されていることが分かる

Arg:aaa [2011msec]
Arg:bbb [2000msec]
Arg:aaa [2000msec]
Arg:bbb [2000msec]

終わり。

今回はConcurrentHashMapを使用したキャッシュだったので、
もう少し高機能なCaffeineを試したいと思います。


【参考】
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html
http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html
https://blog.ik.am/entries/339
http://d.hatena.ne.jp/Kazuhira/20170204/1486188414

サンプルコードは下記に置きました。

github.com