CLOVER🍀

That was when it all began.

InfinispanのJCache×CDI連携で、CacheアノテーションをTransactionalにする

今のJCacheにはトランザクションに関する機能はありませんが、そのうち実装されるような雰囲気を以下の記事で見ることができます。

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

Cacheにトランザクションなんて必要?という意見もあるかもしれませんが、他のリソースをキャッシュしている場合、一貫性という面でトランザクションに対応していた方が好ましいケースも考えられます。
更新されうるデータベースの内容をキャッシュしていた時とか…。

で、先に述べたようにJCacheそのものにはトランザクションに関する機能はないのですが、JCacheの実装にはトランザクションに関する機能を備えているものもあります。

今回は、その中でも比較的(自分には)実現が用意な組み合わせとして、WildFlyとInfinispanを使用して、JCacheの各種アノテーションに関するInterceptorの処理をTransactional(JTAと連携)にしてみたいと思います。

というわけで、ここから書くのは以下の内容です。

  • InfinispanでTransactionalなCacheを定義する
  • JAX-RSCDIで簡単なWebアプリケーションを作って、その中で@Transactionalと@CacheXXXの各種アノテーションを使用する
  • 処理が正常に終了した場合はCacheが更新され、RuntimeExceptionがスローされた場合にはCacheが更新されないことを確認する
  • Cache以外のトランザクションリソースは、今回は使用しない

では、書いていきます。

準備

まずは、ビルド定義。
build.sbt

name := "embedded-jcache-cdi"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.7"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

enablePlugins(JettyPlugin)

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

webappWebInfClasses := true

libraryDependencies ++= Seq(
  "javax" % "javaee-web-api" % "7.0" % "provided",
  "javax.cache" % "cache-api" % "1.0.0",
  "org.infinispan" % "infinispan-jcache" % "7.2.3.Final",
  "net.jcip" % "jcip-annotations" % "1.0" % "provided"
)

xsbt-web-pluginを使って、Webアプリケーションを作ります。

プラグインの定義。
project/plugins.sbt

logLevel := Level.Warn

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

Infinispanは、WildFlyに含まれているものではなくて、明示的にWARファイルに含めるようにしました。

また、WildFly 9.0.0.Finalは以下よりダウンロードして、

Downloads
http://wildfly.org/downloads/

展開後、起動しておきます。

$ unzip wildfly-9.0.0.Final.zip
$ wildfly-9.0.0.Final/bin/standalone.sh

TransactionalなCacheの定義とbeans.xmlの設定

プログラム内で使うCacheの定義を、Infinispanの設定ファイルで行います。
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:7.2 http://www.infinispan.org/schemas/infinispan-config-7.2.xsd"
        xmlns="urn:infinispan:config:7.2">
    <cache-container name="cacheManager" shutdown-hook="REGISTER">
        <local-cache name="transactionalCache">
            <transaction mode="NON_XA" auto-commit="true"/>
            <locking isolation="READ_COMMITTED"/>
        </local-cache>
    </cache-container>
</infinispan>

TransactionalなCacheを、そのまま「transactionalCache」という名前で定義し、トランザクションのモードは今回は簡単のためトランザクション管理するものをCacheのみとするので、「NON_XA」(javax.transaction.Synchronizationを使ってトランザクションに参加する)とします。その他にもちょっと設定を書いていますが、デフォルトと同じだったりします。

なお、Infinispanで選択できるトランザクションのモードには、以下の4つがあります。

Configuring transactions
http://infinispan.org/docs/7.2.x/user_guide/user_guide.html#_configuring_transactions

グローバルトランザクションに参加するXAリソースとしたい場合は、必要に応じて「NON_DURABLE_XA」または「FULL_XA」を選ぶことになるかと思います。

今回はJCacheとCDIの連携とするので、beans.xmlに設定するInfinispanの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.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>

CacheManagerのProducerを作成する

先に作った、InfinispanのCache定義を利用するように、EmbeddedCacheManagerに対するProducerを定義します。
src/main/scala/org/littlewings/infinispan/producer/CacheProducer.scala

package org.littlewings.infinispan.producer

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

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

