CLOVER🍀

That was when it all began.

SpringのCache Abstractionで、 アノテーションに複数のキャッシュを指定した場合の動きを確認する

SpringのCache Abstractionで提供される各種アノテーションでは、使用するキャッシュを複数指定することが
できます。

Cache / Declarative annotation-based caching

複数のキャッシュを設定した時に、どのような動作をするのか確認してみましょう。なんとなく、ある程度自明な
気もしますが。

例えば、こういうやつです。

    @Cacheable(cacheNames = {"firstCache", "secondCache"}, key = "#value")
    public Message build(String value) {
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }

        return new Message(value);
    }

@CacheableアノテーションのcacheNamesに、複数のキャッシュを指定しています。

例えば、ここで指定している「firstCache」と「secondCache」の2つのキャッシュに、別々の有効期限を指定して
動きを見る、ということをやってみましょう。

準備

まずは、Maven依存関係から。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.boot.version>1.5.1.RELEASE</spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

キャッシュのバックエンドには、Redis(3.2.7)を使用します。protected modeは、とりあえずオフで。

サンプルアプリケーション

それでは、サンプルアプリケーションを用意します。

まずはキャッシュに値として保存するクラス。
src/main/java/org/littlewings/spring/cache/Message.java

package org.littlewings.spring.cache;

import java.io.Serializable;
import java.time.LocalDateTime;

public class Message implements Serializable {
    private static final long serialVersionUID = 1L;

    private String value;
    private LocalDateTime now;

    public Message() {
    }

    public Message(String value) {
        this.value = value;
        now = LocalDateTime.now();
    }

    public String getValue() {
        return value;
    }

    public LocalDateTime getNow() {
        return now;
    }
}

続いて、キャッシュを使う方のクラス。@Cacheableと@CachePutアノテーションを付与し、それぞれcacheNamesに
2つキャッシュを設定します。
src/main/java/org/littlewings/spring/cache/MessageService.java

package org.littlewings.spring.cache;

import java.util.concurrent.TimeUnit;

import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class MessageService {
    @Cacheable(cacheNames = {"firstCache", "secondCache"}, key = "#value")
    public Message build(String value) {
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }

        return new Message(value);
    }

    @CachePut(cacheNames = {"firstCache", "secondCache"}, key = "#message.value")
    public Message update(Message message) {
        return message;
    }
}

@Cacheableでキャッシュになければ値を登録、キャッシュにあればその値を使用、@CachePutでキャッシュを更新ですね。
キャッシュの値は、ともにメソッドの戻り値になりますと。

@Cacheableを付与したメソッドには、3秒間のスリープを入れています。

キャッシュの設定。@EnableCacingでCache Abstractionを有効化するとともに、それぞれのキャッシュの有効期限を
設定します。
src/main/java/org/littlewings/spring/cache/CacheConfig.java

package org.littlewings.spring.cache;

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

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);

        cacheManager.setUsePrefix(true);

        Map<String, Long> expires = new HashMap<>();
        expires.put("firstCache", 3L);
        expires.put("secondCache", 6L);

        cacheManager.setExpires(expires);

        cacheManager.afterPropertiesSet();
        return cacheManager;
    }
}

今回は、「firstCache」の有効期限を3秒、「secondCache」の有効期限を6秒にしました。

最後に、@SpringBootApplicationアノテーションを付与したクラスを用意。テストのためだけに存在するので、
今回はmainメソッドは用意していません。
src/main/java/org/littlewings/spring/cache/App.java

package org.littlewings.spring.cache;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
}

確認

それでは、テストコードで確認してみます。こんな感じの雛形を用意。
src/test/java/org/littlewings/spring/cache/MultipleCacheTest.java

package org.littlewings.spring.cache;

import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.CacheManager;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StopWatch;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MultipleCacheTest {
    @Autowired
    CacheManager cacheManager;

    @Autowired
    MessageService messageService;

    // ここに、テストを書く!
}

先ほど用意したキャッシュを使ったクラスとは別に、直接キャッシュの状況を確認するためにCacheManagerも使います。

まずは@Cacheableの確認をしてみましょう。テストコードはこちら。

    @Test
    public void cacheable() throws InterruptedException {
        StopWatch stopWatch = new StopWatch();

        // 初回アクセス
        stopWatch.start();
        Message m1 = messageService.build("Hello World");
        stopWatch.stop();

        // 3秒かかる
        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isGreaterThanOrEqualTo(3L);

        // どちらもキャッシュされている
        Message m1FromFirst = (Message) cacheManager.getCache("firstCache").get("Hello World").get();
        Message m1FromSecond = (Message) cacheManager.getCache("secondCache").get("Hello World").get();
        assertThat(m1FromFirst.getNow())
                .isEqualTo(m1FromSecond.getNow());

        // 2回目アクセス
        stopWatch.start();
        Message m2 = messageService.build("Hello World");
        stopWatch.stop();

        // 高速
        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isEqualTo(0L);

        // 3秒スリープ
        TimeUnit.SECONDS.sleep(3L);

        // firstCacheが期限切れ
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        Message m2FromSecond = (Message) cacheManager.getCache("secondCache").get("Hello World").get();

        assertThat(m2FromSecond.getNow())
                .isEqualTo(m1.getNow())
                .isEqualTo(m2.getNow());

        // 3回目アクセス
        stopWatch.start();
        Message m3 = messageService.build("Hello World");
        stopWatch.stop();

        // 高速
        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isEqualTo(0L);

        // secondCacheはまだ有効、@Cacheableなメソッドを呼び出したからといってfirstCacheは復活しない
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

        // 3秒スリープ
        TimeUnit.SECONDS.sleep(3L);

        // どちらも有効期限切れ
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNull();
    }

