CLOVER🍀

That was when it all began.

JCacheのCDI連携を、GlassFish 4.1で動かす

PayaraにJCacheが搭載されたことで、少しJCacheの話題を見かけるようになりましたが、そういえばJCacheのCDI連携ってInfinispanとHazelcast(ただし、これはPayara上)の実装以外で試してないなーということに気付き、試してみることにしました。

JCacheのCDI連携って?

CDI連携と銘打ったものの、JSR-107では「11. Caching Annotations」と題され、CDIのみならずSpringやGuiceなどで使えるようなInterceptorを提供しようね、という感じの機能です。

JCacheのAPIとしては、@CacheResult、@CachePut、@CacheRemoveといったアノテーションや、CacheResolverなどのInterceptorが内部で使用するインターフェースが定義してあります。Interceptorのインターフェースそのものは定義されていません。

アノテーションをメソッドに付与することでInterceptorなどが介入し、メソッド呼び出しの結果をCacheから返却したり、Cacheの情報を更新するなどの操作を行うことができるようになります。宣言的Cache的な?

今回試すJCacheの実装と動作環境

今回試すJCacheの実装は、以下に載っているものからOSSのもので選ぶことにしました。

JSR-000107 JCACHE - Java Temporary Caching API Compatible Implementations
https://jcp.org/aboutJava/communityprocess/implementations/jsr107/index.html

Oracle Coherenceは対象外とします。

つまり、今回対象にするのは

  • Reference Implementation
  • Ehcache
  • Ehcache 3(オマケ)
  • Hazelcast
  • Infinispan
  • Apache Ignite

とします。

また、コードの動作確認環境は、GlassFish 4.1とします。いつもならWildFlyを選んでいるところですが、今回はこちらにしました。WildFlyはInfinispanが入っているので(JCacheの実装はまだ入っていませんが)、なんとなく…。

GlassFish - World's first Java EE 7 Application Server
https://glassfish.java.net/

サンプルコード

まず、各実装で共通的に使うコードを用意します。あくまでJCacheの実装を切り替えるだけとしたいので、コードそのものは変えないつもりです。

Maven依存関係については、以下の定義は常に含めるものとします。

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
            <version>1.0.0</version>
        </dependency>

サンプルコードは、JAX-RSCDI管理Beanとします。

まずはJAX-RS側。
src/main/java/org/littlewings/javaee7/cache/rest/JaxrsApplication.java

package org.littlewings.javaee7.cache.rest;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("rest")
public class JaxrsApplication extends Application {
}

リソースクラス。
src/main/java/org/littlewings/javaee7/cache/rest/CalcResource.java

package org.littlewings.javaee7.cache.rest;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.littlewings.javaee7.cache.cdi.CalcService;

@Path("calc")
@RequestScoped
public class CalcResource {
    @Inject
    private CalcService calcService;

    @Path("add")
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public int add(@QueryParam("a") int a, @QueryParam("b") int b) {
        return calcService.add(a, b);
    }
}

続いて、CDI管理Bean。こちらのクラスに@CacheDefaultsおよび@CacheResultアノテーションを付与することで、JCacheを使うようにします。
src/main/java/org/littlewings/javaee7/cache/cdi/CalcService.java

package org.littlewings.javaee7.cache.cdi;

import java.util.concurrent.TimeUnit;
import javax.cache.annotation.CacheDefaults;
import javax.cache.annotation.CacheResult;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
@CacheDefaults(cacheName = "calcCache")
public class CalcService {
    @CacheResult
    public int add(int a, int b) {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            // ignore
        }

        return a + b;
    }
}

Cacheが効いていることがわかりやすいように、3秒間のスリープを入れています。

また、Cacheエントリが有効期限切れするように、CDI管理Beanで使用するCacheの設定も入れておきます。
src/main/java/org/littlewings/javaee7/cache/cdi/CacheProducer.java

package org.littlewings.javaee7.cache.cdi;

import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;
import javax.ejb.Singleton;
import javax.ejb.Startup;