@Dependent
class CacheProducer {
  @ApplicationScoped
  @Produces
  def createEmbeddedCacheManager: EmbeddedCacheManager = new DefaultCacheManager("infinispan.xml")

  def destroyEmbeddedCacheManager(@Disposes embeddedCacheManager: EmbeddedCacheManager): Unit = embeddedCacheManager.stop()
}

まあ、こちらは単純です。

JAX-RSリソースクラスとCDI管理Beanを作成する

それでは、ここまで用意した設定やCacheManagerを使うプログラムを書いてみます。

まずは、Cacheアノテーションを使用するCDI管理Bean。
src/main/scala/org/littlewings/infinispan/service/MessageService.scala

package org.littlewings.infinispan.service

import javax.cache.annotation.{CacheKey, CachePut, CacheResult, CacheValue}
import javax.enterprise.context.ApplicationScoped

@ApplicationScoped
class MessageService {
  @CachePut(cacheName = "transactionalCache")
  def putCache(@CacheKey key: String, @CacheValue message: String): Unit = ()

  @CacheResult(cacheName = "transactionalCache")
  def message(key: String): String = "default-message"
}

ちょっと使い方がいびつなんですけど、MessageService#messageで指定されたKeyに対するメッセージをCacheから取得し(@CacheResult)、MessageService#putCacheでCacheに指定されたKeyでメッセージを登録します(@CachePut、@CacheKey、@CacheValue)。

Cacheが更新されていない場合には、MessageService#messageの戻り値は「default-message」となります。

MessageService#putCacheは、InterceptorがCacheを更新する以外に何もすることがないので、実装が空です。

なお、今回はCacheの名前を@CacheDefaultsではなく、各アノテーションに指定してみました。この部分ですね。

  @CacheResult(cacheName = "transactionalCache")

そして、JAX-RSリソースクラス。
src/main/scala/org/littlewings/infinispan/rest/CacheResource.scala

package org.littlewings.infinispan.rest

import javax.enterprise.context.RequestScoped
import javax.inject.Inject
import javax.transaction.Transactional
import javax.ws.rs.core.MediaType
import javax.ws.rs.{GET, Path, Produces, QueryParam}

import org.littlewings.infinispan.service.MessageService

@Path("cache")
@RequestScoped
class CacheResource {
  @Inject
  private var messageService: MessageService = _

  @GET
  @Path("put")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @Transactional
  def put(@QueryParam("key") key: String, @QueryParam("message") message: String): String = {
    messageService.putCache(key, message)
    s"Putted $key:$message."
  }

  @GET
  @Path("putFail")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @Transactional
  def putFail(@QueryParam("key") key: String, @QueryParam("message") message: String): String = {
    messageService.putCache(key, message)
    throw new RuntimeException("Oops!!")
  }

  @GET
  @Path("get")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @Transactional
  def get(@QueryParam("key") key: String): String =
    messageService.message(key)
}

CDI管理Beanとし、全メソッドに@Transactionalを付けています。最後のは参照のみなので、なくてもいいですが…。

以下の2つのメソッドについては、Cacheを更新しようとするところは同じですが、片方はそのまま終了し、もう片方はRuntimeExceptionがスローされます。

  @GET
  @Path("put")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @Transactional
  def put(@QueryParam("key") key: String, @QueryParam("message") message: String): String = {
    messageService.putCache(key, message)
    s"Putted $key:$message."
  }

  @GET
  @Path("putFail")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @Transactional
  def putFail(@QueryParam("key") key: String, @QueryParam("message") message: String): String = {
    messageService.putCache(key, message)
    throw new RuntimeException("Oops!!")
  }

ここで見るのは、後者の時はロールバックされるかどうかですね。

結果は、こちらのメソッドで確認します。

  @GET
  @Path("get")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @Transactional
  def get(@QueryParam("key") key: String): String =
    messageService.message(key)

最後に、JAX-RSの有効化。
src/main/scala/org/littlewings/infinispan/rest/JaxrsApplication.scala

package org.littlewings.infinispan.rest

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

@ApplicationPath("rest")
class JaxrsApplication extends Application

動作確認