時間は、SpringのStopWatchで計測しています。

初回アクセスは、キャッシュになにも入っていないので時間がかかります。

        // 初回アクセス
        stopWatch.start();
        Message m1 = messageService.build("Hello World");
        stopWatch.stop();

        // 3秒かかる
        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isGreaterThanOrEqualTo(3L);

        // どちらもキャッシュされている
        Message m1FromFirst = (Message) cacheManager.getCache("firstCache").get("Hello World").get();
        Message m1FromSecond = (Message) cacheManager.getCache("secondCache").get("Hello World").get();
        assertThat(m1FromFirst.getNow())
                .isEqualTo(m1FromSecond.getNow());

そして、キャッシュにエントリが登録されます、と。

2回目のアクセス。こちらは高速です。

        // 2回目アクセス
        stopWatch.start();
        Message m2 = messageService.build("Hello World");
        stopWatch.stop();

        // 高速
        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isEqualTo(0L);

3秒間スリープを入れると、先に「firstCache」が有効期限切れします。

        // 3秒スリープ
        TimeUnit.SECONDS.sleep(3L);

        // firstCacheが期限切れ
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        Message m2FromSecond = (Message) cacheManager.getCache("secondCache").get("Hello World").get();

        assertThat(m2FromSecond.getNow())
                .isEqualTo(m1.getNow())
                .isEqualTo(m2.getNow());

3回目のアクセス。「secondCache」にはキャッシュにエントリがまだ残っているので、変わらず高速です。

        // 3回目アクセス
        stopWatch.start();
        Message m3 = messageService.build("Hello World");
        stopWatch.stop();

        // 高速
        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isEqualTo(0L);

        // secondCacheはまだ有効、@Cacheableなメソッドを呼び出したからといってfirstCacheは復活しない
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

しかし、@Cacheableなメソッドを呼び出したからといって、「firstCache」になにか変化があるわけではありません。ということは、
エントリを持ったキャッシュを順次探し、見つかればそれを返す、くらいなわけですね。

さらに3秒間スリープすると、どちらも有効期限切れします、と。

        // 3秒スリープ
        TimeUnit.SECONDS.sleep(3L);

        // どちらも有効期限切れ
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNull();

続いて、@CachePutも交えて確認してみましょう。テストコードは、こちら。

    @Test
    public void cachePut() throws InterruptedException {
        StopWatch stopWatch = new StopWatch();

        // キャッシュ登録
        stopWatch.start();
        Message m1 = messageService.build("Hello World");
        stopWatch.stop();

        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isGreaterThanOrEqualTo(3L);
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNotNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

        // 3秒スリープ
        TimeUnit.SECONDS.sleep(3L);

        // firstCacheのみ有効期限切れ
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

        assertThat(((Message) cacheManager.getCache("secondCache").get("Hello World").get()).getNow())
                .isEqualTo(m1.getNow());

        // キャッシュ更新
        Message newMessage = new Message("Hello World");
        messageService.update(newMessage);

        // firstCache、secondCacheともに更新される
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNotNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

        assertThat(((Message) cacheManager.getCache("firstCache").get("Hello World").get()).getNow())
                .isEqualTo(((Message) cacheManager.getCache("secondCache").get("Hello World").get()).getNow())
                .isEqualTo(newMessage.getNow())
                .isNotEqualTo(m1.getNow());
    }

とりあえず、@Cacheableなメソッドでキャッシュにエントリを登録します。

        // キャッシュ登録
        stopWatch.start();
        Message m1 = messageService.build("Hello World");
        stopWatch.stop();

        assertThat((long) stopWatch.getLastTaskInfo().getTimeSeconds())
                .isGreaterThanOrEqualTo(3L);
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNotNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

3秒間スリープさせると、「firstCache」のキャッシュエントリは有効期限切れします。

        // 3秒スリープ
        TimeUnit.SECONDS.sleep(3L);

        // firstCacheのみ有効期限切れ
        assertThat(cacheManager.getCache("firstCache").get("Hello World"))
                .isNull();
        assertThat(cacheManager.getCache("secondCache").get("Hello World"))
                .isNotNull();

        assertThat(((Message) cacheManager.getCache("secondCache").get("Hello World").get()).getNow())
                .isEqualTo(m1.getNow());

ここで、@CachePutなメソッドを使用すると、「firstCache」と「secondCache」が合わせて更新されます。
この場合は、列挙したキャッシュが全部更新されるということですね。

まとめ

簡単に、SpringのCache Abstractionのアノテーションで、複数のキャッシュを指定した場合の動作を確認してみました。

まあ直感的な動作なので、ある意味そうだろう的な感じではありますが。

今回の処理は、大方はここ中に書いてあるので、興味のある方は見てみるとよいでしょう。
https://github.com/spring-projects/spring-framework/blob/v4.3.6.RELEASE/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java#L356