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