@Singleton
@Startup
public class CacheProducer {
    @PostConstruct
    public void createCalcCache() {
        CachingProvider cachingProvider = Caching.getCachingProvider();
        CacheManager cacheManager = cachingProvider.getCacheManager();
        Configuration<?, ?> configuration =
                new MutableConfiguration<>()
                        .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, 10)));

        cacheManager
                .createCache("calcCache",
                        configuration);
    }
}

有効期限(TTL)は、10秒とします。

それでは、ここにJCacheの実装を加えて動かしてみます。

Reference Implementation

いわゆるJCache RIですね。

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

実装をRIとして、かつCDI連携を使う場合のMaven依存関係は以下となります。

        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-ri-impl</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-annotations-ri-cdi</artifactId>
            <version>1.0.0</version>
        </dependency>

※「javaee-web-api」と「cache-api」は省略しています。

「cache-annotations-ri-cdi」が、CDI連携のためのInterceptorなどを含んだモジュールになります。

RIには、その他SpringやGuice用のモジュールもあったりします。

https://github.com/jsr107/RI/tree/master/cache-annotations-ri

また、beans.xmlにInterceptorを登録します。
src/main/webapp/WEB-INF/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>

省略できないかなーと思いましたが、書かないとうまく動きませんでした…。

ここまでしたら、パッケージングしてデプロイ。WARファイルの名前は、「jcache-ri-cdi.war」とします。

$ glassfish4/bin/asadmin deploy ri/target/jcache-ri-cdi.war

確認。最初は、遅いです。

$ time curl -i 'http://localhost:8080/jcache-ri-cdi/rest/calc/add?a=3&b=5'
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  4.1 
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Content-Type: text/plain
Date: Thu, 09 Jul 2015 13:26:40 GMT
Content-Length: 1

8
real	0m3.072s
user	0m0.006s
sys	0m0.000s

2回目は高速になります。

$ time curl -i 'http://localhost:8080/jcache-ri-cdi/rest/calc/add?a=3&b=5'
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  4.1 
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Content-Type: text/plain
Date: Thu, 09 Jul 2015 13:26:42 GMT
Content-Length: 1

8
real	0m0.021s
user	0m0.006s
sys	0m0.000s

しばらく待っていると、Cacheエントリが有効期限切れするので、また遅くなります。

$ time curl -i 'http://localhost:8080/jcache-ri-cdi/rest/calc/add?a=3&b=5'
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  4.1 
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Content-Type: text/plain
Date: Thu, 09 Jul 2015 13:26:59 GMT
Content-Length: 1

8
real	0m3.018s
user	0m0.006s
sys	0m0.000s

この後は、またしばらく高速になります。

以後、基本的に動作確認結果は同じなので、結果については省略します。ただ、このRIの「cache-annotations-ri-cdi」モジュールですが、他の実装でもとても大切な意味を持ちます。

では、次の実装に移ります。

Ehcache

Ehcache 2.X系のJCacheの実装です。

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

EhcacheでJCacheのCDI連携を使うための依存関係は、以下のようにしました。

        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>jcache</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-annotations-ri-cdi</artifactId>
            <version>1.0.0</version>
        </dependency>

なんと、RIの「cache-annotations-ri-cdi」モジュールが入っています。

READMEにもしっかり書かれていますが、アノテーションを使う場合はRIのものを使ってくれというスタンスみたいです…。

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

ちなみに、RIの「cache-annotations-ri-cdi」モジュールを含む各種(CDI、Spring、Guice向けの)Interceptor等の実装は、JCache RIへの依存関係を持っていません。JCache APIおよびRIのアノテーション関連のクラスのみの依存関係となっています。このため、JCache APIには汎用的に使えるようです。

というか、JCacheの実装ってこのあたりのクラスは用意しなくてもいいんですね…。

よって、beans.xmlもこのようになります。
src/main/webapp/WEB-INF/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>

ちなみにこのEhcacheのJCacheの実装ですが、Ehcache本体への追従が薄いですし、あんまり使われている感じもしないので、積極的に使いたくない感じです…。

Ehcache 3

