--- title: SpringのCache Abstractionについて tags: ["JCache", "Spring"] categories: ["Programming", "Java", "org", "springframework", "cache"] date: 2015-05-16T16:00:57Z updated: 2015-05-16T16:00:57Z --- 便利だけれどあまり知られていない、Springがもつ[キャッシュ抽象化機構](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html)について説明します。 Springのキャッシュ機能では * `org.springframework.cache.CacheManager`によるキャッシュ製品共通API * AOP(`@Cacheable`)による透過的キャッシュ がサポートされています。 ### CacheManagerの使い方 `org.springframework.cache.CacheManager`インターフェースを通じて、様々なキャッシュ(`ConcurrentHashMap`や EhCacheなどなど)を同じAPIでアクセスすることができます。 使い方は ``` java // キャッシュ取得 Cache cache = cacheManager.getCache("foo"); // キャシュにキー=値を追加 cache.put("hoge", "test"); // キャシュから値を取得 System.out.println(cache.get("hoge", String.class)); // => test // キャシュから値を削除 cache.evict("hoge"); System.out.println(cache.get("hoge", String.class)); // => null // キャシュをクリア cache.clear(); ``` このAPIを使う事で製品ごとのAPIを使う必要がなくなるので、製品を入れ替えが容易になります。 例えば開発・テスト中は`ConcurrentHashMap`を使い、本番はキャッシュ製品を使うというような使い方が可能です。 #### Spring Bootから使う Spring Bootで動くプログラム全文を載せておきます。 ``` java package demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import java.util.Arrays; @SpringBootApplication public class DemoApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } // Bean定義 @Bean // CacheManagerをBean定義する必要がある CacheManager cacheManager() { // org.springframework.cache.Cacheの実装をマニュアルで登録するCacheManagerクラス SimpleCacheManager cacheManager = new SimpleCacheManager(); // ConcurrentHashMapを使ったorg.springframework.cache.Cacheの実装を登録する cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("foo"))); return cacheManager; } // ここから主プログラム @Autowired CacheManager cacheManager; @Override public void run(String... strings) throws Exception { Cache cache = cacheManager.getCache("foo"); cache.put("hoge", "test"); System.out.println(cache.get("hoge", String.class)); cache.evict("hoge"); System.out.println(cache.get("hoge", String.class)); } } ``` #### 様々なCacheManager実装 先の例では、単純な`SimpleCacheManager`を紹介しましたが、これ以外にもたくさんの`CacheManager`実装があります。 対応製品(Springまたは製品側にCacheManagerの実装があるもの)は以下の通りです。 * [ConcurrentMap](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/concurrent/ConcurrentMapCacheManager.html) * [EhCache](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/ehcache/EhCacheCacheManager.html) * [Guava](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/guava/GuavaCacheManager.html) * [JCache](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/jcache/JCacheCacheManager.html) * [Redis](http://docs.spring.io/spring-data/data-redis/docs/current/api/org/springframework/data/redis/cache/RedisCacheManager.html) * [Gemfire, Apache Geode](http://docs.spring.io/spring-gemfire/docs/current/api/org/springframework/data/gemfire/support/GemfireCacheManager.html) * [Hazelcast](http://docs.hazelcast.org/docs/latest-dev/javadoc/com/hazelcast/spring/cache/HazelcastCacheManager.html) * [Apache Ignite](https://ignite.incubator.apache.org/releases/1.0.0/javadoc/org/apache/ignite/cache/spring/SpringCacheManager.html) * [Infinispan](http://infinispan.org/docs/7.2.x/user_guide/user_guide.html#_using_infinispan_as_a_spring_cache_provider) * [ElastiCache (Memcached)](https://github.com/spring-cloud/spring-cloud-aws/blob/master/spring-cloud-aws-context/src/main/java/org/springframework/cloud/aws/cache/memcached/SimpleSpringMemcached.java) (`Cache`の実装のみ) * [WebSphere eXtreme Scale](http://www-01.ibm.com/support/knowledgecenter/SSS8GR_2.1.0/com.ibm.websphere.datapower.xc.doc/txsspringprovide.html) (`Cache`の実装のみ) `JCacheManager`は`javax.cache.CacheManager`のアダプターです。したがって[JCache対応製品](https://jcp.org/aboutJava/communityprocess/implementations/jsr107/index.html)(Oracle Coherenceなど)はこのアダプターを通じてSpringのキャッシュ機構から利用可能です。 そのほか、[CompositeCacheManager](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/support/CompositeCacheManager.html)は複数の`CacheManager`を混合することができますし、[AbstractTransactionSupportingCacheManager](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.html)実装クラスは`@Transactional`で(対応していれば)トランザクション管理が可能です。 クラス図貼っておきます。 * `CacheManger` ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/bbfedfeb-d066-4aba-fc68-4c95bf3a2f60.png) * `Cache` ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/c576f4f0-e0c3-526e-7540-46750bd4367f.png) ### AOPによる透過的キャッシュ AOPを使うことで、先の例のように明示的に`Cache`を使うことなくキャッシュを利用することができます。 `@Cacheable`アノテーションをつけたメソッドの返り値を自動で`Cache`に登録できます。 まずはキャッシュを使わない例を紹介します。外部Webサービス(Open Weather Map API)にアクセスするプログラムです。 ``` java package demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.util.Map; @SpringBootApplication public class DemoApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } // Bean定義 @Bean RestTemplate restTemplate() { return new RestTemplate(); } // ここから主プログラム @Autowired WeatherService weatherService; @Override public void run(String... strings) throws Exception { perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); perfMon(() -> System.out.println(weatherService.getWeather("Osaka"))); perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); } void perfMon(Runnable runnable) { long start = System.currentTimeMillis(); runnable.run(); long elapsed = System.currentTimeMillis() - start; System.out.println("took " + elapsed + " [ms]"); } } @Service class WeatherService { @Autowired RestTemplate restTemplate; @SuppressWarnings("unchecked") public String getWeather(String where) { Map result = (Map) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class); Double temperature = (Double) ((Map) result.get("main")).get("temp") - 273; Double wind = (Double) ((Map) result.get("wind")).get("speed") * 3.6; return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")"; } } ``` 実行すると、 ``` The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:23:53.125) took 374 [ms] The current temperature 11.12299999999999 degrees and the wind is 7.02 km/h. (2015-05-17T04:23:53.209) took 82 [ms] The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:23:53.293) took 84 [ms] ``` と出力されます。WebサービスへHTTPアクセスしているので遅いです。 天気の情報はそう頻繁に変わらないので、同じ場所の結果はキャッシュさせるようにしましょう。ここで`@Cacheable`の登場です。 ``` package demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Map; @SpringBootApplication @EnableCaching // AOPによるキャッシュアクセスを有効にする public class DemoApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } // Bean定義 @Bean // 使用するCacheManagerの定義 CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("weather"))); return cacheManager; } @Bean RestTemplate restTemplate() { return new RestTemplate(); } // ここから主プログラム @Autowired WeatherService weatherService; @Override public void run(String... strings) throws Exception { perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); perfMon(() -> System.out.println(weatherService.getWeather("Osaka"))); perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); } void perfMon(Runnable runnable) { long start = System.currentTimeMillis(); runnable.run(); long elapsed = System.currentTimeMillis() - start; System.out.println("took " + elapsed + " [ms]"); } } @CacheConfig(cacheNames = "weather") // キャッシュ名を指定 @Service class WeatherService { @Autowired RestTemplate restTemplate; @Cacheable // キャッシュさせたいメソッドにつける @SuppressWarnings("unchecked") public String getWeather(String where) { Map result = (Map) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class); Double temperature = (Double) ((Map) result.get("main")).get("temp") - 273; Double wind = (Double) ((Map) result.get("wind")).get("speed") * 3.6; return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")"; } } ``` 実行してみます。 ``` The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:30:24.011) took 344 [ms] The current temperature 11.12299999999999 degrees and the wind is 7.02 km/h. (2015-05-17T04:30:24.346) took 334 [ms] The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:30:24.011) took 1 [ms] ``` 2回目のTokyoの結果が速くなっているのがわかります。また、1回目と同じ時刻になっているので、初回の結果がキャッシュされていることもわかります。 既存の処理を簡単に、透過的に高速化できることが実感できたのではないでしょうか。 ちなみに、`@CacheConfig`を使わなくても`@Cacheable("weather")`というように一つ一つのメソッド毎にキャッシュ名を指定することも可能です。 デフォルトではキャッシュのキーは引数のオブジェクトです。複数ある場合は`java.util.Arrays#deepHashCode`の結果が使用されます。キャッシュのキーはSpEL式を使って柔軟に表現することができます。詳細は[マニュアル](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html#cache-annotations-cacheable-key)を参照してください。 キャッシュのサイズや生存期間を指定するAPIはSpringには用意されておらず、キャッシュ製品依存になります。 ここでは`ConcurrentHash`並みに簡単に使えるGoogle Guavaのキャッシュ機構を利用した例を紹介します。 ``` java @Bean CacheManager cacheManager() { GuavaCacheManager cacheManager = new GuavaCacheManager("weather", "..."); cacheManager.setCacheBuilder(CacheBuilder.newBuilder() .maximumSize(1000) // 最大1000件キャッシュ .expireAfterAccess(1, TimeUnit.SECONDS) // 最後のアクセスから1秒後に破棄 .removalListener(e -> System.out.println("==> " + e.getKey() + " has been removed!"))); return cacheManager; } ``` この設定を使い、APIにアクセスするプログラムを以下のように修正します。 ``` @Override public void run(String... strings) throws Exception { perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); perfMon(() -> System.out.println(weatherService.getWeather("Osaka"))); perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); TimeUnit.SECONDS.sleep(1); perfMon(() -> System.out.println(weatherService.getWeather("Tokyo"))); } ``` 結果は以下のようになります。 ``` The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:56:03.441) took 492 [ms] The current temperature 11.478000000000009 degrees and the wind is 4.716 km/h. (2015-05-17T04:56:03.608) took 161 [ms] The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:56:03.441) took 1 [ms] ==> Tokyo has been removed! The current temperature 16.12299999999999 degrees and the wind is 22.32 km/h. (2015-05-17T04:56:04.706) took 91 [ms] ``` 最後のアクセス時にはキャッシュがなくなっているので再度Web APIにアクセスしているのがわかります。 明示的なキャッシュの更新、破棄は`@CachePut`、`@CacheEvict`を使えます。 ``` java // import org.springframework.cache.annotation.*; @CacheConfig(cacheNames = "weather") @Service class WeatherService { @Autowired RestTemplate restTemplate; @Cacheable @SuppressWarnings("unchecked") public String getWeather(String where) { Map result = (Map) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class); Double temperature = (Double) ((Map) result.get("main")).get("temp") - 273; Double wind = (Double) ((Map) result.get("wind")).get("speed") * 3.6; return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")"; } @CachePut public String update(String where) { return getWeather(where); } @CacheEvict public String refresh(String where) { return getWeather(where); } @CacheEvict(allEntries = true) public void clear() { } } ``` アクセス頻度の高いメソッドは積極的にこの機能の利用を検討してもよいでしょう。特に更新頻度が低い場合に有効です。 一件取得処理なんかはほとんど`@Cachebale`つけてもよいと思います。 ### JCache(JSR-107)アノテーションのサポート Springでは`org.springframework.cache.annotation.Cacheable`の代わりに、Java EE 8から導入されるJCacheのアノテーション(`@javax.cache.annotation.CacheResult`など)も使えます。Springでは`javax.cache.CacheManager`の実装クラスは存在する必要はありません。 この機能を有効にするには`org.springframework:spring-context-support`を依存関係に追加する必要があります。 先の`WeatherService`の例をJCacheアノテーションを使うと以下のようになります。 ``` java // import javax.cache.annotation.*; @CacheDefaults(cacheName = "weather") @Service class WeatherService { @Autowired RestTemplate restTemplate; @CacheResult @SuppressWarnings("unchecked") public String getWeather(String where) { Map result = (Map) restTemplate.getForObject("http://api.openweathermap.org/data/2.5/weather?q=" + where, Map.class); Double temperature = (Double) ((Map) result.get("main")).get("temp") - 273; Double wind = (Double) ((Map) result.get("wind")).get("speed") * 3.6; return "The current temperature " + temperature + " degrees and the wind is " + wind + " km/h. (" + LocalDateTime.now() + ")"; } // @CachePut // // 引数から更新値を@CacheValueで指定する必要があるので先の例を表現できない // public String update(String where) { // return getWeather(where); // } @CacheRemove public String refresh(String where) { return getWeather(where); } @CacheRemoveAll public void clear() { } } ``` JCacheアノテーションはSpringのものとほとんど同じように使えることがわかります。比較表は[マニュアル](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html#cache-jsr-107-summary)に載っています。 `@javax.cache.annotation.CachePut`と`@org.springframework.cache.annotation.CachePut`の仕様に少し違いがあるのが注意です。 あとはSpringの方がキーの指定のSpELが使える分、柔軟かなと思います。 ---------- Springのキャッシュ機構、かなり便利なのでアプリケーションの高速化、DBアクセス負荷低減のために積極的に検討しましょう! **追記** Spring Boot 1.3からAutoConfiguration対応するらしい。