CLOVER🍀

That was when it all began.

Javaのシリアライザーをいろいろ試してみる(Java標準、Kryo、MessagePack、Protocol Buffers、JBoss Marshalling)

少しシリアライズ関係のライブラリを目にする機会がありまして、そういえばこういうまとめ記事あったなぁということを思い出しました。

MessagePack、Kryo、Protocol Buffersなどのシリアライザーのパフォーマンス比較
http://blog.katty.in/4567

気にはなっていたものの、実際にこれらのライブラリを使ってコードを書いたことはなかったので(Protocol Buffersは除く)、いい機会だなと思い試してみました。

今回は、以下について書いていきます。

  • Java標準
  • Kryo
  • MassagePack
  • Protocol Buffers
  • JBoss Marshalling

最後の方にかなり個人的な趣向が入っていますが、気にしない方向で…。Java標準が入っているのは、とりあえずといった感じで。

ここから、簡単にシリアライザーごとにシリアライズ対象のクラスと、シリアライズ/デシリアライズのコードをテストコードとして(JUnit+AssertJ)書いていきます。

基本的には、各シリアライザーのドキュメントからコードを起こしていますが、ちょっと不安なものは先述の参照エントリに言及されていた以下の内容とか見ています。

thrift-protobuf-compare
https://github.com/eishay/jvm-serializers/wiki
https://github.com/eishay/jvm-serializers/tree/master/tpc/src/serializers

では、書いていきます。

Java標準

まずは、Javaに標準搭載されているものから。普通に、ObjectInputStream/ObjectOutputStreamを使うやつですね。

シリアライズ対象のクラス。
src/main/java/example/StandardBook.java

package example;

import java.io.Serializable;
import java.util.List;

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

    public String isbn;
    public String title;
    public int price;
    public List<String> tags;
}

クラスそのものや、フィールドとして定義されたメンバーがSerializableであればOKです。

シリアライズ/デシリアライズを確認するテストコード。
src/test/java/org/littlewings/serialization/JavaStandardTest.java

package org.littlewings.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;

import example.StandardBook;
import org.junit.Test;

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

public class JavaStandardTest {
    @Test
    public void testJavaStandardSerialization() throws IOException, ClassNotFoundException {
        StandardBook src = new StandardBook();
        src.isbn = "978-4774169316";
        src.title = "Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ";
        src.price = 1980;
        src.tags = new ArrayList<>(Arrays.asList("あなたと", "Java", "今すぐ", "ダウンロード!"));

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // シリアライズ
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(src);
        }

        byte[] binary = baos.toByteArray();

        // シリアライズ後のサイズ
        assertThat(binary)
                .hasSize(325);

        // デシリアライズ
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(binary))) {
            StandardBook dest = (StandardBook) ois.readObject();

            assertThat(dest.isbn)
                    .isEqualTo("978-4774169316");
            assertThat(dest.title)
                    .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
            assertThat(dest.price)
                    .isEqualTo(1980);
            assertThat(dest.tags)
                    .containsSequence("あなたと", "Java", "今すぐ", "ダウンロード!");
        }
    }
}

ObjectOutputStreamの各種writeメソッドでシリアライズ、ObjectInputStreamの各種readメソッドでデシリアライズです。

使い方としては、単純だと思います。

ただし、参照エントリのベンチマーク結果にもありましたが、シリアライズ後のサイズ大きめ、速度遅めです。

Kryo

参照エントリのベンチマークでも、かなり気になる結果を叩き出しているシリアライザー。

Kryo
https://github.com/EsotericSoftware/kryo

後で記載するMessagePackやProtocol Buffersとは異なり、Java向けのシリアライザーになります。

利用するためには、ライブラリの依存関係を書く必要がありますので、ここから先は必要に応じてMaven依存関係を書いていきます。

Maven依存関係。

        <dependency>
            <groupId>com.esotericsoftware</groupId>
            <artifactId>kryo</artifactId>
            <version>3.0.1</version>
        </dependency>

現在は、3系が最新のようです。

シリアライズ対象のクラス。
src/main/java/example/KryoBook.java

package example;

import java.util.List;

public class KryoBook {
    public String isbn;
    public String title;
    public int price;
    public List<String> tags;
}

今回はこのクラスをKryoのシリアライザーに放り込みましたが、ここについてはSerializableを実装しておく必要はないみたいです。

フィールドなどは、今回の例だとデフォルトのものが使われる、という感じなのでしょう。

Default serializers
https://github.com/EsotericSoftware/kryo#default-serializers

シリアライズ/デシリアライズを確認するテストコード。
src/test/java/org/littlewings/serialization/KryoTest.java

package org.littlewings.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import example.KryoBook;
import org.junit.Test;

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

