CLOVER🍀

That was when it all began.

APIが刷新されたEhcache 3.0(M1)を試してみる

2015/3/24に、Ehcache 3.0のM1がリリースされました。

Ehcache 3.0.0.m1 release
https://github.com/ehcache/ehcache3/releases/tag/v3.0.0.m1

2014/12/12に、アルファ版がリリースされていたのですが、この時点では特に触れず。今回M1となったので、そろそろ試してみるかなぁ〜ということで。

そもそもEhcache 3って?

Ehcache自体は、Javaのキャッシュライブラリとしてはかなり有名な部類に入ると思いますが、今時点で広く使われているのはEhcache 2.Xです。

Ehcache
http://ehcache.org/

この2系とは別に開発が進められているのが、Ehcache 3.0です。

Starting with Ehcache 3.0
http://codespot.net/2014/02/26/starting-ehcache-3/

GitHubでは、こちら。

The Ehcache 3.x line is currently the development line.
https://github.com/ehcache/ehcache3

M1がリリースされてから、Webサイトもできたようです。

Ehcache - Caching for the JVM
http://ehcache.github.io/

Ehcache 3が作られているのは、こういう理由から?

  • Ehcacheはリリースから10年ほど経過しており(Java 5リリース頃)、APIの互換性を壊さず開発してきた
  • Javaの標準キャッシュAPI、JSR-107(JCache)が策定された
  • Ehcache 3.0では、JSR-107に基づいた新しいAPIとするべきだ(また、JSR-107を拡張する)

要は、リリースから10年経ってAPIの互換性を維持し続けるのも厳しいし、JSR-107も出たのでAPIをJSR-107をベースに刷新しましょうってことでしょうか。

Ehcache 2.Xまでと、何が違う?

JSR-107(JCache)のAPIに基づいて新しくすると言っているくらいなので、けっこう変わります。ざっと見ただけでも

  • パッケージ名の変更 net.sf.ehcache → org.ehcache(Maven ArtifactのgroupIdも変わります)
  • APIと実装などでモジュール分割が行われている
  • CacheインターフェースのAPIが、JSR-107をかなり意識した形になった
  • Cacheインターフェースがタイプセーフになった
  • JSR-107の実装が、最初から含まれている
  • 設定ファイルの書き方も大幅変更
  • キャッシュストアとして、Off-Heapが最初から使える
  • ビルドシステムはGradleに(ユーザーとして使う分には関係ないですが…)

などがあり、Ehcache 2系とはAPIに互換性のない、もはや別物と言ってよいものになっています。

