先日、Payara 4.1.153がリリースされまして。
What's New in Payara Server 4.1.153 ?
http://www.payara.co.uk/whats-new-in-payara-server-41153
Payara Server 4.1.153 Release Notes
http://www.payara.co.uk/release_notes
新機能などの中で、個人的にちょっと気になった@NamedCacheを試してみます。「What's New in Payara Server 4.1.153 ?」内では、「JCache Injection」と書かれています。
では、ちょっと使ってみましょう。
今回もPayara Microを使うので、Payara Micro 4.1.153は以下からダウンロードしておきます。
Downloads
http://www.payara.co.uk/downloads
@NamedCacheとは?
こちらのIssueから追加されたもの、ってことなのかな。
CDI container should be able to inject Cache #188
https://github.com/payara/Payara/issues/188
CDIでCacheをインジェクションする際に、Cacheの見分けができない、どれをインジェクションしたらいいのかわからない、という状況を解決するために導入されたものってことでいいんでしょうか。
ドキュメントは、こちらに。
JCache (Payara 4.1.153) 3.2.1 Creating a custom Cache using Injection
https://github.com/payara/Payara/wiki/JCache-%28Payara-4.1.153%29#321-creating-a-custom-cache-using-injection
JCache (Payara 4.1.153) 4.1 NamedCache Annotation
https://github.com/payara/Payara/wiki/JCache-%28Payara-4.1.153%29#41-namedcache-annotation
パッと見た感じ、こういう使い方をしそうですね。
- @Injectと一緒に、@NamedCacheを付与してCacheをインジェクション
- インジェクションされるCacheは、@NamedCacheのcacheNameにより選択される
- Cacheが未存在だった場合は、@NamedCacheで指定したcacheNameのCacheが作成される
- @NamedCacheにはいくつかパラメーターがあり、生成されるCacheの設定が可能
このあたりの実装が行われているのが、このあたりです。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L82
@NamedCache自体は、こちら。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/impl/NamedCache.java#L34
いかんせんドキュメントがほぼこれだけなので、あとは自分で試しながら見ていきたいと思います。
@NamedCacheを使ったサンプルの実装
それでは、@NamedCache(fish.payara.cdi.jsr107.impl.NamedCache)を使ったサンプルを書いていきます。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>payara-micro-namedcache</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> </properties> <build> <finalName>payara-micro-namedcache</finalName> </build> <dependencies> <dependency> <groupId>fish.payara.extras</groupId> <artifactId>payara-micro</artifactId> <version>4.1.153</version> <scope>provided</scope> </dependency> </dependencies> </project>
今回は、JAX-RSを使って試します。
src/main/java/org/littlewings/hazelcast/JaxrsApplication.java
package org.littlewings.hazelcast; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("rest") public class JaxrsApplication extends Application { }
JAX-RSリソースクラス。
src/main/java/org/littlewings/hazelcast/CalcResource.java
package org.littlewings.hazelcast; import java.util.concurrent.TimeUnit; import javax.cache.Cache; 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 fish.payara.cdi.jsr107.impl.NamedCache; @Path("calc") @RequestScoped public class CalcResource { @NamedCache(cacheName = "myCache") @Inject private Cache myCache; @GET @Path("add") @Produces(MediaType.TEXT_PLAIN) public String add(@QueryParam("a") int a, @QueryParam("b") int b) throws Exception { String key = a + "+" + b; if (myCache.containsKey(key)) { return String.format("CacheName = %s, result = %d", myCache.getName(), myCache.get(key)); } else { TimeUnit.SECONDS.sleep(3); int result = a + b; myCache.put(key, result); return String.format("CacheName = %s, result = %d", myCache.getName(), result); } } }
「myCache」というCacheをインジェクションしています。
@NamedCache(cacheName = "myCache") @Inject private Cache myCache;
また、レスポンスにCacheの名前を含めるようにしているので、
return String.format("CacheName = %s, result = %d", myCache.getName(), myCache.get(key));
これらでも確認することにします。
return String.format("CacheName = %s, result = %d", myCache.getName(), result);
パッケージングして
$ mvn package
デプロイ。
$ java -jar payara-micro-4.1.153.jar --deploy target/payara-micro-namedcache.war
動作確認。
## 1回目 $ time curl 'http://localhost:8080/payara-micro-namedcache/rest/calc/add?a=3&b=5' CacheName = myCache, result = 8 real 0m3.524s user 0m0.008s sys 0m0.000s ## 2回目 $ time curl 'http://localhost:8080/payara-micro-namedcache/rest/calc/add?a=3&b=5' CacheName = myCache, result = 8 real 0m0.023s user 0m0.003s sys 0m0.003s
Cacheの名前も「myCache」になっていますし、OKそうですね。
とここまでだと簡単そうに見えるのですが…。
ハマったこと
簡単に使えるかな?と思い、テキトーにやったらけっこうハマりました…。
Cacheに型パラメーターを適用できない
最初、何も考えずにこういう定義をしていたら
@NamedCache(cacheName = "myCache") @Inject private Cache<String, Integer> myCache;
デプロイに失敗しました。
[2015-08-01T17:06:16.313+0900] [Payara 4.1] [SEVERE] [NCLS-CORE-00026] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1438416376313] [levelValue: 1000] [[ Exception during lifecycle processing org.glassfish.deployment.common.DeploymentException: CDI deployment failure:WELD-001408: Unsatisfied dependencies for type Cache<String, Integer> with qualifiers @Default at injection point [BackedAnnotatedField] @NamedCache @Inject private org.littlewings.hazelcast.CalcResource.myCache at org.littlewings.hazelcast.CalcResource.myCache(CalcResource.java:0)
このCacheにインジェクションできませんよ、と…。
で、ドキュメントをよくよく見ると、
@NamedCache(cacheName = "custom") @Inject Cache cache;
あー、raw typeですねー。
そもそも、JSR107Producer#createCacheの戻り値がraw typeのCacheでございます。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L77
というわけで、Cacheに型パラメーターは適用するな、と。
@NamedCacheのパラメーターとして、keyClassとvalueClassが指定できるのですが…それほど意味がなかったり…。
有効期限を設定できない
@NamedCacheでは、JCacheで使うCacheLoader、CacheWriter、ExpiryPolicyを使うためのファクトリクラスを設定できるようです。
今回は、有効期限を設定しようとexpiryPolicyFactoryClassを使おうと考えました。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/impl/NamedCache.java#L94
が、Classクラスがあるだけなので、何を設定していいのかがわかりません(笑)。
JSR107Producer#createCacheのソースを見ていて…
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L102
ExpiryPolicyを実装したクラスを作成すればいいのかなーと思い、実装。
src/main/java/org/littlewings/hazelcast/MyExpiryPolicy.java
package org.littlewings.hazelcast; import java.util.concurrent.TimeUnit; import javax.cache.expiry.Duration; import javax.cache.expiry.ExpiryPolicy; public class MyExpiryPolicy implements ExpiryPolicy { @Override public Duration getExpiryForCreation() { return new Duration(TimeUnit.SECONDS, 10); } @Override public Duration getExpiryForAccess() { return null; } @Override public Duration getExpiryForUpdate() { return null; } }
意味的には、エントリ登録後、10秒で有効期限切れするということになります。
では、これを使ってJAX-RSリソースクラスを実装。
src/main/java/org/littlewings/hazelcast/ExpiryCalcResource.java
package org.littlewings.hazelcast; import java.util.concurrent.TimeUnit; import javax.cache.Cache; import javax.cache.expiry.CreatedExpiryPolicy; import javax.cache.expiry.ExpiryPolicy; 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 fish.payara.cdi.jsr107.impl.NamedCache; @Path("expirycalc") @RequestScoped public class ExpiryCalcResource { @NamedCache(cacheName = "expiryCache", expiryPolicyFactoryClass = MyExpiryPolicy.class) @Inject private Cache expiryCache; @GET @Path("add") @Produces(MediaType.TEXT_PLAIN) public String add(@QueryParam("a") int a, @QueryParam("b") int b) throws Exception { String key = a + "+" + b; if (expiryCache.containsKey(key)) { return String.format("CacheName = %s, result = %d", expiryCache.getName(), expiryCache.get(key)); } else { TimeUnit.SECONDS.sleep(3); int result = a + b; expiryCache.put(key, result); return String.format("CacheName = %s, result = %d", expiryCache.getName(), result); } } }
では、デプロイして動作確認。
$ time curl 'http://localhost:8080/payara-micro-namedcache/rest/expirycalc/add?a=3&b=5' <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><title>Payara Micro #badassfish - Error report</title><style type="text/css"><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 500 - Internal Server Error</h1><hr/><p><b>type</b> Exception report</p><p><b>message</b>Internal Server Error</p><p><b>description</b>The server encountered an internal error that prevented it from fulfilling this request.</p><p><b>exception</b> <pre>javax.servlet.ServletException: java.lang.RuntimeException: Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy</pre></p><p><b>root cause</b> <pre>java.lang.RuntimeException: Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy</pre></p><p><b>root cause</b> <pre>java.lang.ClassNotFoundException: org.littlewings.hazelcast.MyExpiryPolicy</pre></p><p><b>note</b> <u>The full stack traces of the exception and its root causes are available in the Payara Micro #badassfish logs.</u></p><hr/><h3>Payara Micro #badassfish</h3></body></html> real 0m0.622s user 0m0.002s sys 0m0.005s
がっつりエラー…。
裏を見ると、Hazelcastがコケております。
[2015-08-01T17:15:53.606+0900] [Payara 4.1] [SEVERE] [] [com.hazelcast.cache.impl.operation.CacheContainsKeyOperation] [tid: _ThreadID=47 _ThreadName=hz.c30e3817-4f56-44ee-99cc-0368703760b2.partition-operation.thread-3] [timeMillis: 1438416953606] [levelValue: 1000] [[ [172.17.42.1]:5900 [dev] [3.5] Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy java.lang.RuntimeException: Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy at javax.cache.configuration.FactoryBuilder$ClassFactory.create(FactoryBuilder.java:134) at com.hazelcast.cache.impl.AbstractCacheRecordStore.<init>(AbstractCacheRecordStore.java:132) at com.hazelcast.cache.impl.CacheRecordStore.<init>(CacheRecordStore.java:61) at com.hazelcast.cache.impl.CacheService.createNewRecordStore(CacheService.java:77) at com.hazelcast.cache.impl.CachePartitionSegment$1.createNew(CachePartitionSegment.java:56) at com.hazelcast.cache.impl.CachePartitionSegment$1.createNew(CachePartitionSegment.java:53) at com.hazelcast.util.ConcurrencyUtil.getOrPutSynchronized(ConcurrencyUtil.java:40) at com.hazelcast.cache.impl.CachePartitionSegment.getOrCreateCache(CachePartitionSegment.java:76) at com.hazelcast.cache.impl.AbstractCacheService.getOrCreateCache(AbstractCacheService.java:120) at com.hazelcast.cache.impl.operation.AbstractCacheOperation.beforeRun(AbstractCacheOperation.java:68) at com.hazelcast.spi.impl.operationservice.impl.OperationRunnerImpl.run(OperationRunnerImpl.java:131) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.processOperation(OperationThread.java:154) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.process(OperationThread.java:110) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.doRun(OperationThread.java:101) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.run(OperationThread.java:76) Caused by: java.lang.ClassNotFoundException: org.littlewings.hazelcast.MyExpiryPolicy at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at javax.cache.configuration.FactoryBuilder$ClassFactory.create(FactoryBuilder.java:130) ... 14 more ]]
しかも、ClassNotFoundException…。
Caused by: java.lang.ClassNotFoundException: org.littlewings.hazelcast.MyExpiryPolicy
よく見ると、これはHazelcast内部の動作なんですよねー。しかも、アプリケーションとは別スレッド。
at com.hazelcast.spi.impl.operationservice.impl.OperationRunnerImpl.run(OperationRunnerImpl.java:131) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.processOperation(OperationThread.java:154) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.process(OperationThread.java:110) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.doRun(OperationThread.java:101) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.run(OperationThread.java:76)
ここで、WARファイルでデプロイしたクラスまでのClassLoaderの階層は、以下の様になっているようです。
org.glassfish.web.loader.WebappClassLoader org.glassfish.internal.api.DelegatingClassLoader java.net.URLClassLoader com.sun.enterprise.v3.server.APIClassLoaderServiceImpl$APIClassLoader sun.misc.Launcher$ExtClassLoader
つまり、今回作成した自前のExpiryPolicyはWebappClassLoaderでロードされていることになります。
これに対して、HazelcastのOperationThreadが動作している時のClassLoaderの階層は…
sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader
いたって普通。Hazelcastをロードしているの、Webアプリじゃないですからねぇ…。しかもリクエストを処理しているスレッドとは別スレッド…なんか詰んでいる感じが…。
※javax.cache.configuration.FactoryBuilder$ClassFactoryも、ContextClassLoaderは使うのですが
https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/configuration/FactoryBuilder.java#L128
とすると、CacheLoader、CacheWriterを使っても同じ目に遭いそうな気がします。
微妙…。
一応補足しておくと、@Injectを契機に動作するJSR107Producer#createCacheによるCacheの作成は成功します。その後の、Hazelcastの内部動作がついてこれない、と。
最初にうまく動いていたこちらのコードでは、特にWebアプリケーション内にあるClassを要求するわけではないので、うまくいくという話ですね。
@NamedCache(cacheName = "myCache") @Inject private Cache myCache;
こちらのコードでも、OperationThreadの動作自体は行われます。
なお、蛇足ですが…Payara MicroのJARに先ほどの自作のExpiryPolicyのClassを入れれば動作はしますが…
$ jar uvf payara-micro-4.1.153.jar -C target/payara-micro-namedcache/WEB-INF/classes org/littlengs/hazelcast/MyExpiryPolicy.class org/littlewings/hazelcast/MyExpiryPolicy.classを追加中です(入=822)(出=425)(48%収縮されました)
こんなこと、まずやりませんね!!
というわけで、忘れます。なお、今回はPayara Microで動かしていますが、Payara Serverを使って確認しても、やっぱりClassNotFoundExceptionになりました…。
Interceptorには無関係
今回追加された@NamedCacheは、あくまで@Injectに関わるものなので、CDIと連携するためのInterceptorの機能とは無関係です。
仮に、こういうクラスを書いたとしても
src/main/java/org/littlewings/hazelcast/CachedCalcResource.java
package org.littlewings.hazelcast; import java.util.concurrent.TimeUnit; import javax.cache.annotation.CacheResult; import javax.enterprise.context.RequestScoped; 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; @Path("cachedcalc") @RequestScoped public class CachedCalcResource { @GET @Path("add") @Produces(MediaType.TEXT_PLAIN) @CacheResult(cacheName = "calcCache") public int add(@QueryParam("a") int a, @QueryParam("b") int b) throws InterruptedException { TimeUnit.SECONDS.sleep(3); return a + b; } }
Cacheの設定を行う術がありません。
InterceptorまわりのCacheの取得方法は、依然としてCacheManager#createCacheおよびgetCacheだからです。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/impl/PayaraCacheResolverFactory.java#L49
この場合、EJBのSingleon+StarupやWebListenerなどで最初にCacheの定義を行う必要があるでしょう。
まとめ
Payara 4.1.153で追加された、@NamedCacheアノテーションを試してみました。
ちょっと、実際にこれがいろんなところで使われる状況は想像できませんが…。
Cacheって、インスタンスそのものをいろんなクラスに@Injectするものとは考えにくいですし、それがraw typeのままでもね…とちょっと思います。それに、(ClassLoaderの話は無視しても)Cacheの設定が可能とはいえ、同じ@NamedCacheを指定するものについては、全部のアノテーションの定義を揃えないと意味ないですし。
@NamedCacheに指定したパラメーターを使ってCacheを作成するのは、最初の1回だけなので…。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L94
Interceptorには効果がないことを考えると、Cacheの設定を行いたい場合には、やっぱりアプリケーションの初期化時になんとかしてしまうのかなーと思いました。
@NamedCacheを使って、Cacheの名前解決を行うことはできますが(ただし、raw type…)、@Qualifier書いてもいいのかなーとも思いました。
なんか、微妙な結論に…。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/payara-micro-namedcache