public class KryoTest {
    @Test
    public void testKryoSerialization() {
        KryoBook src = new KryoBook();
        src.isbn = "978-4774169316";
        src.title = "Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ";
        src.price = 1980;
        src.tags = new ArrayList<>(Arrays.asList("あなたと", "Java", "今すぐ", "ダウンロード!"));

        Kryo kryo = new Kryo();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // シリアライズ
        try (Output output = new Output(baos)) {
            kryo.writeObject(output, src);
        }

        byte[] binary = baos.toByteArray();

        // シリアライズ後のサイズ
        assertThat(binary)
                .hasSize(169);

        // デシリアライズ
        try (Input input = new Input(new ByteArrayInputStream(binary))) {
            KryoBook dest = kryo.readObject(input, KryoBook.class);

            assertThat(dest.isbn)
                    .isEqualTo("978-4774169316");
            assertThat(dest.title)
                    .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
            assertThat(dest.price)
                    .isEqualTo(1980);
            assertThat(dest.tags)
                    .containsSequence("あなたと", "Java", "今すぐ", "ダウンロード!");
        }
    }
}

使い方自体は、けっこう単純です。

提供されているシリアライザーが足りなかったら、自分で作るかこの辺りから引っ張ってきたりするのでしょうか。

A project that provides kryo (v2) serializers for some jdk types and some external libs like e.g. joda time.
https://github.com/magro/kryo-serializers

説明がkryo (v2)となっていますが、Maven依存関係を見てみると一応3系にはなっていそうです…。

MessagePack

バイナリシリアライゼーションフォーマット、MessagePackのJava向け実装。

MessagePack for Java
https://github.com/msgpack/msgpack-java

ここでは、v7(0.7)系を扱います。

v7では、coreとjackson-dataformat-msgpackに分かれていて、最初に見たサンプルがcoreのみで書かれていたものでした。

https://github.com/msgpack/msgpack-java/blob/v07-develop/msgpack-core/src/main/java/org/msgpack/core/example/MessagePackExample.java

Packer/Unpackerを使ったもので、これは面倒そうだなぁと思ってちょっと試した後にjackson-dataformat-msgpackの内容をちゃんと見て、あ…って感じになってました…。Jacksonのモジュールとして使えるんですねぇ。

では、使ってみます。

Maven依存関係。

        <dependency>
            <groupId>org.msgpack</groupId>
            <artifactId>jackson-dataformat-msgpack</artifactId>
            <version>0.7.0-p8</version>
        </dependency>

シリアライズ対象のクラス。
src/main/java/example/MsgPackBook.java

package example;

import java.util.List;

public class MsgPackBook {
    public String isbn;
    public String title;
    public int price;
    public List<String> tags;
}

シリアライズ/デシリアライズを確認するテストコード。
src/test/java/org/littlewings/serialization/MsgPackTest.java

package org.littlewings.serialization;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import com.fasterxml.jackson.databind.ObjectMapper;
import example.MsgPackBook;
import org.junit.Test;
import org.msgpack.jackson.dataformat.MessagePackFactory;

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

public class MsgPackTest {
    @Test
    public void testMsgPackSerialization() throws IOException {
        MsgPackBook src = new MsgPackBook();
        src.isbn = "978-4774169316";
        src.title = "Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ";
        src.price = 1980;
        src.tags = new ArrayList<>(Arrays.asList("あなたと", "Java", "今すぐ", "ダウンロード!"));

        ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());

        // シリアライズ
        byte[] binary = objectMapper.writeValueAsBytes(src);

        // シリアライズ後のサイズ
        assertThat(binary)
                .hasSize(167);

        // デシリアライズ
        MsgPackBook dest = objectMapper.readValue(binary, MsgPackBook.class);

        assertThat(dest.isbn)
                .isEqualTo("978-4774169316");
        assertThat(dest.title)
                .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
        assertThat(dest.price)
                .isEqualTo(1980);
        assertThat(dest.tags)
                .containsSequence("あなたと", "Java", "今すぐ", "ダウンロード!");
    }
}

使い方は、JacksonのObjectMapperを使えばいいだけなので、割と簡単です。なお、JSONにはならないのでObjectMapper#writeValueAsStringとかすると例外が飛んできます。