ようやく、Cacheインターフェースがジェネリクスに対応しました!それにEhcacheを使うとよく出てきていた、あのElementというクラスもいなくなってしまいましたね。JCacheへの対応も、2系の時は別モジュールとして存在しておりEhcache本体の最新版に全然追従しないなど、あまりやる気が感じられませんでしたが(https://github.com/ehcache/ehcache-jcache)、今回は本体に最初から含まれているので今後は状況が変わるでしょう。クラス名とかの至るところに「107」と書かれているのには、ちょっと引っかかりますが…。

あと、Off-Heapがいきなり使えるところが特徴ですが、M1のリリースはこちらを含めるところまでだったみたいです。

Terracotta Strengthens Ehcache3 with Off-heap Storage
http://blog.terracotta.org/2015/03/23/terracotta-strengthens-ehcache3-with-off-heap-storage/

とはいえ、2系の頃から比べるとまだまだ機能不足で、徐々に実装していくみたいです。

などなど。個人的には、キャッシュソリューションとしてどこまで要るのかなぁという気がしないでもないですが…。

使ってみる

では、Ehcache 3.0 M1を使ってみます。まだM1なので今後APIの変更は十分に入りうるという点には、ご注意ください。ドキュメントもまだまだ少ない状態です。

参考にしたのは、こちらのドキュメントとテストコードです。

Getting started with the new API
http://ehcache.github.io/docs/user/3.0/

で、今回書くコードですが、

  • Java APIでCache定義
  • XML設定ファイルでCache定義

の2つのアプローチで行います。

今回のエントリでは、JSR-107の実装についてはひとまずスルーします。

Maven依存関係

Ehcache 3.0を使用するにあたり、Maven依存関係の定義を行います。

    <dependency>
      <groupId>org.ehcache</groupId>
      <artifactId>ehcache</artifactId>
      <version>3.0.0.m1</version>
    </dependency>

Ehcache 3.0はいくつかのモジュールで構成されているのですが、依存関係として上記を含めると全部入ります。

例外として、JSR-107の実装として使う時は、別途JSR-107のAPI(javax.cache:cache-api:1.0.0)を依存関係に含める必要があります。

サンプルコードでは、この他にJUnitとAssertJを使用します。

Java APIでCacheを定義して使用する(基本的な使い方)

まずは、Ehcache 3.0のCacheをJava APIで定義して使ってみます。ここからのサンプルコードは、以下のimport文が定義済みとします。

import static org.assertj.core.api.Assertions.*;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.CacheManagerBuilder;
import org.ehcache.config.CacheConfigurationBuilder;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;

import org.assertj.core.data.MapEntry;
import org.junit.Test;

見ておくのは、主に「org.ehcache」の部分ですからね。

CacheManagerの取得と、Cacheの定義。

        CacheManager cacheManager =
            CacheManagerBuilder
            .newCacheManagerBuilder()
            .withCache("myCache",
                       CacheConfigurationBuilder.newCacheConfigurationBuilder().buildConfig(Integer.class, String.class))
            .build();

ここでは、CacheManagerを生成すると共に、「myCache」という名前のキー、値の型がIntegerとStringのCacheを定義しています。

この定義したCacheは、CacheManagerから取得することができます。

        Cache<Integer, String> cache =
            cacheManager.getCache("myCache", Integer.class, String.class);

Cacheに、エントリを放り込んでみましょう。Cache#putでOKです。

        IntStream.rangeClosed(1, 10).forEach(i -> cache.put(i, "value" + i));

getでCacheからの取得。

        assertThat(cache.get(1))
            .isEqualTo("value1");
        assertThat(cache.get(2))
            .isEqualTo("value2");

removeで、エントリの削除です。削除後は、nullが返ります。

        cache.remove(3);
        assertThat(cache.get(3))
            .isNull();

このように、Mapによく似たAPIですが、Cache#sizeやCache#countといったエントリ数をカウントするAPIはありません。代わりに、CacheがIterableなのでforなどで反復させることはできます。

        int size = 0;
        for (Cache.Entry<Integer, String> entry : cache) {
            size++;
        }
        assertThat(size).isEqualTo(9);

このあたり、JSR-107と一緒ですね。

あと、キーに紐づく値があるかどうかを確認するCache#containsKey、

        assertThat(cache.containsKey(5))
            .isTrue();

指定のキーの集合からCache.Entryを一気に取得するCache#getAll、

        Set<Integer> keySet = new HashSet<>(Arrays.asList(6, 7, 8));
        assertThat(cache.getAll(keySet))
            .containsOnly(MapEntry.entry(6, "value6"),
                          MapEntry.entry(7, "value7"),
                          MapEntry.entry(8, "value8"));

Cacheにエントリを一括登録するCache#putAll、

        Map<Integer, String> data = new HashMap<>();
        data.put(11, "value11");
        data.put(12, "value12");

        cache.putAll(data);

        assertThat(cache.get(11))
            .isEqualTo("value11");

replaceやputIfAbsentも備えます。

        assertThat(cache.replace(9, "value9", "new-value9"))
            .isTrue();
        assertThat(cache.get(9))
            .isEqualTo("new-value9");

        assertThat(cache.putIfAbsent(10, "new-value10"))
            .isEqualTo("value10");
        assertThat(cache.get(10))
            .isEqualTo("value10");

Cache#clearで、エントリの全クリアです。

        cache.clear();

        StreamSupport
            .stream(cache.spliterator(), false)
            .forEach(entry -> System.out.printf("key = %d, value = %s%n", entry.getKey(), entry.getValue()));

CacheManagerからCacheを削除するには、CacheManager#removeCacheで。

        cacheManager.removeCache("myCache");

最後に、CacheManagerをcloseしましょう。

        cacheManager.close();

こんな形で使うのですが、CacheManagerはCloseableではありません。

Java APIでCacheを定義して使用する(有効期限の設定)

今度は、有効期限付きのCacheを定義してみましょう。

CacheManagerを使用して、Cacheをこのように組みます。

        CacheManager cacheManager =
            CacheManagerBuilder
            .newCacheManagerBuilder()
            .build();

        Cache<Integer, String> cache =
            cacheManager
            .createCache("myCache",
                         CacheConfigurationBuilder
                         .newCacheConfigurationBuilder()
                         .withExpiry(Expirations.timeToLiveExpiration(new Duration(10, TimeUnit.SECONDS)))
                         .withExpiry(Expirations.timeToIdleExpiration(new Duration(5, TimeUnit.SECONDS)))
                         .buildConfig(Integer.class, String.class));

今回は、TTLを10秒、アイドル時間による有効期限を5秒としました。

確認。

        IntStream.rangeClosed(1, 10).forEach(i -> cache.put(i, "value" + i));

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get(3))
            .isEqualTo("value3");
        assertThat(cache.get(4))
            .isEqualTo("value4");

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get(3))
            .isEqualTo("value3");
        assertThat(cache.get(4))
            .isEqualTo("value4");

        assertThat(cache.get(5))
            .isNull();
        assertThat(cache.get(6))
            .isNull();

        TimeUnit.SECONDS.sleep(5);

        assertThat(cache.get(3))
            .isNull();
        assertThat(cache.get(4))
            .isNull();

