今日、JCacheについて、こんな質問を受けまして。
「Hazelcast ClientをJCacheの実装に使って値に独自のクラスを入れる時、サーバー側にも独自のクラスをデプロイしないといけないの?」
なんでこんなことを聞かれたかって、実際にやってみたらサーバー側で「クラスが見つからない」とエラーになったと言います。
ただ、Client/Server構成のグリッドの使い方から考えると、これでエラーになるのはちょっと不便なので、何が起こっているのか調べてみることにしました。
というわけでコード的にはJCacheの話ですが、内容としてはHazelcast ClientをJCacheの実装として採用したケースの話になりますのでその点についてはご注意ください。
準備
Client/Server構成となるので、両方のプログラムが必要です。
Server側は非常に単純に作ったので、あとで載せます。
Client側はふつうに作るので、まずはMaven依存関係から記載。
<!-- JCache --> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-client</artifactId> <version>3.6.2</version> </dependency> <!-- for Testing --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.4.1</version> <scope>test</scope> </dependency>
JCache APIと、JCacheの実装としてHazelcast Clientを追加。Hazelcastの場合、Hazelcast Clientがクラスパス上に存在していた場合、デフォルトでClientとして起動します。
独自のクラスを作成
なんでもいいので、単純なSerializableなクラスを作ります。今回は、こんなのにしました。
src/main/java/org/littlewings/jcache/hazelcast/MyValueClass.java
package org.littlewings.jcache.hazelcast; import java.io.Serializable; public class MyValueClass implements Serializable { private static final long serialVersionUID = 1L; private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
はい。
Server側
ここで、Server側を。こちらは、Groovyで超簡易に作成しました。
server.groovy
@Grab('javax.cache:cache-api:1.0.0') @Grab('com.hazelcast:hazelcast:3.6.2') import com.hazelcast.core.Hazelcast def hazelcast = Hazelcast.newHazelcastInstance() System.console().readLine("> Enter stop Hazelcast server.") hazelcast.lifecycleService.shutdown()
これだけです。
とりあえず、このServerに起動していてもらいます。クラスパスなどには、一切追加は行いません。
$ groovy server.groovy 5 19, 2016 9:46:46 午後 com.hazelcast.config.XmlConfigLocator 情報: Loading 'hazelcast-default.xml' from classpath. 5 19, 2016 9:46:46 午後 com.hazelcast.instance.DefaultAddressPicker 情報: [LOCAL] [dev] [3.6.2] Prefer IPv4 stack is true. 5 19, 2016 9:46:46 午後 com.hazelcast.instance.DefaultAddressPicker 情報: [LOCAL] [dev] [3.6.2] Picked Address[172.17.0.1]:5701, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5701], bind any local is true 5 19, 2016 9:46:46 午後 com.hazelcast.system 情報: [172.17.0.1]:5701 [dev] [3.6.2] Hazelcast 3.6.2 (20160405 - 0f88699) starting at Address[172.17.0.1]:5701 5 19, 2016 9:46:46 午後 com.hazelcast.system 情報: [172.17.0.1]:5701 [dev] [3.6.2] Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved. 5 19, 2016 9:46:46 午後 com.hazelcast.system 情報: [172.17.0.1]:5701 [dev] [3.6.2] Configured Hazelcast Serialization version : 1 5 19, 2016 9:46:46 午後 com.hazelcast.spi.OperationService 情報: [172.17.0.1]:5701 [dev] [3.6.2] Backpressure is disabled 5 19, 2016 9:46:46 午後 com.hazelcast.spi.impl.operationexecutor.classic.ClassicOperationExecutor 情報: [172.17.0.1]:5701 [dev] [3.6.2] Starting with 4 generic operation threads and 8 partition operation threads. 5 19, 2016 9:46:47 午後 com.hazelcast.instance.Node 情報: [172.17.0.1]:5701 [dev] [3.6.2] Creating MulticastJoiner 5 19, 2016 9:46:47 午後 com.hazelcast.core.LifecycleService 情報: [172.17.0.1]:5701 [dev] [3.6.2] Address[172.17.0.1]:5701 is STARTING 5 19, 2016 9:46:47 午後 com.hazelcast.nio.tcp.nonblocking.NonBlockingIOThreadingModel 情報: [172.17.0.1]:5701 [dev] [3.6.2] TcpIpConnectionManager configured with Non Blocking IO-threading model: 3 input threads and 3 output threads 5 19, 2016 9:46:49 午後 com.hazelcast.cluster.impl.MulticastJoiner 情報: [172.17.0.1]:5701 [dev] [3.6.2] Members [1] { Member [172.17.0.1]:5701 this } 5 19, 2016 9:46:49 午後 com.hazelcast.core.LifecycleService 情報: [172.17.0.1]:5701 [dev] [3.6.2] Address[172.17.0.1]:5701 is STARTED > Enter stop Hazelcast server.
テストコード
それでは、このServerに対してJCacheのAPIでアクセスするテストコードを書きます。
src/test/java/org/littlewings/jcache/hazelcast/CacheClientTest.java
package org.littlewings.jcache.hazelcast; import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.Caching; import javax.cache.configuration.Configuration; import javax.cache.configuration.MutableConfiguration; import javax.cache.spi.CachingProvider; import com.hazelcast.nio.serialization.HazelcastSerializationException; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class CacheClientTest { // ここに、テストを書く! }
まず最初に、JCache APIを真っ当に使って書いてみます。だいたいこんな感じのコードになると思います。
@Test public void failedCreateCacheWithMyDefinedClass() { try (CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager()) { Configuration<String, MyValueClass> configuration = new MutableConfiguration<String, MyValueClass>() .setTypes(String.class, MyValueClass.class); assertThatThrownBy(() -> manager.createCache("mySimpleCache", configuration)) .isInstanceOf(HazelcastSerializationException.class) .hasMessage("java.lang.ClassNotFoundException: org.littlewings.jcache.hazelcast.MyValueClass"); } }
ところが、テストコードが示しているように、このコードは実行に失敗します。しかも、シリアライズのエラー、その原因としてClassNotFoundExceptionと言われるわけです。
この時、スタックトレースとしてはこのような状態になります。
com.hazelcast.nio.serialization.HazelcastSerializationException: java.lang.ClassNotFoundException: org.littlewings.jcache.hazelcast.MyValueClass at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$ClassSerializer.read(JavaDefaultSerializers.java:181) at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$ClassSerializer.read(JavaDefaultSerializers.java:169) at com.hazelcast.internal.serialization.impl.StreamSerializerAdapter.read(StreamSerializerAdapter.java:46) at com.hazelcast.internal.serialization.impl.AbstractSerializationService.readObject(AbstractSerializationService.java:214) at com.hazelcast.internal.serialization.impl.ByteArrayObjectDataInput.readObject(ByteArrayObjectDataInput.java:600) at com.hazelcast.config.CacheConfig.readData(CacheConfig.java:543) at com.hazelcast.internal.serialization.impl.DataSerializer.read(DataSerializer.java:121) at com.hazelcast.internal.serialization.impl.DataSerializer.read(DataSerializer.java:47) at com.hazelcast.internal.serialization.impl.StreamSerializerAdapter.read(StreamSerializerAdapter.java:46) at com.hazelcast.internal.serialization.impl.AbstractSerializationService.toObject(AbstractSerializationService.java:170) at com.hazelcast.spi.impl.NodeEngineImpl.toObject(NodeEngineImpl.java:234) at com.hazelcast.client.impl.protocol.task.cache.CacheCreateConfigMessageTask.prepareOperation(CacheCreateConfigMessageTask.java:46) at com.hazelcast.client.impl.protocol.task.AbstractPartitionMessageTask.processMessage(AbstractPartitionMessageTask.java:58) at com.hazelcast.client.impl.protocol.task.AbstractMessageTask.initializeAndProcessMessage(AbstractMessageTask.java:118) at com.hazelcast.client.impl.protocol.task.AbstractMessageTask.run(AbstractMessageTask.java:98) at com.hazelcast.spi.impl.operationservice.impl.OperationRunnerImpl.run(OperationRunnerImpl.java:127) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.processPartitionSpecificRunnable(OperationThread.java:159) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.process(OperationThread.java:142) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.doRun(OperationThread.java:124) at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.run(OperationThread.java:99) at ------ End remote and begin local stack-trace ------.(Unknown Source) at com.hazelcast.client.spi.impl.ClientInvocationFuture.resolveResponse(ClientInvocationFuture.java:128) at com.hazelcast.client.spi.impl.ClientInvocationFuture.get(ClientInvocationFuture.java:95) at com.hazelcast.client.spi.impl.ClientInvocationFuture.get(ClientInvocationFuture.java:74) at com.hazelcast.client.spi.impl.ClientInvocationFuture.get(ClientInvocationFuture.java:37) at com.hazelcast.client.cache.impl.HazelcastClientCacheManager.createConfig(HazelcastClientCacheManager.java:188) at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.createCacheInternal(AbstractHazelcastCacheManager.java:116) at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.createCache(AbstractHazelcastCacheManager.java:145) at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.createCache(AbstractHazelcastCacheManager.java:63) at org.littlewings.jcache.hazelcast.CacheClientTest.failedCreateCacheWithMyDefinedClass(CacheClientTest.java:20) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) Caused by: java.lang.ClassNotFoundException at com.hazelcast.client.impl.protocol.ClientExceptionFactory$12.createException(ClientExceptionFactory.java:168) at com.hazelcast.client.impl.protocol.ClientExceptionFactory.createException(ClientExceptionFactory.java:613) at com.hazelcast.client.impl.protocol.ClientExceptionFactory.createException(ClientExceptionFactory.java:580) at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.handleClientMessage(ClientInvocationServiceSupport.java:314) at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.process(ClientInvocationServiceSupport.java:296) at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.doRun(ClientInvocationServiceSupport.java:289) at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.run(ClientInvocationServiceSupport.java:266)
ちなみにこれ、以下の部分より上はServer側で生成されたスタックトレースです。
at ------ End remote and begin local stack-trace ------.(Unknown Source)
その内容が、Client側にも現れます。この時、Server側は一切エラーを吐きません。
なので、ClientとServerのスタックトレースが交互に現れた状態になっています。で、よく見るとCacheManager#createCacheでコケているようですね。
なにが起こったのか?
これで、なにが起こっているのかを説明しておきます。
CacheManager#createCacheを呼び出した時、Hazelcast ClientによるJCache実装は、その定義内容をServer側へシリアライズして送信します。
Data cacheConfigData = clientContext.getSerializationService().toData(config);
Future<ClientMessage> future = clientInvocation.invoke(); if (syncCreate) { final ClientMessage response = future.get();
Server側は、この内容を受け取りCacheを定義しようとします。
この時、定義内容をデシリアライズしようとするわけですが、送信内容にMutableConfiguration#setTypesした時のClassクラスが含まれています。
Configuration<String, MyValueClass> configuration = new MutableConfiguration<String, MyValueClass>() .setTypes(String.class, MyValueClass.class);
今回は、MyValueClassというクラスです。
で、これをServer側でデシリアライズする時にClassクラスをロードしちゃうわけですね。
//SUPER
keyType = in.readObject();
valueType = in.readObject();
結果、ClassNotFoundExceptionとなるわけです。Client側には、シリアライズ時の処理で失敗したということで、まとめて報告されることになります。
回避方法を考える
というわけで、このままでは使えません。
もちろんServer側のクラスパスに自作のクラスを加えれば問題は解消できますが、Client/Server構成であることを考えると、その案は却下としたいところです。
Client/Server構成だからといって、CacheManager#createCacheせずに、いきなりCacheManager#getCacheしてみても、これはうまくいきません。
@Test public void notCreateCacheIsNull() { try (CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager(); Cache<String, MyValueClass> cache = manager.getCache("mySimpleCache")) { assertThat(cache).isNull(); // CacheManager#createCacheしない場合は、null } }
多くのJCacheのCacheManagerの実装は、createCacheした時のCacheのインスタンスを内部的にMapで持っていることが多いみたいで、Hazelcastもそのひとつになります。で、Client側のCacheManagerにその定義が見つからない、と…。
というわけで、CacheManager#createCacheは呼ぶ必要があるわけです。
となると、まあ泥縄的な案しかないわけですが。
結果としては、MutableConfiguration#setTypesで独自の型を指定するのをやめると回避することができます。。
@Test public void workaround() { Configuration<String, MyValueClass> configuration = new MutableConfiguration<String, MyValueClass>(); // .setTypes(String.class, MyValueClass.class); // <- 削除 try (CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager(); Cache<String, MyValueClass> cache = manager.createCache("mySimpleCache", configuration)) { MyValueClass value = new MyValueClass(); value.setValue("Hello JCache!!"); cache.put("key1", value); assertThat(cache.get("key1").getValue()) .isEqualTo("Hello JCache!!"); cache.remove("key1"); assertThat(cache.containsKey("key1")).isFalse(); } }
この場合、キーと値の型にObjectを指定したのと同義になります。
もしくは、Java SE標準APIの範囲でならsetTypesしてもいいですが…。
一応、この実装方法をとってもCache自体はタイプセーフを保つことはできます。
そもそもMutableConfiguration#setTypesで指定した型をいつ使うかですが、CacheManager#getCacheする時に追加の引数として使うもので(取得しようとしているCache定義の検証として)、指定しなくても動かないなんてことはありません。
ですが、極力指定すべきだとは思いますけどねぇ…。
かなり実装を透かして見た感じになりますが、ワークアラウンドでした。
ところで
他のClient/Server構成を取れる製品だと、どうなのでしょう?
Infinispanでは、MUtableConfiguration#setTypesで独自の型を指定しても、Serverへのデプロイなどなしのままで問題なく動きました。
まとめ
というわけで、Hazelcast ClientをJCache実装に採用した場合に、シリアライズでエラーになるケースのひとつ(他は知りませんが)の回避策っぽいものを書いてみました。
が、個人的にはちょっと微妙です。MutableConfiguration#setTypesでハマるとは、たぶん使う側は考えないでしょうし、ここは本来指定すべきだと思いますからねぇ…。