なお、Packer/Unpackerを使って手動でシリアライズ/デシリアライズすると、こんな感じになりました。Jacksonと統合している方から見ると、Low LevelなAPIになりますね。

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try (MessagePacker packer = MessagePack.newDefaultPacker(baos)) {
            packer
                    .packString(src.isbn)
                    .packString(src.title)
                    .packInt(src.price)
                    .packArrayHeader(4)
                    .packString(src.tags.get(0))
                    .packString(src.tags.get(1))
                    .packString(src.tags.get(2))
                    .packString(src.tags.get(3));
        }

        byte[] binary = baos.toByteArray();

        assertThat(binary)
                .hasSize(144);

        try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(binary)) {
            MsgPackBook dest = new MsgPackBook();
            dest.isbn = unpacker.unpackString();
            dest.title = unpacker.unpackString();
            dest.price = unpacker.unpackInt();

            int size = unpacker.unpackArrayHeader();
            dest.tags = new ArrayList<>();

            IntStream
                    .range(0, size)
                    .forEach(i -> {
                        try {
                            dest.tags.add(unpacker.unpackString());
                        } catch (IOException e) {
                            throw new UncheckedIOException(e);
                        }
                    });

            assertThat(dest.isbn)
                    .isEqualTo("978-4774169316");
            assertThat(dest.title)
                    .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
            assertThat(dest.price)
                    .isEqualTo(1980);
            assertThat(dest.tags)
                     .containsSequence("あなたと", "Java", "今すぐ", "ダウンロード!");
        }

Protocol Buffers

Googleシリアライズデータフォーマット、Protocol BuffersのJava向け実装。

Protocol Buffers
https://developers.google.com/protocol-buffers/

Developer Guide
https://developers.google.com/protocol-buffers/docs/overview

Protocol Buffer Basics: Java
https://developers.google.com/protocol-buffers/docs/javatutorial

Java Generated Code
https://developers.google.com/protocol-buffers/docs/reference/java-generated

今回紹介しているシリアライザーで、唯一データ構造の定義ファイル(IDL)が必要なものです。

Maven依存関係。

        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>2.6.1</version>
        </dependency>

IDL。
src/main/resources/book.proto

// $ protoc --java_out=src/main/java src/main/resources/book.proto
package example;

// option java_package = "example";
option java_outer_classname = "ProtocolBuffersBook";

message BookData {
    required string isbn = 1;
    required string title = 2;
    required int32 price = 3;
    repeated string tags = 4;
}

コマンドや、書かなくても今回は結果が同じ部分はコメントアウトで書いています。

で、コメントにも書いていますが、このIDLからこういうコマンドでJavaクラスを生成します。

$ protoc --java_out=src/main/java src/main/resources/book.proto

できあがったクラス…は長いので省略…。

一部抜き出すと、このような結果になっています。
src/main/java/example/ProtocolBuffersBook.java

// Generated by the protocol buffer compiler.  DO NOT EDIT!
// source: src/main/resources/book.proto

package example;

public final class ProtocolBuffersBook {
  private ProtocolBuffersBook() {}
  public static void registerAllExtensions(
      com.google.protobuf.ExtensionRegistry registry) {
  }
  public static final class BookData extends
      com.google.protobuf.GeneratedMessage
      implements BookDataOrBuilder {
  // 〜省略〜
  }

  public static final class BookData extends
      com.google.protobuf.GeneratedMessage
      implements BookDataOrBuilder {
  // 〜省略〜
  }
  // 〜省略〜

シリアライズ/デシリアライズを確認するテストコード。
src/test/java/org/littlewings/serialization/ProtocolBuffersTest.java

package org.littlewings.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

import example.ProtocolBuffersBook;
import org.junit.Test;

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

public class ProtocolBuffersTest {
    @Test
    public void testProtocolBuffers() throws IOException {
        ProtocolBuffersBook.BookData.Builder bookBuilder = ProtocolBuffersBook.BookData.newBuilder();
        bookBuilder.setIsbn("978-4774169316");
        bookBuilder.setTitle("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
        bookBuilder.setPrice(1980);
        bookBuilder.addAllTags(Arrays.asList("あなたと", "Java", "今すぐ", "ダウンロード!"));

        ProtocolBuffersBook.BookData src = bookBuilder.build();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // シリアライズ
        src.writeTo(baos);

        byte[] binary = baos.toByteArray();

        // シリアライズ後のサイズ
        assertThat(binary)
                .hasSize(148);

        // デシリアライズ
        ProtocolBuffersBook.BookData dest = ProtocolBuffersBook.BookData.parseFrom(new ByteArrayInputStream(binary));

        assertThat(dest.getIsbn())
                .isEqualTo("978-4774169316");
        assertThat(dest.getTitle())
                .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
        assertThat(dest.getPrice())
                .isEqualTo(1980);
        assertThat(dest.getTagsList())
                .containsSequence("あなたと", "Java", "今すぐ", "ダウンロード!");
    }
}

Builderを使って組み上げたりと、自動生成されたコードを使うので雰囲気がちょっと他とは違いますね。

JBoss Marshalling

ちょっと個人的な興味で、今回試してみたシリアライザーです。

JBoss Marshalling
http://jbossmarshalling.jboss.org/

位置付け的にはJava標準のシリアライザーを改善したもの、という感じみたいです。

ただ、ちょっと困ったこととしてはドキュメントがほとんどなく、以下のドキュメントとInfinispanのソースコード、そして最初に紹介したベンチマークのコードを見ながら書いてみました。

Marshalling API quick start
https://docs.jboss.org/author/display/JBMAR/Marshalling+API+quick+start

あと、JBoss MarshallingにはRiverとSerialがあるようなのですが、両者の使い分けはよくわかっていません。Riverの方が速い?あとよく使われていそう?

Maven依存関係。