まだ開発途中なので番外編的な感じですが、Ehcache 3もJCacheの実装を持っています。

Ehcache 3でJCacheのCDI連携を使う場合には、Maven依存関係はこのようになります。

        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.0.0.m1</version>
        </dependency>
        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-annotations-ri-cdi</artifactId>
            <version>1.0.0</version>
        </dependency>

ここでも、「cache-annotations-ri-cdi」が入っています。つまり、やっぱりアノテーション関連の実装はEhcache 3にも含まれていません。

というわけで、beans.xmlも同じことになります。
src/main/webapp/WEB-INF/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>

Hazelcast

インメモリ・データグリッドのHazelcast。こちらは、PayaraにJCacheの実装として採用されています。

Hazelcast
http://hazelcast.org/

Hazelcastには形態として、Server(Peer to Peer)とClient/Serverを取りますが、今回はServer(Peer to Peer)を使います。とはいえ、Client/Serverになっても依存関係とかがちょっと変わるだけですが。

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.5</version>
        </dependency>
        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-annotations-ri-cdi</artifactId>
            <version>1.0.0</version>
        </dependency>

ここでも、「cache-annotations-ri-cdi」が必要です…。

よってbeans.xmlも、同じ…。
src/main/webapp/WEB-INF/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>

Client/Serverモードになっても、JCache APIは使えますがアノテーション関連のクラスはないので、やっぱり「cache-annotations-ri-cdi」が必要です。

Infinispan

JBoss AS/WildFlyに採用されている、インメモリ・データグリッドです。

Infinispan
http://infinispan.org/

InfinispanもEmbedded ModeとClient/Server Modeを持ちますが、今回はEmbedded Modeとします。

Embedded ModeでJCacheのAPIを使う場合のMaven依存関係は、こちら。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-jcache</artifactId>
            <version>7.2.3.Final</version>
        </dependency>

Infinispanの場合は、「infinispan-jcache」にCDI関連のモジュールが含まれ、なおかつJCacheのアノテーション関連の実装も含んでいるため、他の実装と異なりRIのモジュールは必要ではありません。

この場合、他の実装と同じ挙動にするには、beans.xmlを以下のように設定します。
src/main/webapp/WEB-INF/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.infinispan.jcache.annotation.CacheResultInterceptor</class>
        <class>org.infinispan.jcache.annotation.CachePutInterceptor</class>
        <class>org.infinispan.jcache.annotation.CacheRemoveEntryInterceptor</class>
        <class>org.infinispan.jcache.annotation.CacheRemoveAllInterceptor</class>
    </interceptors>
</beans>

少し、含みを入れました。

これなんですけど、JCacheのアノテーションを使った記述が、InfinispanのCDIサポートのところで登場します。

19.6. Use JCache caching annotations
http://infinispan.org/docs/7.2.x/user_guide/user_guide.html#_use_jcache_caching_annotations

で、この通りに設定すると、ちょっと動きが変わってしまいます。
src/main/webapp/WEB-INF/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.infinispan.jcache.annotation.InjectedCacheResultInterceptor</class>
        <class>org.infinispan.jcache.annotation.InjectedCachePutInterceptor</class>
        <class>org.infinispan.jcache.annotation.InjectedCacheRemoveEntryInterceptor</class>
        <class>org.infinispan.jcache.annotation.InjectedCacheRemoveAllInterceptor</class>
    </interceptors>
</beans>

具体的に言うと、今回用意した実装だとTTLが無視されます。

これは、Cache取得方法が変わるからです。Injected〜Interceptorを使用した場合は、InfinispanのEmmbeddedCacheManagerをCDIで取得しようとします。このため、今回用意したJCacheのCacheManagerが使われず、デフォルトのEmbeddedCacheManagerからCacheを作って使用してしまいます。よって、他の実装方法と同じ挙動に持っていくには、InfinispanのEmbeddedCacheManagerを使ってCacheの設定をする必要があります。

つまり、こういう実装になります。
src/main/java/org/littlewings/javaee7/cache/cdi/CacheProducer.java

