CLOVER🍀

That was when it all began.

MarshallingがリファクタリングされたInfinispanを、Hot Rodで試す

これは、なにをしたくて書いたもの?

Infinispan 10.0で、Marshallingまわりがリファクタリングされ、ProtoStream(Protocol Buffers 2)がデフォルトのMarshalingの
仕組みになったということを、以前書きました。
※それまでは、JBoss Marshallingがデフォルトでした

Blog: Infinispan 10.0.0.Final - Infinispan

Infinispan 10.0でMarshallingがリファクタリングされたという話(Embedded Mode) - CLOVER🍀

なんですけど、この時はEmbeddedだけ試していて、Hot Rodの方を完全に忘れていたので、後にQuarkusで遊ぶ時に
「あれ?これやってなかったな?」という気になりました…。

ProtoStreamが使えるようになった、Quarkus+Infinispan Client(Hot Rod) Extensionを試す - CLOVER🍀

というわけで、今回はシンプルにHot Rodのみでやってみることにしました。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-76-generic", arch: "amd64", family: "unix"

Infinispan Serverは10.1.1.Finalを使用し、動作しているIPアドレスは172.17.0.2とします。

お題

基本的には、この時と同じで。

Infinispan 10.0でMarshallingがリファクタリングされたという話(Embedded Mode) - CLOVER🍀

RemoteCacheのエントリとして、String、そしてユーザー定義クラスを使った時の差を見てみたいと思います。

ユーザー定義クラスの方のお題は、書籍で。

準備

Maven依存関係は、こちら。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>10.1.1.Final</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.6.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.11.1</version>
            <scope>test</scope>
        </dependency>

JUnit 5とAssertJは、テストコード用です。

JUnit 5があるので、Maven Surefire Pluginのバージョンも指定しておきます。

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

では、ソースコードを書いていきます。

サンプルコードを書きつつ、動作確認

まず、テストコードの雛形から。
src/test/java/org/littlewings/infinispan/marshalling/MarshallingTest.java

package org.littlewings.infinispan.marshalling;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class MarshallingTest {
    @BeforeEach
    public void setUp() {
        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        .build();

        RemoteCacheManager manager = new RemoteCacheManager(configuration);

        manager.getCache("simpleCache").clear();
        manager.getCache("bookCache").clear();
    }

    <K, V> void withCache(String cacheName, Consumer<RemoteCache<K, V>> consumer) {
        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        .build();

        RemoteCacheManager manager = new RemoteCacheManager(configuration);

        RemoteCache<K, V> cache = manager.getCache(cacheName);

        try {
            consumer.accept(cache);
        } finally {
            cache.stop();
            manager.stop();
        }
    }

    // ここに、テストを書く!
}

Cacheは、「simpleCache」と「bookCache」という2つのものを使い、テストごとにデータを空っぽにします。

利用するCacheは、Infinispan Server側にあらかじめ作成しておきます。

