CLOVER🍀

That was when it all began.

ローカルキャッシュにCaffeineでも

この記事は、「Java Advent Calendar 2016 - Qiita」の4日目の記事となります。
昨日は、@susumuisさんの「Javaが僕にくれたもの | susumuis Info」でした。
明日は、@fukushiwさんのご担当となります。

ローカルキャッシュ、今ならなにを使うでしょう?

Javaのキャッシュライブラリといえば、なにを使うでしょうか?OSSものを中心に考えると、次の2つあたりが浮かぶのではないかなぁと思います。

なお、今回は分散キャッシュは考えないことにします。あくまで、ローカルキャッシュを対象に。

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の代わりになるんでしょうか?

Roadmap Future

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を参照してください。

Refresh · ben-manes/caffeine Wiki · GitHub

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のインターフェースに対する実装こそ提供するものの、機能的に互換であるわけではありません。

互換性についてはこちらを参照してください。

Guava / API compatibility

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さんのご担当です。よろしくお願いします!