CLOVER🍀

That was when it all began.

JCacheの実装とキャッシュ管理のご紹介 #javaee

はじめに

この記事は、「Java EE Advent Calendar 2014」の4日目の記事となります。
昨日は、@k-kobayashiさんの「Goodbye Struts, Hello Java EE #javaee - いつブロ」でした。
明日は、@tq_jappyさんのご担当となります。

なお、去年も4日目を担当させていただきました。今年もよろしくお願いします!

JCacheについて

取り扱うネタについても、去年に引き続きJCacheです。

今年の3月に、JSR-107(JCache)が1.0.0になりました。

JSR 107: JCACHE - Java Temporary Caching API
https://jcp.org/en/jsr/detail?id=107

JSR107 (JCache)
https://github.com/jsr107/jsr107spec

JCACHEの仕様が完成
http://www.infoq.com/jp/news/2014/04/jcache-finalized

Java EE 7には間に合わなかったため、Java EE 8で入るらしいですね。

JCacheは、Mapによく似たキャッシュを扱うAPIを提供します。データの保存方法などについては、JCacheでは規程しません。

簡単にAPI紹介

まず、簡単にJCacheのAPIを使ったコード例をご紹介します。
*JCacheにはCDIとの連携もありますが、今回は対象外とします

Mavenで使用する場合は、以下のようにJCache APIへの依存関係を定義します。

    <!-- JCache API -->
    <dependency>
      <groupId>javax.cache</groupId>
      <artifactId>cache-api</artifactId>
      <version>1.0.0</version>
    </dependency>

サンプルコードでは、以下の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.configuration.Configuration;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.AccessedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;

あと、JUnit/Hamcrestも使います。

Cacheの定義と作成。

        CachingProvider cachingProvider = Caching.getCachingProvider();
        CacheManager cacheManager = cachingProvider.getCacheManager();

        Configuration<String, Integer> config =
            new MutableConfiguration<String, Integer>()
            .setTypes(String.class, Integer.class)
            .setExpiryPolicyFactory(AccessedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, 5)));

        Cache<String, Integer> cache =
            cacheManager.createCache("simpleCache", config);

主な登場人物はCachingProvider、CacheManager、Cacheで、CacheManagerからCacheを定義・取得する時に、設定(Configuration)が合わせて必要です。

Configurationを定義する時に、キーと値の型(setTypes)は行った方がよいでしょう。指定しないと、キーも値もObjectなCacheを定義したことになります。

なお、保存するキーと値は、Serializableであるべきです。

取得したCacheを使ってみます。

        // put
        cache.put("key", 10);
        assertThat(cache.get("key"), is(10));

        // remove
        cache.remove("key");
        assertThat(cache.get("key"), nullValue());

        // Expire
        cache.put("key1", 10);
        cache.put("key2", 20);

        TimeUnit.SECONDS.sleep(3);
        cache.get("key1");

        TimeUnit.SECONDS.sleep(3);

        assertThat(cache.get("key1"), is(10));         // 1度アクセスしているので、まだ有効なエントリ
        assertThat(cache.get("key2"), nullValue());  // こちらは有効期限切れ

        TimeUnit.SECONDS.sleep(6);

        assertThat(cache.get("key1"), nullValue());         // こちらも有効期限切れ

今回の例では、Cacheに保持したエントリの有効期限を5秒(ポリシーは「Access」)にしていたので、5秒以内にアクセスがあればエントリは生存しますし、5秒経過すればエントリは有効期限切れでキャッシュから取得できなくなります。

1度定義したCacheは、CacheManagerから取得可能です。

        // Cache名、キーと値の型を指定して、Cacheを取得
        Cache<String, Integer> definedCache =
            cacheManager.getCache("simpleCache", String.class, Integer.class);
        assertThat(definedCache, not(nullValue()));

キーと値に指定するClassクラスが定義と合わなかった場合は、取得できません。一応、Cacheの名前だけでも取得できるのですが…。

