今の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-RS+CDIで簡単な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つがあります。
- NONE … トランザクション非対応
- NON_XA … javax.transaction.Synchronizationを使ってトランザクションに参加する(これを今回利用)
- NON_DURABLE_XA … XAに対応したTransactional Cacheとする(リカバリは無効)
- FULL_XA … リカバリも有効にした、XA対応のTransactional Cacheとする
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
Transactions
http://apacheignite.readme.io/docs/transactions
実装依存でなければ、標準化を待つ感じですね。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-jcache-cdi