アイドル時間を5秒と設定しているので、5秒以内にアクセスしないエントリはCacheからなくなります。また、10秒経過するとアイドル時間に関係なくCacheから取得できなくなります。

最後にclose。

        cacheManager.close();

XML設定ファイルでCacheを定義して使用する

続いて、XML設定ファイルでCacheを定義します。

XML Configuration
http://ehcache.github.io/docs/user/3.0/xml.html

今回は、このような設定ファイルを用意しました。
src/test/resources/ehcache-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache:config
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xmlns:ehcache='http://www.ehcache.org/v3'>
  <ehcache:cache alias="offHeapCache">
    <ehcache:key-type>java.lang.Integer</ehcache:key-type>
    <ehcache:value-type>java.lang.String</ehcache:value-type>
    <ehcache:resources>
      <ehcache:heap size="5" unit="entries"/>
      <ehcache:offheap size="50" unit="mb"/>
    </ehcache:resources>
  </ehcache:cache>

  <ehcache:cache-template name="expiryTtiTtlTemplate">
    <ehcache:key-type>java.lang.Integer</ehcache:key-type>
    <ehcache:value-type>java.lang.String</ehcache:value-type>
    <ehcache:expiry>
      <ehcache:tti unit="seconds">5</ehcache:tti>
    </ehcache:expiry>
    <!--
    <ehcache:expiry>
      <ehcache:ttl unit="seconds">10</ehcache:ttl>
    </ehcache:expiry>
    -->
    <ehcache:resources>
      <ehcache:heap size="100" unit="entries"/>
    </ehcache:resources>
  </ehcache:cache-template>

  <ehcache:cache alias="withExpiryCache" usesTemplate="expiryTtiTtlTemplate">
  </ehcache:cache>
</ehcache:config>

定義しているCacheは2つで、Off-Heapを使用したものと有効期限を(テンプレート使用して)設定したものです。

ちなみに、Ehcacheに限らずCacheの設定は、(XMLでなくてもいいですが)設定ファイルで書く方が好みです。Java APIを使ってBuilder形式で組み立てていくのもそれなりに便利だと思うのですが(特にテスト時とか)、あんまり複雑なCacheをBuilder APIで組むと、だんだん何やっているかわからなくなる感がありまして…。後で見直すことなどを考えると、設定ファイル化した方が読みやすいかなーと思います。

完全に、個人の主観ですが。話が逸れましたね、戻りましょう。

このXML設定ファイルを使ったテストコードでは、以下のimport文があることを前提とします。

import static org.assertj.core.api.Assertions.*;

import java.io.IOException;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import java.util.stream.*;
import org.xml.sax.SAXException;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.CacheManagerBuilder;
import org.ehcache.config.Configuration;
import org.ehcache.config.xml.XmlConfiguration;

import org.junit.Test;

それでは、最初はOff-Heapの方を使ってみます。定義は、こんな感じで作りました。cache要素のalias属性がCache名、key-type、value-type要素でキーと値の型を表すようです。

  <ehcache:cache alias="offHeapCache">
    <ehcache:key-type>java.lang.Integer</ehcache:key-type>
    <ehcache:value-type>java.lang.String</ehcache:value-type>
    <ehcache:resources>
      <ehcache:heap size="5" unit="entries"/>
      <ehcache:offheap size="50" unit="mb"/>
    </ehcache:resources>
  </ehcache:cache>

Off-Heapの部分は、CacheStore扱いのようなので、メインとなるHeapに持つ設定がなかった場合はCacheの構成がNGとされます。