最後は、取得したCache、CacheManager、CachingProviderをクローズします。

        // Cache、CacheManager、CachingProviderはCloseableです
        cache.close();
        cacheManager.close();
        cachingProvider.close();

これらは、Closeableインターフェースを実装しているので、try-with-resourcesでクローズ可能です。

このあたりのCacheのAPIの使い方(Mapに似ていますが、Mapを実装しているわけではないです)や、設定はそれほど多くないので、あとはドキュメントやJavadocを見ればよいでしょう。

また、CacheManagerはCachingProviderからURI(と厳密にはClassLoader)で、それぞれ別に管理することができます。このように。

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        CachingProvider cachingProvider = Caching.getCachingProvider();

        // URIで、CacheManagerを分ける
        // JCacheの実装によっては、URIから設定ファイルを読み込むことも可能
        CacheManager cacheManager1 =
            cachingProvider.getCacheManager(URI.create("cacheManager1"), classLoader);
        CacheManager cacheManager2 =
            cachingProvider.getCacheManager(URI.create("cacheManager2"), classLoader);

コメントにも書いていますが、実装によってはURIに設定ファイルへのパスを書くことで、実装依存の設定ファイルを読み込ませることもできたりします。

CacheManagerを分けると、それぞれのCacheManagerでCacheを別々に管理することができます。

        Configuration<String, Integer> config1 =
            new MutableConfiguration<String, Integer>()
            .setTypes(String.class, Integer.class);

        Configuration<String, String> config2 =
            new MutableConfiguration<String, String>()
            .setTypes(String.class, String.class);

        // それぞれのCacheManagerに、Cacheを作成
        cacheManager1.createCache("cache1FromCacheManager1", config1);
        cacheManager1.createCache("cache2FromCacheManager1", config1);

        cacheManager2.createCache("cacheFromCacheManager2", config2);

        // CacheManagerごとに、Cacheは別々に管理される
        assertThat(cacheManager1.getCacheNames(),
                   containsInAnyOrder("cache1FromCacheManager1", "cache2FromCacheManager1"));
        assertThat(cacheManager1.getCache("cacheFromCacheManager2"), nullValue());

        assertThat(cacheManager2.getCacheNames(),
                   containsInAnyOrder("cacheFromCacheManager2"));
        assertThat(cacheManager2.getCache("cache1FromCacheManager1"), nullValue());
        assertThat(cacheManager2.getCache("cache2FromCacheManager1"), nullValue());

        // あとでそれぞれクローズ

ところで、このコードはこれだけだと依存関係にJCacheの実装を入れていないので動作しません。

javax.cache.CacheException: No CachingProviders have been configured

先ほどMaven依存関係に定義したのはあくまでAPIだけなので、JCacheの実装を追加する必要があります。

JCacheの実装紹介

ここでは、JCacheのAPIを実装したCache Providerを、オープンソースのものを中心にご紹介します。JCacheは、名前こそたまに見かけるものの頻度が少ないですし、その実装についての話題はさらに見かけないので、これを機に、と思いまして。

必要なMaven依存関係についてもご紹介しますが、JCache APIを明示的に追加する必要があるかどうかは、実装次第です。最近の傾向を見ていると、実装側ではJCache APIがprovidedで指定されるように修正されることが多いようです。

Reference Implementation

GitHubにある、JSR-107のorganizationにある参照実装(RI)です。

JCache Reference Implementation
https://github.com/jsr107/RI

最初の注意書きに、プロダクション環境では使用するべきではない、他のオープンソースや商用のJCacheの実装を使用しなさいとありますが、こちらでも簡単にJCacheを試すことができます。

キャッシュの保存先は、インメモリ(ConcurrentHashMap)です。実装依存の設定ファイルもありません。

RIを使用するのに必要なMaven依存関係は、こちら。

    <!-- Reference Implementation -->
    <dependency>
      <groupId>org.jsr107.ri</groupId>
      <artifactId>cache-ri-impl</artifactId>
      <version>1.0.0</version>
    </dependency>
Ehcache

Javaでキャッシュといえば、たぶん1番有名なライブラリではないでしょうか。Ehcacheによる、JCacheの実装です。

