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
mavenのdependencyには下記を追記しました。
<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
サンプルコードは下記に置きました。