CLOVER🍀

That was when it all began.

Infinispan × CDI(Embedded Cache)

とあるネタからの、スピンオフというか延長線上の話で。

Embedded Cacheのみですが、InfinispanのCDIサポートを試してみました。

CDI Support
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_cdi_support

最近Java EE 6を始めたばかりですが、InfinispanとEEの統合ネタを扱うのは初めてですね。アプリケーションサーバには、JBoss AS 7.1.1を使用します。

準備

相変わらず、sbt+xsbt-web-pluginでいきます。
build.sbt

name := "infinispan-cdi"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "littlewings"

seq(webSettings :_*)

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee6-web." + artifact.extension
}

resolvers += "Public JBoss Group" at "http://repository.jboss.org/nexus/content/groups/public-jboss"

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % "9.1.0.v20131115" % "container",
  "javax" % "javaee-web-api" % "6.0" % "provided",
  "org.infinispan" % "infinispan-core" % "6.0.0.Final" excludeAll(
    ExclusionRule(organization = "org.jgroups", name = "jgroups"),
    ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling-river"),
    ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling"),
    ExclusionRule(organization = "org.jboss.logging", name = "jboss-logging"),
    ExclusionRule(organization = "org.jboss.spec.javax.transaction", name = "jboss-transaction-api_1.1_spec")
  ),
  "org.infinispan" % "infinispan-cdi" % "6.0.0.Final",
  "org.jgroups" % "jgroups" % "3.4.1.Final",
  "org.jboss.spec.javax.transaction" % "jboss-transaction-api_1.1_spec" % "1.0.1.Final",
  "org.jboss.marshalling" % "jboss-marshalling-river" % "1.3.18.GA",
  "org.jboss.marshalling" % "jboss-marshalling" % "1.3.18.GA",
  "org.jboss.logging" % "jboss-logging" % "3.1.2.GA",
  "net.jcip" % "jcip-annotations" % "1.0"
)

CDI用のモジュールを追加しています。

  "org.infinispan" % "infinispan-cdi" % "6.0.0.Final",

*自分でEmbeddedCacheManagerとかCacheをきっちりProduces/Disposesする場合は、いらないかも…?

project/plugins.sbt

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.5.0")

今回から、xsbt-web-pluginが0.5.0になりました!

Java EE 6でCDIを使用するので、空ファイルでもbeans.xmlを作成します。

src/main/webapp/WEB-INF/beans.xml

インジェクションしてみる

以下の内容を参考に。

Inject an embedded cache
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_inject_an_embedded_cache

確認には、JAX-RSを使うことにします。
src/main/scala/javaee6/web/rest/CacheApplication.scala

package javaee6.web.rest

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

@ApplicationPath("/")
class CacheApplication extends Application {
}

InfinispanのCacheをインジェクションする、Resourceクラス。
src/main/scala/javaee6/web/rest/CalcResource.scala

package javaee6.web.rest

import javax.inject.Inject
import javax.ws.rs.{GET, Path, QueryParam}

import org.infinispan.Cache

@Path("calc")
class CalcResource {
  @Inject
  private var cache: Cache[String, Int] = _

  @GET
  @Path("add")
  def add(@QueryParam("p1") p1: Int, @QueryParam("p2") p2: Int): String = {
    val key = s"$p1:$p2"

    cache.containsKey(key) match {
      case true => s"Result By Cache[${cache.getName}] => ${cache.get(key)}"
      case false =>
        Thread.sleep(3 * 1000L)
        val value = p1 + p2
        cache.put(key, value)
        s"Result By Real => $value"
    }
  }
}

addメソッドでQueryStringからパラメータを受け取り、Cacheにすでに登録されていればその値を使用し、そうでなければ3秒後にCacheに結果を格納しつつレスポンスを返すクラスです。

つまり、初めて計算する値は、3秒かかるわけですね。

では、これをパッケージングして

> packageWar

デプロイ。

$ cp /path/to/javaee6-web.war $JBOSS_HOME/standalone/deployments/

確認してみます。

$ time curl "http://localhost:8080/javaee6-web/calc/add?p1=2&p2=3"
Result By Real => 5
real	0m4.422s
user	0m0.024s
sys	0m0.020s

1回目は、当然時間がかかります。

$ time curl "http://localhost:8080/javaee6-web/calc/add?p1=2&p2=3"
Result By Cache[___defaultcache] => 5
real	0m0.045s
user	0m0.008s
sys	0m0.024s

2度目は、Cacheにエントリがあるので早くなりましたね。なお、Cacheの名前も合わせて返却するようにしています。今回は、デフォルトのCacheが使用されています。

もちろん、パラメータを変えたりすると、また時間がかかるようになります。

$ time curl "http://localhost:8080/javaee6-web/calc/add?p1=2&p2=5"
Result By Real => 7
real	0m3.049s
user	0m0.016s
sys	0m0.016s

ところで、このデフォルトの状態だと再デプロイすると、JMX重複登録されてしまうためだと思いますが、コケてしまいました…。

ISPN000034: There's already an cache manager instance registered under 'org.infinispan' JMX domain. If you want to allow multiple instances configured with same JMX domain enable 'allowDuplicateDomains' attribute in 'globalJmxStatistics' config element
ISPN000034: There's already an cache manager instance registered under 'org.infinispan' JMX domain. If you want to allow multiple instances configured with same JMX domain enable 'allowDuplicateDomains' attribute in 'globalJmxStatistics' config element
MSC00004: Failure during stop of service jboss.deployment.unit."javaee6-web.war".WeldService: org.infinispan.jmx.JmxDomainConflictException: Domain already registered org.infinispan when trying to register: type=CacheManager,name="DefaultCacheManager"