About Ehcache Support for JSR107
http://ehcache.org/generated/2.9.0/html/ehc-all/#page/Ehcache_Documentation_Set%2Fco-jsr_about_support_for_jsr107.html%23

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

キャッシュの使用方法としては、ローカルキャッシュとしての利用が多かったりするのでしょうか?レプリケーションにも対応しているようです(RMI、JGroups、JMS)。

Ehcache-JCacheについては、CacheManagerを取得する際にURIで設定ファイルを指定することができます。

Ehcache-JCacheを使用するのに必要なMaven依存関係は、こちら。

    <!-- Ehcache-JCache -->
    <dependency>
      <groupId>org.ehcache</groupId>
      <artifactId>jcache</artifactId>
      <version>1.0.0</version>
    </dependency>

なお、このモジュールはEhcache本体とは別開発みたいなので、Ehcache自体の最新版に追従していなかったり、Cacheを設定ファイルで構築した時にJCache API越しに操作するとExpireが効かないなどの微妙なバグがあったりします。

Expiration configured in ehcache.xml in not honoured
https://github.com/ehcache/ehcache-jcache/issues/26

開発中と思われるEhcache 3では、最初からJSR-107の対応が本体にいるようなので、こちらに期待すべきかもしれませんね。

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

当分、出てこないと思いますが…。

Infinispan

WildFlyJBoss ASの中でクラスタリングやキャッシュに使用されているライブラリで、単なるキャッシュというよりはインメモリ・データグリッドという位置付けです。

Infinispan
http://infinispan.org/

Using Infinispan as a JSR107 (JCache) provider
http://infinispan.org/docs/7.0.x/user_guide/user_guide.html#_using_infinispan_as_a_jsr107_jcache_provider

ついこの間出てきた、7.0.0.FinalからJCache 1.0.0に対応しています。

キャッシュの構成としては、ローカルキャッシュと分散、レプリケーションなどの構成を取ることができます。デフォルトはローカルキャッシュですが、分散構成も簡単に設定することができます。

CacheManager取得の際に、設定ファイルを読み込ませることも可能です。

InfinispanのJCache Providerを使用するのに必要なMaven依存関係は、こちらです。下記のうち、用途に応じてどちらかを指定すればよいです。

    <!-- Infinispan JCache -->
    <!-- JCache -->
    <dependency>
      <groupId>org.infinispan</groupId>
      <artifactId>infinispan-jcache</artifactId>
      <version>7.0.2.Final</version>
    </dependency>

    <!-- JCacheも含めて全部 -->
    <dependency>
      <groupId>org.infinispan</groupId>
      <artifactId>infinispan-embedded</artifactId>
      <version>7.0.2.Final</version>
    </dependency>

JCacheだけ使いたいなら、「infinispan-jcache」でOKです。「infinispan-embedded」は、その他の機能も全部入りのものです。

Hazelcast

こちらも、キャッシュというよりはインメモリ・データグリッドとしての位置付けになります。

Hazelcast
http://hazelcast.org/

Hazelcast JCache Implementation
http://docs.hazelcast.org/docs/3.3/manual/html-single/hazelcast-documentation.html#hazelcast-jcache-implementation

Hazelcast 3.3.1より、JCacheの実装が含まれるようになりました。

Hazelcastは他の実装と異なりローカルのみというモードを持たないので、クラスタ構成前提で起動します。このため、複数のJavaVMでHazelcastのJCache実装を使用すると、デフォルトでクラスタが構成されます。

また、インメモリ・データグリッドでよくとられるEmbedded、クライアント/サーバーの形態がJCacheとしての使用方法にも影響します。

HazelcastでJCache使用するのに必要なMaven依存関係は、こちらです。下記のうち、いずれかを指定します。

    <!-- Hazelcast -->
    <!-- Hazelcast本体 -->
    <dependency>
      <groupId>com.hazelcast</groupId>
      <artifactId>hazelcast</artifactId>
      <version>3.3.3</version>
    </dependency>

    <!-- Client Provider -->
    <dependency>
      <groupId>com.hazelcast</groupId>
      <artifactId>hazelcast-client</artifactId>
      <version>3.3.3</version>
    </dependency>

    <!-- Client/Server全部 -->
    <dependency>
      <groupId>com.hazelcast</groupId>
      <artifactId>hazelcast-all</artifactId>
      <version>3.3.3</version>
    </dependency>