        <dependency>
            <groupId>org.jboss.marshalling</groupId>
            <artifactId>jboss-marshalling-river</artifactId>
            <version>1.4.10.Final</version>
        </dependency>

シリアライズ対象のクラス。
src/main/java/example/MarshallingBook.java

package example;

import java.io.Serializable;
import java.util.List;

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

    public String isbn;
    public String title;
    public int price;
    public List<String> tags;
}

JBoss Marshallingの場合は、対象がSerializableである必要があります。

シリアライズ/デシリアライズを確認するテストコード。
src/test/java/org/littlewings/serialization/JBossMarshallingRiverTest.java

package org.littlewings.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import example.MarshallingBook;
import org.jboss.marshalling.Marshaller;
import org.jboss.marshalling.MarshallerFactory;
import org.jboss.marshalling.Marshalling;
import org.jboss.marshalling.MarshallingConfiguration;
import org.jboss.marshalling.Unmarshaller;
import org.junit.Test;

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

public class JBossMarshallingRiverTest {
    @Test
    public void testJBossMarshallingRiver() throws IOException, ClassNotFoundException {
        MarshallingBook src = new MarshallingBook();
        src.isbn = "978-4774169316";
        src.title = "Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ";
        src.price = 1980;
        src.tags = new ArrayList<>(Arrays.asList("あなたと", "Java", "今すぐ", "ダウンロード!"));

        MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("river");

        MarshallingConfiguration configuration = new MarshallingConfiguration();

        configuration.setVersion(3);

        Marshaller marshaller = marshallerFactory.createMarshaller(configuration);
        Unmarshaller unmarshaller = marshallerFactory.createUnmarshaller(configuration);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // シリアライズ
        marshaller.start(Marshalling.createByteOutput(baos));
        marshaller.writeObject(src);
        marshaller.finish();

        byte[] binary = baos.toByteArray();

        // シリアライズ後のサイズ
        assertThat(binary)
                .hasSize(237);

        // デシリアライズ
        unmarshaller.start(Marshalling.createByteInput(new ByteArrayInputStream(binary)));
        MarshallingBook dest = unmarshaller.readObject(MarshallingBook.class);
        unmarshaller.finish();

        assertThat(dest.isbn)
                .isEqualTo("978-4774169316");
        assertThat(dest.title)
                .isEqualTo("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] ");
        assertThat(dest.price)
                .isEqualTo(1980);
        assertThat(dest.tags)
                .containsSequence("あなたと", "Java", "今すぐ", "ダウンロード!");
    }
}

例がほとんどないのですが、登場人物さえわかれば使うのはそう難しくなさそう…?

なお、今回使ったのはRiverですが、Serialに切り替えるにはMaven依存関係に以下を加えて

        <dependency>
            <groupId>org.jboss.marshalling</groupId>
            <artifactId>jboss-marshalling-serial</artifactId>
            <version>1.4.10.Final</version>
        </dependency>

MarshallerFactoryの取得部分をこう変えます。

        MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");

そして、MarshallingConfigurationのバージョン指定を外すか、「5」を指定します。

        // バージョン指定はなくてもよい。指定する場合は「5」のみ
        // configuration.setVersion(5);

一応、なんとか使えました。

終わりに

とりあえずこれだけ使ってみました。簡単にベンチマークを取ったりしたのですが、こちらとそう変わらない印象になりました。

MessagePack、Kryo、Protocol Buffersなどのシリアライザーのパフォーマンス比較
http://blog.katty.in/4567

Kryoがかなり速かったです。とはいえ、MessagePackもそう劣らない感じだったので、好みとか用途で選ぶ感じかなぁと。残念ながら、今回はProtocol Buffersだけ速度を見なかったのですが、たぶん速度でそう困ることはないのでしょう(以下参照)。

Protocol Buffersは遅い
http://frsyuki.hatenablog.com/entry/20081116/p1

あと、Java標準のシリアライザーの遅さとシリアライズ後のバイナリサイズの大きさはちょっと目立つ感じでしたね。他にシリアライザーが登場するのも、なるほどなぁと思いました。

今回、比較的単純なAPIをざっと舐めただけなので、どれのシリアライザーももう少しLow LevelなAPIなどあったりするので、そのあたりは全然試していません。ここから、必要に応じて追う感じですね。