CLOVER🍀

That was when it all began.

JCache 1.0に対応した、EhcacheのJCacheモジュールを使ってみる

今年の5月に、JSR 107(JCache)の1.0がリリースされました。

JSR 107
https://jcp.org/en/jsr/detail?id=107

*今度、JSRちゃんと読み直さないと…。

これに対応した実装が出てくるのを待っていたのですが、Ehcacheの対応モジュールがMaven Centralにアップされていたので、遊んでみました。

Ehcache-JCache
https://github.com/ehcache/ehcache-jcache

なお、Ehcache本家サイトの扱いは、けっこう寂しいものです…。

JSR107 Support
http://ehcache.org/documentation/integrations/jsr107

1.0付けてMaven Centralにアップされているのだから、正式版と思いたいのですがまだ違うのかなぁ…?

まあ、何はともあれ試してみましょう。

準備

まずは、Getting Startedを見たりMaven Centralを見ながらMaven依存関係を設定。

Getting Started
https://github.com/ehcache/ehcache-jcache/tree/master/ehcache-jcache

Maven依存関係。

    <dependency>
      <groupId>org.ehcache</groupId>
      <artifactId>jcache</artifactId>
      <version>1.0.0</version>
    </dependency>

「net.sf.ehcache」ではなく、「org.ehcache」です。

あとは、テストコードのためにJUnitとAssertJを追加します。

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>1.6.1</version>
      <scope>test</scope>
    </dependency>

このあたりは、個人のお好みで。

はじめてのJCache

で、Getting Startedにしたがってコードを書いていきたいところなのですが、見たところCacheを作成するためのコードが残念ながら間違っています。

このあたりについては、去年自分がJavaEE Advent CalendarでJCacheについて書いた時のものがそのまま使えそうなので、気になる方はそちらを参照ください。

Standard Caching
http://d.hatena.ne.jp/Kazuhira/20131204/1386163253

*この頃は、JCacheは1.0-PFDでした。

とりあえず、最低限これくらいimportしておいてください。

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;

あと、テストのために以下を追加しています。

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

import org.junit.Test;

それでは、もっとも基本的なコードを。

    @Test
    public void testSimpleCache() {
        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             Cache<String, String> cache = manager.createCache("simpleCache",
                                                               new MutableConfiguration<String, String>())) {
            cache.put("key1", "value1");

            assertThat(cache.get("key1"))
                .isEqualTo("value1");

            cache.remove("key1");

            assertThat(cache.get("key1")).
                isNull();
        }
    }

CacheManager、CacheProvider、Cacheと取得していき、最後のCacheをMapのように使います。とはいえ、CacheはMapインターフェースを実装しているわけではないので、ご注意を。

まあ、それほど戸惑うことはないと思いますが。

キャッシュの有効期間を設定する

続いて、キャッシュの有効期間を設定してみましょう。

JCacheではExpiryPolicyという設定で、キャッシュの有効期間を決めることができます。

ExpirePolicyには、以下の3つがあります。

名前 意味
Creation エントリを登録してからの経過時間で、有効かどうかを判断
Access エントリにアクセスしてからの経過時間で、有効かどうかを判断
Update エントリを更新してからの経過時間で、有効かどうかを判断

この3つと、Cacheの各メソッドの関係(例えば、getはAccessとして扱われ、replaceはUpdateとして扱われる、など)は、JSRの仕様に対応表がずらずらと書かれているので、そちらを見た方がよいです。
*PDFでは、「6. Expiry Policies」

そして、この3つに対して有効期限の設定を以下の中から選択することができます。

名前 対応するExpiryPolicy
CreatedExpiryPolicy Creation
ModifiedExpiryPolicy Update
AccessedExpiryPolicy Access
TouchedExpiryPolicy Access/Update
EternalExpiryPolicy なし(有効期限切れしない:デフォルト)

って、このあたりはAdvent Calenderで書いたものをそのまま貼っています…。

さて、続けましょう。

今回は、以下のimport文を追加して動かしてみます。

import java.util.concurrent.TimeUnit;
import javax.cache.configuration.Configuration;
import javax.cache.expiry.AccessedExpiryPolicy;
import javax.cache.expiry.Duration;

今回は、AccessedExpiryPolicyを使用して、アクセスしてから5秒で有効期限切れするキャッシュを設定してみます。

    @Test
    public void testExpiryCache() {
        Configuration<String, String> configuration = 
            new MutableConfiguration<String, String>()
                .setExpiryPolicyFactory(
                    AccessedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, 5)));

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             Cache<String, String> cache = manager.createCache("expiryCache",
                                                               configuration)) {
            cache.put("key1", "value1");

            assertThat(cache.get("key1"))
                .isEqualTo("value1");

            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }

            assertThat(cache.get("key1"))
                .isEqualTo("value1");

            try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { }

            assertThat(cache.get("key1"))
                .isNull();
        }
    }

6秒のスリープを入れた後、最終的にキーに対応する値がnullになることが確認できました。

EntryProcessor

あと、キャッシュ内の特定のキーに対して処理を行う、EntryProcessorというものもご紹介します。

今回のサンプルコードを動作させるためには、以下のimport文を追加してください。

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.cache.processor.EntryProcessorResult;