「hazelcast」を選ぶとサーバーモード(Embeddedと言い換えてもよいかも…)になり、単体で動作させることができます。「halzelcast-client」または「hazelcast-all」を選択すると、デフォルトがクライアントモードになります。この場合は別途サーバーモードのHazelcastインスタンスが起動しており、かつこちらに接続できない場合にはクライアント側は起動することができません。

クライアント/サーバーモードについてはインメモリ・データグリッドでは(実装によっていろいろ異なりますが)よく見る話ですが、JCacheにまで表れているところはちょっと面白いです。

追加する依存関係によってデフォルトの挙動が変わりますが、こちらはCacheProviderやシステムプロパティの指定などでクライアント/サーバーモードを切り替えることができます。

なお、Hazelcast自体には設定ファイルが存在しますが、JCache Providerとして使用する時に、URIから設定ファイルを指定することはできません。現状、名前空間分けのみで利用されているようです。

ちなみに、Ehcacheの作者であるGreg Luck氏ですが、HazelcastのCTOになっています。

Hazelcast Appoints Greg Luck as CEO
http://blog.hazelcast.com/2014/06/18/hazelcast-appoints-greg-luck-as-ceo/

その他

Apache Commons JCSって、JCacheに対応するんでしょうかねぇ…。ソースリポジトリを見ると対応しようというところは見れるのですが、リリースされる雰囲気があまりないような…。

商用製品だと、Oracle CoherenceがJCacheに対応しています。

Java EE 7/8の新機能を先取り! Oracle WebLogic ServerとOracle Coherenceが12.1.3にバージョンアップ
http://builder.japan.zdnet.com/sp_oracle/weblogic/35052344/2/

Introduction to Coherence JCache
https://docs.oracle.com/middleware/1213/coherence/develop-applications/jcache_intro.htm#COHDG5778

Coherenceのドキュメントは、JCache APIの使い方もかなり触れられているので、Coherenceを使わずともJCacheに興味がある場合は、参照されるとよいかもしれません。

他のオープンソースのものだとGridGainがあったり、商用製品にもインメモリ・データグリッドはありますが、これらのJCacheへの対応状況はわかりませんでした…。

JCache Providerが複数存在する場合

これまでの例では、CachingというクラスからCachingProviderを取得する際に、以下のようなコードを書いていました。

        CachingProvider cachingProvider = Caching.getCachingProvider();

このコードは、クラスパスにJCacheの実装がひとつの場合のみに動作します。JCacheの実装が複数クラスパスに存在してしまうと、どの実装を使用するのか決定することができなくなるため、例外がスローされてしまいます。

javax.cache.CacheException: Multiple CachingProviders have been configured when only a single CachingProvider is expected

このような状況の場合、以下のような設定・実装を行うことで対処することができます。

システムプロパティで、CachingProviderの実装を指定する

プログラムの実行時に、「javax.cache.spi.CachingProvider」システムプロパティにCachingProviderの実装の完全修飾名を与えます。例として、以下ではReference ImplementationのCachingProviderを指定しています。

-Djavax.cache.spi.CachingProvider=org.jsr107.ri.spi.RICachingProvider
Caching#getCachingProviderの呼び出し時に、引数を追加する

Caching#getCachingProviderメソッドの呼び出し時に、CachingProviderの完全修飾名を与えることで、どのCachingProviderを使用するか指定することができます。