これ以降の手順では、EmbeddedCacheManagerを自分で作成してシャットダウンするコードを追加したからだと思いますが、この事象は発生しなくなりました。

EmbeddedCacheManagerを自分で登録する

ドキュメントでは、Configurationをカスタマイズする手順が主に載っていますが、自分は設定ファイルを書く方が好みなので、EmbeddedCacheManagerを触る方を中心に見ていきたいと思います。

Override the default embedded cache manager and configuration
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_override_the_default_embedded_cache_manager_and_configuration

EmbeddedCacheManagerの、ProducesとDisposesに対応したクラスを用意。
src/main/scala/javaee6/web/cache/EmbeddedCacheManagerProvider.scala

package javaee6.web.cache

import javax.enterprise.context.ApplicationScoped
import javax.enterprise.inject.{Disposes, Produces}

import org.infinispan.manager.{DefaultCacheManager, EmbeddedCacheManager}

class EmbeddedCacheManagerProvider {
  @Produces
  @ApplicationScoped
  def getDefaultEmbeddedCacheManager: EmbeddedCacheManager =
    new DefaultCacheManager("infinispan.xml")

  private def stopEmbeddedCacheManager(@Disposes cacheManager: EmbeddedCacheManager): Unit =
    cacheManager.stop()
}

スコープは、ApplicationScopedとしましょう。

設定ファイルも用意しました。
src/main/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:6.0 http://www.infinispan.org/schemas/infinispan-config-6.0.xsd"
    xmlns="urn:infinispan:config:6.0">

  <global>
    <globalJmxStatistics
        enabled="true"
        jmxDomain="org.infinispan"
        cacheManagerName="DefaultCacheManager"
        />

    <shutdown hookBehavior="REGISTER"/>
  </global>

  <default>
    <expiration lifespan="5000" />
  </default>
</infinispan>

デフォルト設定として、5秒後にエントリの有効期限が切れるようにしています。

で、パッケージングしてデプロイすると1度アクセスしたエントリは5秒間は高速に動作5秒後には再度時間がかかるようになります。

$ time curl "http://localhost:8080/javaee6-web/calc/add?p1=2&p2=3"

デフォルトのCacheではなく、特定の名前のCacheをインジェクションする

ここまでは、デフォルトのCacheをインジェクションしてきました。最後に、特定の名前のCacheをインジェクションするようにしてみます。

Infinispanのドキュメントには、特定の名前のCacheに対するConfigurationを行ってインジェクションするものはあるのですが、EmbeddedCacheManagerを使うものはなかったので、どうするのかな…?と思っていたのですが。

冷静に考えると、できそうな感じがしたので試してみたら、やっぱりできました。

今回インジェクションする対象のCacheの名前は、「calcCache」とします。では、まずアノテーションを書きましょう。

アノテーションを書く…?ここですぐに、ScalaJavaで認識できるアノテーションが書けないことに気付き、パタと手が止まりました。

ここまできて、Javaを書けと…?

いやまあ、書きますよ。というわけで書いたのは、こんなアノテーション
src/main/java/javaee6/web/cache/CalcCache.java

package javaee6.web.cache;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Qualifier
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CalcCache {
}

ポイントは、@Qualifierアノテーションが付いているところですね。

CacheのProviderとして、こういうクラスを用意。
src/main/scala/javaee6/web/cache/EmbeddedCacheProvider.scala

package javaee6.web.cache

import javax.enterprise.context.ApplicationScoped
import javax.enterprise.inject.{Disposes, Produces}
import javax.inject.Inject

import org.infinispan.Cache
import org.infinispan.manager.EmbeddedCacheManager

class EmbeddedCacheProvider {
  @Inject
  private var cacheManager: EmbeddedCacheManager = _

  @CalcCache
  @Produces
  @ApplicationScoped
  def getCalcCache: Cache[String, Int] =
    cacheManager.getCache("calcCache")

  private def stopEmbeddedCacheManager(@CalcCache @Disposes cache: Cache[String, Int]): Unit =
    cache.stop
}

ここで、先ほど作成した@CalcCacheアノテーションを使用し、生成と破棄を扱っています。

設定ファイルもわかりやすいようにデフォルトはそのままにし、有効期限が5秒なのは「calcCache」のみとしました。
src/main/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:6.0 http://www.infinispan.org/schemas/infinispan-config-6.0.xsd"
    xmlns="urn:infinispan:config:6.0">

  <global>
    <globalJmxStatistics
        enabled="true"
        jmxDomain="org.infinispan"
        cacheManagerName="DefaultCacheManager"
        />

    <shutdown hookBehavior="REGISTER"/>
  </global>

  <default />

  <namedCache name="calcCache">
    <expiration lifespan="5000" />
  </namedCache>
</infinispan>

最後に、インジェクションするCacheに@CalcCacheを付与します。

@Path("calc")
class CalcResource {
  @CalcCache
  @Inject
  private var cache: Cache[String, Int] = _

これで、パッケージングしてデプロイ。

確認すると

$ time curl "http://localhost:8080/javaee6-web/calc/add?p1=2&p2=3"
Result By Cache[calcCache] => 5
real	0m0.055s
user	0m0.024s
sys	0m0.024s

Cacheの名前が、「calcCache」になっていることが確認できます。もちろん、5秒後には有効期限切れするところも設定できました。