この記事は、「Java Advent Calendar 2016 - Qiita」の4日目の記事となります。
昨日は、@susumuisさんの「Javaが僕にくれたもの | susumuis Info」でした。
明日は、@fukushiwさんのご担当となります。
ローカルキャッシュ、今ならなにを使うでしょう?
Javaのキャッシュライブラリといえば、なにを使うでしょうか?OSSものを中心に考えると、次の2つあたりが浮かぶのではないかなぁと思います。
- Ehcache ※2系が強いのかな?
- Google Guava
なお、今回は分散キャッシュは考えないことにします。あくまで、ローカルキャッシュを対象に。
Ehcacheについては、すでにAPIが刷新された3系がリリースされており、現時点で3.1が利用できます。…あんまり名前を聞きませんけれど。
ここで、対象として挙げたいのが、今回紹介するCaffeineです。
GitHub - ben-manes/caffeine: A high performance caching library for Java 8
Caffeineとは?
Java 8で書かれたキャッシュライブラリで、Google GuavaのCacheにインスパイアされたものだとGitHubには書かれています。実際、APIもよく似た感じになっています。
またhigh performanceを謳っており、他のキャッシュライブラリより良い結果が出ているようです。
Benchmarks · ben-manes/caffeine Wiki · GitHub
このベンチマークだけを鵜呑みにするのも…という気はしますが、けっこう機能的に重厚なEhcacheよりもGoogle Guava Cacheの方が軽いという傾向があり、ただGoogle Guavaだと導入するとCache以外にもその他いろいろ付いてくるという点から、このCaffeineはちょうどいいライブラリだなぁと思ったりしています。
Roadmapを見ると、将来的にはGoogle Guava Cacheの代わりになるんでしょうか?
GitHub上でもStarがEhcache 3を越えており、すでにいくつかのライブラリなどで採用されているキャッシュライブラリです。
Springでは、4.3から採用されています。
[SPR-13690] Caffeine caching support - Spring JIRA
そしてSpringのGoogle Guava Cacheのサポートは、Spring 5.0で削除されます。
[SPR-13797] Drop Guava caching - superseded by Caffeine - Spring JIRA
というわけでJavaでキャッシュライブラリを選ぶ際の選択肢としては、知っておいてもいいのではないかなぁと。
日本では、あんまり名前を聞かない気もしますけどね?
で、個人的にはこのエントリで初めてCaffeineを試すのですが、今回は以下のテーマでCaffeineを扱ってみたいと思います。
- とりあえず使ってみる
- Google Guavaの代替としてのCaffeine
- JCache ProviderとしてのCaffeine
- Spring CacheでのCaffeine
始める前に
ここから先は、Caffeineを使ったJavaのコードを書いていきます。
主にテストコードで動作確認をしますが、Spring Cacheのパートを除き、以下のMaven依存関係がpom.xmlに定義されているものとしてください。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.5.2</version> <scope>test</scope> </dependency>
JUnitとAssertJを使用します。
利用するCaffeineのバージョンは、明示的に指定する場合は2.3.5とします。
Caffeineのドキュメントについては、Wikiを参照することになります。
Home · ben-manes/caffeine Wiki · GitHub
また、ところどころsleepしたり時間を計測したりするコードが出てきますが、以下のメソッドが定義されているものとしてください。
void sleep(long sleepSec) { try { TimeUnit.SECONDS.sleep(sleepSec); } catch (InterruptedException e) { // ignore } } // stop-watch long sw(Runnable runnable) { long startTime = System.nanoTime(); runnable.run(); long elapsedTime = System.nanoTime() - startTime; return TimeUnit.SECONDS.convert(elapsedTime, TimeUnit.NANOSECONDS); }
とりあえず使ってみる
では、まずはCaffeineを使ってみましょう。
ドキュメントは、今回はこのあたりを参考に。
Population · ben-manes/caffeine Wiki · GitHub
Removal · ben-manes/caffeine Wiki · GitHub
Eviction · ben-manes/caffeine Wiki · GitHub
準備
Caffeineを使うためには、最低限Maven依存関係にこちらがあればOKです。
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.3.5</version> </dependency>
今回利用する、Caffeineのバージョンは2.3.5です。
また、以下のimport文が定義済みとします。
import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.IntStream; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat;
初めてのCaffeine
まずはCacheを作成します。Caffeine#newBuilderから、Cacheを作成することができます。今回は、特に何も設定せずCacheを作成しました。
// キャッシュの作成
Cache<String, String> cache =
Caffeine
.newBuilder()
.build();
Cacheに対して、データの登録と取得。
// 登録 cache.put("key1", "value1"); cache.put("key2", "value2"); // 取得 assertThat(cache.getIfPresent("key1")) .isEqualTo("value1"); assertThat(cache.getIfPresent("key2")) .isEqualTo("value2");
エントリ数の取得。
// エントリ数 assertThat(cache.estimatedSize()) .isEqualTo(2L);
登録していないキーに対してはnullが返りますが、Functionを実行して値を決めることもできます。
// 登録していないキーに対しては、nullが返る assertThat(cache.getIfPresent("missing-key")) .isNull(); // 登録していないキーに対する呼び出しに対して、Functionを実行することもできる assertThat(cache.get("key3", key -> "value" + key.replace("key", ""))) .isEqualTo("value3");
とてもGoogle Guava Cacheっぽいですね。
遅いFunctionを登録して値をロードすると、1回目は低速ですが2回目はCacheに乗るので高速になったり。
// 遅いFunction Function<String, String> slowEntryLoader = key -> { sleep(3L); return "value" + key.replace("key", ""); }; // 1回目は低速 assertThat( sw(() -> assertThat(cache.get("key4", slowEntryLoader)).isEqualTo("value4")) ).isGreaterThanOrEqualTo(3L); // 2回目は高速 assertThat( sw(() -> assertThat(cache.get("key4", slowEntryLoader)).isEqualTo("value4")) ).isLessThan(1L);
エントリの削除。
// エントリの削除 cache.invalidate("key1"); assertThat(cache.getIfPresent("key1")) .isNull(); // エントリの全削除 cache.invalidateAll(); assertThat(cache.estimatedSize()) .isZero();
とまあ、ふつうに使うならこんな感じです。
LoadingCache
Google Guavaのように、LoadingCacheを作成することもできます。あらかじめCacheの構築時にCacheLoaderを仕込んでおき、キーに対応するエントリがなかった時に、CacheLoaderを実行することができます。
Caffeine#newBuilderを呼び出し後、buildメソッド呼び出し時にCacheLoaderを指定すると、LoadingCacheを作成することができます。
// 低速なエントリロード用のCacheLoader CacheLoader<String, String> slowLoader = key -> { sleep(3L); return "value" + key.replace("key", ""); }; // CacheLoaderを使用して、LoadingCacheを作成 LoadingCache<String, String> cache = Caffeine .newBuilder() .build(slowLoader); // 登録していないキーに対しても、いきなり値を取得できる assertThat(cache.get("key1")) .isEqualTo("value1"); // 呼び出しは、1回目は低速(CacheLoaderが低速なので) assertThat( sw(() -> assertThat(cache.get("key2"))) ).isGreaterThanOrEqualTo(3L); // 2回目は、ロード済みなので高速 assertThat( sw(() -> assertThat(cache.get("key2"))) ).isLessThan(1L);
Cacheの設定をしてみる
続いて、Cacheの設定をしてみましょう。Caffeine#newBuilderを呼び出してから、buildするまでの間にCacheの設定をすることができます。
今回は、アクセスしてから5秒後に有効期限切れするCacheを作成してみます。
// アクセス後の有効期限を5秒に設定したCacheを作成 Cache<String, String> cache = Caffeine .newBuilder() .expireAfterAccess(5L, TimeUnit.SECONDS) .build(); // エントリを2つ登録 cache.put("key1", "value1"); cache.put("key2", "value2"); sleep(3L); // 3秒後、片方にアクセス cache.getIfPresent("key1"); sleep(3L); // 3秒後の時点でアクセスした方は、エントリが残っている assertThat(cache.getIfPresent("key1")) .isEqualTo("value1"); // アクセスしなかった方は、エントリが消えている assertThat(cache.getIfPresent("key2")) .isNull(); sleep(5L); // 5秒後、すべてのエントリがクリアされている // ※Cache#estimatedSizeはexpireをすぐには反映しないので、Cache#cleanUpを呼び出している cache.cleanUp(); assertThat(cache.estimatedSize()) .isZero();
Cacheの設定を文字列で行う
Cacheの設定を文字列で指定することもできます。
Cache<String, String> cache = Caffeine.from("maximumSize=10,expireAfterAccess=5s").build(); IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i)); cache.cleanUp(); assertThat(cache.estimatedSize()) .isEqualTo(10L); cache.invalidateAll(); cache.put("key1", "value1"); cache.put("key2", "value2"); sleep(3L); cache.getIfPresent("key1"); sleep(3L); assertThat(cache.getIfPresent("key1")) .isEqualTo("value1"); assertThat(cache.getIfPresent("key2")) .isNull();
設定にどういったものが指定できるかは、こちらを参照するとよいでしょう。
https://github.com/ben-manes/caffeine/blob/v2.3.5/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java#L173
今回はこのくらいの機能の紹介としますが、他にもRefreshなどいろいろあるので、気になる方はWikiを参照してください。
Google Guavaの代替としてのCaffeine
Caffeineには、Google Guava Cacheのインターフェースの実装を提供するモジュールがあります。
Guava · ben-manes/caffeine Wiki · GitHub
準備
Google Guava Cacheのインターフェースの実装を使う場合に、必要な依存関係はこちら。
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>guava</artifactId>
<version>2.3.5</version>
</dependency>
使ってみる
ここまでしてCaffeineを使うケースは、あまりないような気もするので、さらっとだけ書きます。
今回は、LoadingCache(Google Guava Cache)インターフェースの実装を使ってみましょう。
CacheLoader<String, String> loader = key -> { sleep(3L); return "value" + key.replace("key", ""); }; // Caffeineから、Guava Cacheを作成することができる com.google.common.cache.LoadingCache<String, String> cache = CaffeinatedGuava.build( Caffeine .newBuilder() .expireAfterWrite(5L, TimeUnit.SECONDS), loader );
CaffeinatedGuavaを使うことで、Google Guava Cacheのインターフェースを実装したCacheを利用することができます。そのインプットとなるのは、CaffeineのCacheの設定(Caffeineクラスの設定)なわけですが。
今回はCacheLoaderに遅い実装を入れたので、こんな感じで確認。
// 初回は低速 assertThat( sw(() -> { try { assertThat(cache.get("key1")).isEqualTo("value1"); } catch (ExecutionException e) { throw new RuntimeException(e); } }) ).isGreaterThanOrEqualTo(3L); // 2回目は高速 assertThat( sw(() -> { try { assertThat(cache.get("key1")).isEqualTo("value1"); } catch (ExecutionException e) { throw new RuntimeException(e); } }) ).isLessThan(1L); // 有効期限切れまで待つ sleep(5L); // 再度低速になる assertThat( sw(() -> { try { assertThat(cache.get("key1")).isEqualTo("value1"); } catch (ExecutionException e) { throw new RuntimeException(e); } }) ).isGreaterThanOrEqualTo(3L);
なお、Google Guava Cacheのインターフェースに対する実装こそ提供するものの、機能的に互換であるわけではありません。
互換性についてはこちらを参照してください。
JCache ProviderとしてのCaffeine
Caffeineは、JCacheの実装も提供しているようです。
JCache · ben-manes/caffeine Wiki · GitHub
Expireについては、JCacheの仕様と互換性がないようですが…。
準備
CaffeineのJCache用のモジュールを使用するには、以下の依存関係を定義します。
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>jcache</artifactId> <version>2.3.5</version> </dependency>
javax.cache:cache-apiについては、明示的には不要です。CaffeineのJCacheモジュールに、ScopeがCompileとして含まれています(JCacheの実装としては珍しいです)。
ここでは、以下のimport文が定義されていることを前提にします。
import java.util.Arrays; import java.util.HashSet; import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.Caching; import javax.cache.configuration.CompleteConfiguration; import javax.cache.configuration.Configuration; import javax.cache.configuration.MutableConfiguration; import javax.cache.spi.CachingProvider; import org.assertj.core.data.MapEntry; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat;
使ってみる
では、まずはJCacheを使ってみましょう。Cacheは、その場で定義します。
Configuration<String, String> configuration = new MutableConfiguration<String, String>() .setTypes(String.class, String.class); try (CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager(); Cache<String, String> cache = manager.createCache("caffeineCache", configuration)) { cache.put("key1", "value1"); assertThat(cache.get("key1")) .isEqualTo("value1"); }
至って普通です。
デフォルトキャッシュ
実は、CaffeineのJCacheモジュールでは、「default」という名前のCacheがいきなり使えるようになっています。
try (CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager(); // いきなり「default」という名前のCacheが使える Cache<String, String> defaultCache = manager.getCache("default")) { defaultCache.put("key1", "value1"); assertThat(defaultCache.get("key1")) .isEqualTo("value1"); }
これはどういうこと?というわけですが、CaffeineのJCacheモジュールは、内部的にTypesafe Configを使用しています。
GitHub - lightbend/config: configuration library for JVM languages using HOCON files
書き方や設定項目については、こちらを見るとよいです。
https://github.com/ben-manes/caffeine/blob/v2.3.5/jcache/src/main/resources/reference.conf
このreference.confが直接使われているわけではないようですが、以下のように「default」というCacheが定義されているのと同等になっているようなので、いきなり「default」というCacheが使えるようです。
caffeine.jcache { # A named cache is configured by nesting a new definition under the caffeine.jcache namespace. The # per-cache configuration is overlaid on top of the default configuration. default {
独自の設定をしてみる
で、CaffeineのJCacheモジュールを使ってCacheを定義するには、MutableConfigurationで定義する以外にTypesafe Configが使えることがわかりました。
ここで、application.confというファイル名で設定ファイルを追加すると、CaffeineのCacheManagerの実装が初期化時にロードしてくれます。
Typesafe Configのデフォルトのファイル名なので、読んでくれているだけなのですが…。というか、CaffeineのCacheManagerって、Propertiesを無視するのでそこは指定できてもよかったんじゃないかなぁと…。
では、application.confを作成してみます。今回は、Store-By-RefenceとRead Throughの設定をしてみました。
src/test/resources/application.conf
caffeine.jcache { definedCaffeineCache { store-by-value { enabled = true } read-through { enabled = true loader = "javaadventcalendar.MyCacheLoader" } } }
caffeine.jcacheの配下に
caffeine.jcache {
Cache名の要素を定義すればOKです。今回は、「definedCaffeineCache」という名前のCacheを定義しました。
definedCaffeineCache {
CacheLoaderも定義したので、こちらも作成しておきます。
src/test/java/javaadventcalendar/MyCacheLoader.java
package javaadventcalendar; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.cache.integration.CacheLoader; import javax.cache.integration.CacheLoaderException; public class MyCacheLoader implements CacheLoader<String, String> { @Override public String load(String key) throws CacheLoaderException { return "value" + key.replace("key", ""); } @Override public Map<String, String> loadAll(Iterable<? extends String> keys) throws CacheLoaderException { return StreamSupport .stream(keys.spliterator(), false) .collect(Collectors.toMap(key -> key, key -> load(key))); } }
適当…。
では、使ってみます。
try (CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager(); // application.confで定義したCache Cache<String, String> definedCache = manager.getCache("definedCaffeineCache")) { // CacheLoaderが適用されていることが確認できる definedCache.put("key1", "value1"); assertThat(definedCache.get("key1")) .isEqualTo("value1"); assertThat(definedCache.get("key2")) .isEqualTo("value2"); assertThat(definedCache.getAll(new HashSet<>(Arrays.asList("key3", "key4")))) .containsExactly(MapEntry.entry("key3", "value3"), MapEntry.entry("key4", "value4")); // application.confで設定した内容が確認できる CompleteConfiguration<String, String> definedConfiguration = definedCache.getConfiguration(CompleteConfiguration.class); assertThat(definedConfiguration.isStoreByValue()) .isTrue(); assertThat(definedConfiguration.isReadThrough()) .isTrue(); }
設定が適用されていることが、確認できましたね。
といわけで、JCache ProviderとしてのCaffeine、それから設定をTypesafe Configの設定ファイルで行ってみました、と。
CDI & Interceptor
JCacheにはCDIでのアノテーションとInterceptorについての仕様がありますが、Caffeineはこの実装を提供していません。
CDI
JCache RIのモジュールを使いなさい、と。
GitHub - jsr107/RI: Reference Implementation
まあ、多くのJCacheの実装はInterceptorの実装を提供していないので、そう不思議なことではないのですけれどね。
依存関係にこちらを追加した上で
<dependency> <groupId>org.jsr107.ri</groupId> <artifactId>cache-annotations-ri-cdi</artifactId> <version>1.0.0</version> </dependency>
beans.xmlにこのように設定すればよいでしょう。
beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="annotated"> <interceptors> <class>org.jsr107.ri.annotations.cdi.CacheResultInterceptor</class> <class>org.jsr107.ri.annotations.cdi.CachePutInterceptor</class> <class>org.jsr107.ri.annotations.cdi.CacheRemoveEntryInterceptor</class> <class>org.jsr107.ri.annotations.cdi.CacheRemoveAllInterceptor</class> </interceptors> </beans>
他のJCacheの実装で試した時のエントリを、貼っておきます。
JCacheのCDI連携を、GlassFish 4.1で動かす - CLOVER
Spring CacheでのCaffeine
最後は、Spring CacheにおけるCaffeineについてです。
最初にも少し触れましたが、Spring 4.3からSpring CacheでCaffeineをサポートしています。
Spring Cache
Caffeine Cache
また、Spring BootでのAutoConfigureの対象ともなっています。
Spring Boot
31. Caching
Caffeine
Appendix A. Common application properties
今回は、簡単にSpring Bootで使ってみます。
準備
使用するSpring Bootのバージョンは、1.4.2.RELEASEとします。
<properties> <java.version>1.8</java.version> <spring.boot.version>1.4.2.RELEASE</spring.boot.version> </properties>
Caffeineと「spring-boot-starter-cache」を依存関係に加えます。
<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>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
「spring-boot-starter-test」は、テストコード用です。
Cacheを適用するクラスを作成する
まずは、SpringのCacheを適用するクラスを作成します。@CacheConfigと@Cacheableアノテーションを、今回は使用しています。
src/main/java/javaadventcalendar/CalcService.java
package javaadventcalendar; import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @CacheConfig(cacheNames = "calcCache") @Service public class CalcService { @Cacheable public int add(int a, int b) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { // ignore } return a + b; } }
mainメソッドは今回使わないので中身は空ですが、@SpringBootApplicationアノテーションを付与したクラスも作成しておきます。
この時、@EnableCachingアノテーションも付与しておきます。
src/main/java/javaadventcalendar/App.java
package javaadventcalendar; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @EnableCaching public class App { }
Caffeineの設定をする
せっかくなので、Caffeineの設定もしてみます。
@Beanで、CaffeineCacheManagerを直接使って設定してもいいのですが
Caffeine Cache
今回は、Spring Bootのapplication.propertiesで設定することにしましょう。
Caffeine
Appendix A. Common application properties
で、作成したのがこちら。
src/test/resources/application.properties
spring.cache.cache-names=calcCache spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=10s
とりあえず、有効期限の設定を今回は使用します。Specの書き方は、Caffeineの設定を文字列で行った時と同じです。
よって、今回はJava側に直接Caffeineに関するコードや設定は出てきません。
テストコードを書く
では、最後にテストコードを書きます。
テストクラスの宣言と、import文はこんな感じ。
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.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(classes = App.class) public class CaffeineSpringCacheTest { @Autowired CalcService calcService;
先ほど宣言したServiceを、@Autowiredでインジェクションします。
テストコード。
@Test public void gettingStarted() { // 1回目は低速 assertThat( sw(() -> assertThat(calcService.add(1, 2)).isEqualTo(3)) ).isGreaterThanOrEqualTo(5L); // 2回目は高速 assertThat( sw(() -> assertThat(calcService.add(1, 2)).isEqualTo(3)) ).isLessThan(1L); // 有効期限切れを待つ sleep(10L); // 低速に戻る assertThat( sw(() -> assertThat(calcService.add(1, 2)).isEqualTo(3)) ).isGreaterThanOrEqualTo(5L); }
設定が効いていることが、確認できました、と。
まとめ
今回は、JavaのキャッシュライブラリであるCaffeineを紹介してみました。ローカルキャッシュではありますが、Google Guava CacheやEhcacheの代替として十分選択肢に挙がるのではないでしょうか?
気になる方に、少しでもこのエントリがお役に立てば幸いです。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/java-advent-calendar/tree/master/2016
明日は、@fukushiwさんのご担当です。よろしくお願いします!