例えば、今回紹介したJCacheの実装がすべてクラスパス上にいる場合(極端ですが…)には、以下のように実装することでどのCachingProviderを使用するかを指定できます。

        CachingProvider riCachingProvider =
            Caching.getCachingProvider("org.jsr107.ri.spi.RICachingProvider");
        CachingProvider ehcacheCachingProvider =
            Caching.getCachingProvider("org.ehcache.jcache.JCacheCachingProvider");
        CachingProvider infinispanCachingProvider =
            Caching.getCachingProvider("org.infinispan.jcache.JCachingProvider");
        CachingProvider hazelcastCachingProvider =
            Caching.getCachingProvider("com.hazelcast.cache.impl.HazelcastServerCachingProvider");
            // もしくは
            // Caching.getCachingProvider("com.hazelcast.client.cache.impl.HazelcastClientCachingProvider");

Hazelcastの場合は、クライアント/サーバーモードのどちらを使用するかで、使用するCachingProviderが変わります。


これらのCachingProviderから作成されるCacheManagerは、別々に存在するので、それぞれでCacheの定義・管理が可能です。

        CacheManager riCacheManager =
            riCachingProvider.getCacheManager();
        CacheManager ehcacheCacheManager =
            ehcacheCachingProvider.getCacheManager();
        CacheManager infinispanCacheManager =
            infinispanCachingProvider.getCacheManager();
        CacheManager hazelcastCacheManager =
            hazelcastCachingProvider.getCacheManager();

        Configuration<String, Integer> config =
            new MutableConfiguration<String, Integer>()
            .setTypes(String.class, Integer.class);

        riCacheManager.createCache("cache", config);
        ehcacheCacheManager.createCache("cache", config);
        infinispanCacheManager.createCache("cache", config);
        hazelcastCacheManager.createCache("cache", config);

        // クローズ処理は省略
Caching#getCachingProviersで取得する

Caching#getCachingProvidersを使用することで、現在利用可能なCachingProviersをIterableで取得可能です。

        for (CachingProvider cachingProvider : Caching.getCachingProviders()) {
            // ...
        }

先ほどのように、クラスパス上に全部入れた場合、このようにすべてのCachingProviderが取得できることが確認できます。

        List<Class<?>> cachingProviderClasses =
            StreamSupport
            .stream(Caching.getCachingProviders().spliterator(), false)
            .map(CachingProvider::getClass)
            .collect(Collectors.toList());

        assertThat(cachingProviderClasses,
                   containsInAnyOrder(org.jsr107.ri.spi.RICachingProvider.class,
                                      org.ehcache.jcache.JCacheCachingProvider.class,
                                      org.infinispan.jcache.JCachingProvider.class,
                                      com.hazelcast.cache.impl.HazelcastCachingProvider.class));

なお、Hazelcastは先ほど紹介したクラス名と微妙に違いますが、こちらは実行時にHazelcastがクライアント/サーバーモードを自動検出するCachingProviderです。先ほどのクラス名は、このクラスの委譲先になります。このクラス名は、Hazelcast 3.4で変わりそうですが…。

CacheManagerやCacheの管理について

以上のことをまとめると、

  • Caching#getCachingProviderから取得できるCachingProviderは、使用したい実装ごとに指定・選択できる
    • ただし、実行時にJCacheの実装がひとつしか含まれない場合は、引数なしで呼び出しが可能
  • CachingProviderは、実装ごとに存在する
  • CachingProviderから、CacheManagerを複数定義・管理できる
  • CacheManagerから、Cacheを複数定義・管理できる

という感じになります。図にすると、こういったところでしょうか。

※本当は、ここにクラスローダーが入ります

おわり

というわけで、ちょっと長くなりましたが、現時点でのJCacheの実装の紹介と、CachingProviderやCacheManagerによるキャッシュ管理をご紹介しました。

キャッシュだけあってもなかなか使うところがないかもしれませんが、JCacheのAPIやその実装について少しでも興味を持っていただけると幸いです。


今回作成したソースコードは、こちらに置いています。

JCache APIの簡単な使い方
https://github.com/kazuhira-r/javaee-advent-calendar/tree/master/2014/jcache-getting-started

複数のJCache Providerがクラスパス上に存在する場合の使い分け
https://github.com/kazuhira-r/javaee-advent-calendar/tree/master/2014/jcache-mixed