キャッシュ内に含まれる全キーをあらかじめSetに格納しておき、そのキー集合を対象にEntryProcessorを動作させます。この時、キャッシュ内のエントリが保持する値を2倍して返すことにします。

    public void testEntryProcessor() {
        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             Cache<String, Integer> cache = manager.createCache("entryProccesorCache",
                                                                new MutableConfiguration<String, Integer>())) {
             IntStream
                 .rangeClosed(1, 10)
                 .forEach(i -> cache.put(String.format("key%d", i), i));

             Set<String> keySet = new HashSet<>();
             cache.forEach(entry -> keySet.add(entry.getKey()));

             Map<String, EntryProcessorResult<Integer>> results =
                 cache
                     .invokeAll(keySet, (entry, args) -> {
                             if (entry.exists()) {
                                 return entry.getValue() * 2;
                             } else {
                                 return 0;
                             }
                         });

             int result =
                 results
                     .entrySet()
                     .stream()
                     .mapToInt(entry -> entry.getValue().get())
                     .sum();

             assertThat(result)
                 .isEqualTo(110);
        }
    }

Cache#invokeAllの結果はMapとして返るので、今回はその結果をsumしてみました。

Lambdaを使ったのでちょっとわかりにくいかもですが、EntryProcessorはこの部分です。

                 cache
                     .invokeAll(keySet, (entry, args) -> {
                             if (entry.exists()) {
                                 return entry.getValue() * 2;
                             } else {
                                 return 0;
                             }
                         });

invokeAllメソッドに渡している、第2引数ですね。EntryProcessor自体はインターフェースとして定義されています。

なお、Cache#invokeというメソッドもあり、こちらは単一のキーに対してEntryProcessorを起動させることができます。

Advent Calendarの時は、CDIと連携させたりしましたが、Ehcache-JCacheではJSR 107のRIにアノテーション定義があるから、そちらを使ってねというスタンスみたいです。

Using with JSR107 annotations
https://github.com/ehcache/ehcache-jcache/tree/master/ehcache-jcache#using-with-jsr107-annotations

こちらは、試していません…。

Ehcacheの設定ファイルを使用する

今回、JCacheの実装としてEhcacheを使用しましたが、JCacheではAPIを使ったキャッシュの設定しかできません。ここは、Ehcacheの設定ファイルを使用して、キャッシュを設定してみましょう。

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

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="false"
         monitoring="off"
         dynamicConfig="false">
  <cache
      name="jcacheCache"
      maxEntriesLocalHeap="10000"
      eternal="false"
      timeToIdleSeconds="5"
      timeToLiveSeconds="0"
      memoryStoreEvictionPolicy="LFU">
    <persistence strategy="none" />
  </cache>
</ehcache>

有効期限付きキャッシュです。

で、このようにimport文を書いておきます。

import java.net.URI;
import java.util.concurrent.TimeUnit;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;

JUnitとAssertJは省略

こちらが、用意したEhcacheの設定ファイルを読むように書いたコードになります。

    @Test
    public void testUseConfigurationXml() throws Exception {
        ClassLoader classLoader = getClass().getClassLoader();

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager(classLoader.getResource("ehcache-jcache.xml").toURI(),
                                                             classLoader);
             Cache<String, String> cache = manager.getCache("jcacheCache")) {
            cache.put("key1", "value1");

            assertThat(cache.get("key1"))
                .isEqualTo("value1");

            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }

            assertThat(cache.get("key1"))
                .isEqualTo("value1");

            try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { }

            assertThat(cache.get("key1"))
                .isEqualTo("value1");  // ここでキャッシュにエントリが残っているのはおかしいのでは…
        }
    }

CacheManagerを取得する際に、URI(とClassLoader)を渡すことで、どの設定ファイルを読むかを指定することができるようです。

             CacheManager manager = provider.getCacheManager(classLoader.getResource("ehcache-jcache.xml").toURI(),
                                                             classLoader);

なお、引数なしのCachingProvider#getCacheManagerを使用した場合、Ehcacheでは「ehcache.xml」を最初に探し、それがなければ「ehcache-failsafe.xml」を探すみたいですよ。

で、今回有効期限付きのキャッシュを設定したのですが、なぜかエントリが有効期限切れしませんでした…。

            assertThat(cache.get("key1"))
                .isEqualTo("value1");  // ここでキャッシュにエントリが残っているのはおかしいのでは…

何か変だな〜と思い、Cache#unwrapを使用してEhcacheの実体を引きずり出してみました。

で、検証コードを。

import文。

import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;

テストコード。

    @Test
    public void testUseConfigurationXmlAsEhcache() throws Exception {
        ClassLoader classLoader = getClass().getClassLoader();

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager(classLoader.getResource("ehcache-jcache.xml").toURI(),
                                                             classLoader);
             Cache<String, String> cache = manager.getCache("jcacheCache")) {
            Ehcache ehcache = cache.unwrap(Ehcache.class);

            // Expireは設定されている
            assertThat(ehcache.getCacheConfiguration().getTimeToIdleSeconds())
                .isEqualTo(5);

            ehcache.put(new Element("key1", "value1"));

            assertThat((String) ehcache.get("key1").getObjectValue())
                .isEqualTo("value1");

            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }

            assertThat((String) ehcache.get("key1").getObjectValue())
                .isEqualTo("value1");

            try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { }

            assertThat(ehcache.get("key1"))
                .isNull();
        }
    }

こちらは、ちゃんとエントリがなくなりました。設定上も、Expireは設定されているようですが…。

う〜ん、設定は効いているようですが、JCacheのAPI越しだと挙動が何か変ですねぇ…。

他のOSSのものでは、InfinispanやHazelcastでもJCache対応が進んでいるので、リリースされ次第見ていこうと思います。が、最初にEhcacheが出てくるとは思ってませんでした。Coherenceは、最近のリリースですでに対応しているという話みたいですけどね。