ispn@infinispan-server:/opt/infinispan-server$ bin/cli.sh
[disconnected]> connect
[infinispan-server-17735@cluster//containers/default]> 
[infinispan-server-17735@cluster//containers/default]> create cache --template=org.infinispan.DIST_SYNC simpleCache
[infinispan-server-17735@cluster//containers/default]> create cache --template=org.infinispan.DIST_SYNC bookCache

では、テストケースを書きましょう。

まずは、Stringを使った単純なケース。

    @Test
    public void simpleCase() {
        this.<String, String>withCache("simpleCache", cache -> {
            cache.put("key1", "value1");
            cache.put("key2", "value2");
            cache.put("key3", "value3");

            assertThat(cache.get("key1")).isEqualTo("value1");
            assertThat(cache.get("key2")).isEqualTo("value2");
            assertThat(cache.get("key3")).isEqualTo("value3");
        });
    }

こちらは、特に問題なく動作します。

次に、ユーザー定義クラスを作成。お題は、書籍で。
src/test/java/org/littlewings/infinispan/marshalling/Book.java

package org.littlewings.infinispan.marshalling;

import java.io.Serializable;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    String isbn;

    String title;

    int price;

    public static Book create(String isbn, String title, int price) {
        Book book = new Book();

        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

Serializableなクラスとします。

で、このテストケースを実行すると、Marshallerがないと言われてエラーになります。

java.lang.IllegalArgumentException: No marshaller registered for Java type org.littlewings.infinispan.marshalling.Book
    at org.littlewings.infinispan.marshalling.MarshallingTest.lambda$useDefinedClassCase$1(MarshallingTest.java:72)
    at org.littlewings.infinispan.marshalling.MarshallingTest.lambda$useDefinedClassCase$2(MarshallingTest.java:72)
    at org.littlewings.infinispan.marshalling.MarshallingTest.withCache(MarshallingTest.java:41)
    at org.littlewings.infinispan.marshalling.MarshallingTest.useDefinedClassCase(MarshallingTest.java:63)

というわけでHot Rodを使った場合でも、ProtoStream用のMarshallerをなんとか用意しないといけないわけですね。

ドキュメントを見る

では、どうするか…はEmbeddedを見ているのでだいたいわかるのですが、ちょっとドキュメントを探しましょう。

EmbeddedのMarshallingまわりのドキュメント。

Marshalling

ProtoStream (Default)

StringがMarshallingでエラーにならないのは、デフォルトでStringやIntegerなどに対するMarsharinngが考慮されているからですね。

Usage

で、Embedded Modeの時はシリアライズ対象のクラスにProtoStreamが提供するアノテーションを付与し、
SerializationContextInitializerインターフェースのサブインターフェースを作成することになります。

結論から言うと、Hot Rodでも同じようなことをします。

Marshalling data

ProtoStream

まず、先ほどのBookクラスのフィールド(またはgetter)に、@ProtoFieldアノテーションを指定します。
src/test/java/org/littlewings/infinispan/marshalling/Book.java

package org.littlewings.infinispan.marshalling;

import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;

public class Book {
    @ProtoField(number = 1, required = true)
    String isbn;

    @ProtoField(number = 2, required = true)
    String title;

    @ProtoField(number = 3, required = true, defaultValue = "0")
    int price;

    @ProtoFactory
    public static Book create(String isbn, String title, int price) {
        Book book = new Book();

        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

@ProtoFactoryについては、必要に応じてコンストラクタやファクトリメソッドに付けましょう。

続いて、SerializationContextInitializerインターフェースのサブインターフェースを作成します。

Pluggable Annotation Processing APIを使うので、Maven依存関係を追加します。

        <dependency>
            <groupId>org.infinispan.protostream</groupId>
            <artifactId>protostream-processor</artifactId>
            <version>4.3.2.Final</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
            <optional>true</optional>
        </dependency>

両方ともoptionalで良いでしょう。

作成したSerializationContextInitializerインターフェースを拡張したインターフェース。
src/test/java/org/littlewings/infinispan/marshalling/BookContextInitializer.java

package org.littlewings.infinispan.marshalling;

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(
        includeClasses = {Book.class},
        schemaFileName = "book.proto",
        schemaFilePath = "proto/",
        schemaPackageName = "sample")
public interface BookContextInitializer extends SerializationContextInitializer {
}

まあ、@AutoProtoSchemaBuilderアノテーションを使いたいがために存在するのですが…。

Protocol Buffersのスキーマ定義を生成する対象のクラスとパッケージ名、ファイルの情報を指定します。また、合わせてMarshallerの
実装や、SerializationContextInitializerの実装も生成されます。

ビルドすると、こんな感じでMarshallerやSerializationContextInitializerの実装、Protocol Buffersのスキーマ定義が生成されたことが
確認できます。

$ find target/test-classes -type f
target/test-classes/proto/book.proto
target/test-classes/org/littlewings/infinispan/marshalling/MarshallingTest.class
target/test-classes/org/littlewings/infinispan/marshalling/BookContextInitializer.class
target/test-classes/org/littlewings/infinispan/marshalling/Book$___Marshaller_abf7b4d69b46e47579a90635aad0268de3c98b409eef1e9d735fea0f6d115580.class
target/test-classes/org/littlewings/infinispan/marshalling/BookContextInitializerImpl.class
target/test-classes/org/littlewings/infinispan/marshalling/Book.class

ちなみに、生成されたProtocol Bufferesのスキーマ定義はこんな感じになります。
target/test-classes/proto/book.proto

// File name: book.proto
// Generated from : org.littlewings.infinispan.marshalling.BookContextInitializer

syntax = "proto2";

package sample;



message Book {
   
   required string isbn = 1;
   
   required string title = 2;
   
   required int32 price = 3 [default = 0];
}

あとは、RemoteCacheManagerを作成する際のConfigurationに対して、自動生成されたSerializationContextInitializerのインスタンス
指定してあげればOKです。

    <K, V> void withCache(String cacheName, Consumer<RemoteCache<K, V>> consumer) {
        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        .addContextInitializer(new BookContextInitializerImpl())
                        .build();

        RemoteCacheManager manager = new RemoteCacheManager(configuration);

        RemoteCache<K, V> cache = manager.getCache(cacheName);

        try {
            consumer.accept(cache);
        } finally {
            cache.stop();
            manager.stop();
        }
    }

クラスのFQCNをStringで渡す、でも可です。

        Configuration configuration =
                new ConfigurationBuilder()
                        .addServers("172.17.0.2:11222")
                        //.addContextInitializer(new BookContextInitializerImpl())
                        .addContextInitializer("org.littlewings.infinispan.marshalling.BookContextInitializerImpl")
                        .build();

今回は使っていませんが、hotrod-client.propertiesファイルを使う場合は、以下のプロパティを使うことになるようです。

infinispan.client.hotrod.context-initializers=org.littlewings.infinispan.marshalling.BookContextInitializerImpl

https://github.com/infinispan/infinispan/blob/10.1.1.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/ConfigurationProperties.java#L29

ここまで修正すれば、ユーザー定義クラスを使ったMarshallingが成功するようになります。

まとめ

今回は、InfinispanのデフォルトのMarshallingの仕組みが変わったことを、Hot Rod Clientを使って動作確認してみました。

1度Embeddedで試していたのでそれほど困らなかったのですが、あえて言えば自動生成されたSerializationContextInitializerインターフェースの
実装を、どうやって指定させるのか?くらいですね。

幸い、ドキュメントに記載がありましたが。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-without-jboss-marshalling