なお、Off-Heapを使う場合には、-XX:MaxDirectMemorySizeの設定を忘れないようにね!だそうで。

XML設定ファイルを使用してCacheManagerとCacheを取得するコードは、このようになります。

        URL configUrl = Thread.currentThread().getContextClassLoader().getResource("ehcache-config.xml");
        Configuration config =
            new XmlConfiguration(configUrl);

        CacheManager cacheManager = CacheManagerBuilder.newCacheManager(config);

        Cache<Integer, String> cache =
            cacheManager.getCache("offHeapCache", Integer.class, String.class);

Cacheは事前定義済みなので、いきなりCacheManager#getCacheでOKです。

あとは、普通に使います。

        IntStream.rangeClosed(1, 10).forEach(i -> cache.put(i, "value" + i));

        assertThat(cache.get(1))
            .isEqualTo("value1");
        assertThat(cache.get(10))
            .isEqualTo("value10");

        int size = 0;
        for (Cache.Entry<Integer, String> entry : cache) {
            size++;
        }
        assertThat(size).isEqualTo(10);

closeを忘れずに。

        cacheManager.close();

って、これだとOff-Heapが使われていることがわかりませんけどね…。

最後に、有効期限を設定したCacheを定義して使ってみます。XML定義は、このようにしました。

  <ehcache:cache-template name="expiryTtiTtlTemplate">
    <ehcache:key-type>java.lang.Integer</ehcache:key-type>
    <ehcache:value-type>java.lang.String</ehcache:value-type>
    <ehcache:expiry>
      <ehcache:tti unit="seconds">5</ehcache:tti>
    </ehcache:expiry>
    <!--
    <ehcache:expiry>
      <ehcache:ttl unit="seconds">10</ehcache:ttl>
    </ehcache:expiry>
    -->
    <ehcache:resources>
      <ehcache:heap size="100" unit="entries"/>
    </ehcache:resources>
  </ehcache:cache-template>

  <ehcache:cache alias="withExpiryCache" usesTemplate="expiryTtiTtlTemplate">
  </ehcache:cache>

ここでは、Cacheのテンプレートを定義して、それを継承する形でCacheを定義しています(usesTemplate)。今回は、テンプレートの設定をそのまま引き継いでいますが、必要に応じてオーバーライドすることも可能なようです。

Webサイトのドキュメントでは、オーバーライドした例が載っています。

また、有効期限の設定は、

  • class(Expiryを自分で実装して設定)
  • tti(アイドル時間によるタイムアウト)
  • ttl(TTL)
  • none(有効期限なし)

の4つから選ぶのですが、ttiとttlを同時に設定できないような…。アイドル時間とTTLを同時に使いたい場合は、自分でExpiryを実装してねってことなのでしょうか…。それはどうなのでしょう…。

設定ファイルが特徴的なだけで、使い方自体はここまでと変わりません。

        URL configUrl = Thread.currentThread().getContextClassLoader().getResource("ehcache-config.xml");
        Configuration config =
            new XmlConfiguration(configUrl);

        CacheManager cacheManager = CacheManagerBuilder.newCacheManager(config);

        Cache<Integer, String> cache =
            cacheManager.getCache("withExpiryCache", Integer.class, String.class);

        IntStream.rangeClosed(1, 10).forEach(i -> cache.put(i, "value" + i));

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get(3))
            .isEqualTo("value3");
        assertThat(cache.get(4))
            .isEqualTo("value4");

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get(3))
            .isEqualTo("value3");
        assertThat(cache.get(4))
            .isEqualTo("value4");

        assertThat(cache.get(5))
            .isNull();
        assertThat(cache.get(6))
            .isNull();

        TimeUnit.SECONDS.sleep(6);

        int size = 0;
        for (Cache.Entry<Integer, String> entry : cache) {
            size++;
        }
        assertThat(size).isZero();

        cacheManager.close();

まとめ

まだM1がリリースされたばかりで絶賛開発中ですが、Ehcache 3.0をご紹介しました。JSR-107のAPIを意識して、だいぶ今風のCache APIとして生まれ変わった感じがしますね。

個人的には、Ehcacheのようなスタンドアロンで使うキャッシュライブラリは、高機能性よりもある程度シンプル・軽量な感じを追求してくれてもいいと思うのですが…。まあ、まだまだ全貌が見えていないので、今後も見ていきたいと思います。