それでは、作成したアプリケーションをWARファイルにパッケージングしてデプロイしてみます。

> package

これで、「javaee7-web.war」というWARファイルができるので、WildFlyにデプロイします。

$ cp target/scala-2.11/javaee7-web.war /path/to/wildfly-9.0.0.Final/standalone/deployments/

それでは、動作確認してみましょう。

まずは、デフォルトメッセージを取得。Keyは、ここでは「key1」とします。

$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key1'
default-message

続いて、Cacheを更新してみます。

$ curl 'http://localhost:8080/javaee7-web/rest/cache/put?key=key1&message=new-message1'
Putted key1:new-message1.

「key1」に対して、「new-message1」という値を登録しました。

もう1度取得してみます。

$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key1'
new-message1

結果が変わりましたね。

なお、Keyを変えると当然デフォルトメッセージが取得されることになります。

$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key2'
default-message

※Keyを「key2」にしました

それでは、Cache関係のInterceptorは動かすものの、JAX-RSリソース側で例外がスローされるパターンを確認してみます。

$ curl 'http://localhost:8080/javaee7-web/rest/cache/putFail?key=key1&message=new-message1-1'

これを実行すると、例外がスローされるのでエラーが表示されます(省略)。

WildFlyのコンソールでも、例外がスローされたことが確認できます。

Caused by: java.lang.RuntimeException: Oops!!
	at org.littlewings.infinispan.rest.CacheResource.putFail(CacheResource.scala:32)
	at org.littlewings.infinispan.rest.CacheResource$Proxy$_$$_WeldSubclass.putFail$$super(Unknown Source)

ちなみに、ここは値は「new-message1-1」にしようとしていました。

もう1度、メッセージを取得してみます。

$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key1'
new-message1

値は変わっていませんね。OKそうです。

オマケ

一応、Infinispanに設定したトランザクションの定義が有効なんだよね?ということを確認するために、いったんコメントアウトして動作確認してみます。

    <cache-container name="cacheManager" shutdown-hook="REGISTER">
        <local-cache name="transactionalCache">
            <!--
            <transaction mode="NON_XA" auto-commit="true"/>
            <locking isolation="READ_COMMITTED"/>
            -->
        </local-cache>
    </cache-container>

トランザクション非対応にしました。

再度、パッケージングしてデプロイ。

動作確認。ここでは、簡単に載せます。

## デフォルトメッセージ
$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key1'
default-message

## 更新
$ curl 'http://localhost:8080/javaee7-web/rest/cache/put?key=key1&message=new-message1'
Putted key1:new-message1.

## 確認
$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key1'
new-message1

## 例外スロー
$ curl 'http://localhost:8080/javaee7-web/rest/cache/putFail?key=key1&message=new-message1-1'
## ※エラーは省略

## 確認
$ curl 'http://localhost:8080/javaee7-web/rest/cache/get?key=key1'
new-message1-1

というわけで、最後が「new-message1-1」になってしまい、ロールバックされなくなってしまいましたね。設定が効いていると思って、大丈夫そうです。

まとめ

JCache自体は未対応で実装依存の話になりますが、InfinispanのCacheがトランザクション(というかJTA)に対応させてみました。

Infinispanは、トランザクションに関する話題が最初からJTAと統合されているので、このテーマとしては扱いやすかったです。

他の実装はどうか?というと、JCache RIにはそもそもトランザクションに関する機能はありません。そりゃそうですね。

Hazelcastは、Resource Adapterがあるみたいなので、その気になれば可能なのでしょうか…?

J2EE Integration
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#j2ee-integration

と思ってみてみたのですが、トランザクション対応しているのはMapやMultiMap、ListなどでCacheは現時点では対応してなさそうな雰囲気です。

Ehcacheも同じは?

About Transaction Support
http://ehcache.org/generated/2.10.0/html/ehc-all/#page/Ehcache_Documentation_Set%2Fco-tx_about_transaction_support.html%23

Apache Igniteは、独自路線な感じがします…。

Transactions
http://apacheignite.readme.io/docs/transactions

実装依存でなければ、標準化を待つ感じですね。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-jcache-cdi