package org.littlewings.javaee7.cache.cdi;

import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;

import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;

@Dependent
public class CacheProducer {
    @ApplicationScoped
    @Produces
    public EmbeddedCacheManager createCacheManager() {
        EmbeddedCacheManager manager = new DefaultCacheManager();
        manager
                .defineConfiguration(
                        "calcCache",
                        new ConfigurationBuilder()
                                .expiration()
                                .lifespan(10, TimeUnit.SECONDS)
                                .build()
                );

        return manager;
    }
}

これで、他の実装と同じ挙動になります。

なお、JCache over Hot Rodの場合はアノテーション関連のクラスは無いようなので、もしも使うとすれば、RIのモジュールを使うことになりそうです…。

Apache Ignite

最後は、Apache Ignite

Apache Ignite
https://ignite.incubator.apache.org/

GridGainからApacheに移ったものみたいで、けっこう高機能な印象です。実は、初めて使います。

Compatible Implementationsに載っていることからJCacheの実装もあるようなのですが、ドキュメントに載っているのはあんまりJCacheっぽくないAPIの紹介です…。

JCache and Beyond
http://apacheignite.readme.io/docs/jcache

なお、Apache IgniteにもServerおよびClient/Serverがあるようですが、Client/Serverまではまだ把握していません…。今回は、Serverとして?使います。

Maven依存関係は以下となります。

        <dependency>
            <groupId>org.apache.ignite</groupId>
            <artifactId>ignite-core</artifactId>
            <version>1.2.0-incubating</version>
        </dependency>
        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-annotations-ri-cdi</artifactId>
            <version>1.0.0</version>
        </dependency>

ここでも、RIの「cache-annotations-ri-cdi」モジュールが。というわけで、JCacheのアノテーション関連の実装は、Apache Igniteも持っていません。

よって、beans.xmlもこのようになります。
src/main/webapp/WEB-INF/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の各種実装で、CDI連携(Interceptor利用)を試してみました。まとめると、だいたいこんな感じでしょうか。

  • JCacheの実装が、CDI連携のための実装を持っているとは限らない(SpringやGuiceで使う場合も同義)
  • JCache RIはCDI連携のためのモジュールを持つが、別モジュールとして定義してあるので明示的に依存関係への追加が必要
  • Infinispanは、Embedded ModeであればCDI連携のための実装を持っている
  • その他の実装でCDI連携を行うためには、RIのモジュールを使うか、自分で実装する?

JSR-107を見ても、「RIにCDI、Spring、Guiceと連携するためのサンプル実装があるよー」と書いているので、RIを全面的に使うのはいいのかな?という気がしないでもないです。
※RIのREADMEには、「This implementation is not meant for production use.For that we would refer you to one of the many open source and commercial implementations of JCache.」とあるので

JCacheではアノテーションとInterceptorを使用するイメージが強かったりするのかもしれません?が、意外とCacheManagerやCacheの実装のみで、アノテーション関連の実装を含んでいない実装が多いことにはちょっと注意ですね。

実はInterceptorが効いてませんでした、とかありそう…。

RIの実装がそうですが、CacheManagerはデフォルトのもので決まってしまうので、実装依存の設定ファイルを読ませる場合などはちょっと工夫が必要かも。あと、Interceptorが動いた時に、Cacheの定義がなかった場合は勝手にCacheを作って動きますからね(これはJCacheの仕様です)。

https://github.com/jsr107/RI/blob/master/cache-annotations-ri/cache-annotations-ri-common/src/main/java/org/jsr107/ri/annotations/DefaultCacheResolverFactory.java#L59
https://github.com/jsr107/RI/blob/master/cache-annotations-ri/cache-annotations-ri-common/src/main/java/org/jsr107/ri/annotations/DefaultCacheResolverFactory.java#L73

オマケ)Payaraはどうしてるの?

Payaraが採用しているHazelcastは、CDI連携のモジュールを持たないということでしたが、Payaraではどうしているんでしょうか?

答えは、自分で実装している、です。

https://github.com/payara/Payara/tree/payara-server-4.1.152/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107