サイト内を検索

Javaのシリアライズを触ってみました

はじめに

こんにちは、ココネでサーバーエンジニアを務めているBです。
 
JavaのプロジェクトでDTOを定義する際、インターフェースのSerializableを使用することがあります。
最初は「そうなんだ」という感じで使用してきましたが、
「なんでする?」が気になり、調べたことをお話しできればと思います。
 

直列化(シリアライズ)とは?

Javaの直列化はJavaのオブジェクトまたはデータを別のJavaシステムでも利用できるようにbyte形式(stream of bytes)に変換するフォーマット変換技術です。
オブジェクトをファイルに保存したり、ネットワークを通じて送ったりするために使われます。
システム的にJVM(Java Virtual Machine)のメモリに格納している(ヒープまたはスタック)オブジェクトデータをbyte形式(stream of bytes)に変換してデータベースやファイルなどの外部ストレージに保存します。
それを別のシステムでファイルを読み込んで、デシリアライズを通じて、Javaのオブジェクトに変換してJVMメモリに格納します。

シリアライズの利用先

揮発性のあるキャッシュデータを永続的に保存する必要がある場合に利用できます。
例えば、JVMのメモリにのみ存在するオブジェクトデータがシステムが終了しても後で再利用される可能性がある場合、永続化(Persistence)しておくと良いでしょう。このような特性を活かしたJavaのシリアライズは実際に様々な場所で応用されています。例えば次のような場合があります。

  • セッション (Servlet Session)
    • 単にセッションをサーブレットメモリ上で運用する場合、シリアライズは不要ですが、セッションデータを保存し共有する必要がある場合にはシリアライズを利用します。
    • セッションデータをデータベースに保存する場合
    • Tomcatのセッションクラスタリングを通じて各サーバ間でデータ共有が必要な場合
  • キャッシュ (Cache)
    • データベースから取得したオブジェクトデータが他のモジュールでも必要な場合、再度DBに問い合わせするのではなく、オブジェクトをシリアライズしてメモリや外部ファイルに保存しておき、デシリアライズしてキャッシュデータとして利用可能です。
    • もちろん、Javaのシリアライズを利用してキャッシュを保存することはできます。
    • ただ、現在ではRedis、MemcachedなどのキャッシュDBを多く使用する傾向です。

 

シリアライズとJSON

このようにJavaシリアライズは、外部ファイルやネットワークを通じてクライアント間でオブジェクトデータを共有する時に使われます。
ですが、CSV、JSONという立派なデータフォーマットがあるのにわざわざシリアライズする必要があるのか疑問ですね。
実際にJSONはウェブだけではなく、ゲームでも設定ファイルとして使われていたり、データを送受信する時に汎用的に使われています。
そしてシリアライズはJavaプログラムのみ使えますが、JSON形式でオブジェクトデータを保存すると、GolangやJavaScriptでも汎用的に使えます。


 
では、なぜシリアライズは使われているのでしょうか。
シリアライズのメリットは次のようになります。

  1. Javaシステム開発に最適化されている
  2. Javaのリファレンスタイプに対して制約がなく、外部に送ることができる

例えば、int, double, stringやarrayのようなタイプはプログラミング言語で共通で使われるタイプのため、JSONでも十分相互利用できます。
ですが、Javaのコレクション、クラス、インターフェースまたはカスタムで作ったタイプなどは単純なファイルフォーマットだけですと限界があります。
ゆえにこれらを外部に送るために各データをマッチングさせるパーシング(parsing)が必要です。
シリアライズを利用するとしたら、Golang、JavaScriptなどのシステムで使うことはできませんが、シリアライズ条件さえ満たした状態で手間をかけず外部へ送れます。そしてデシリアライズで読み込むとデータタイプが自動で合わさるためJavaのクラスを再利用できます。パーシングする必要なく、シリアライズされた文字列をDBに保存して使ったりもします。
 
このようにシリアライズには幾つかのメリットがありますが、デメリットももちろん存在します。
そして、最近は汎用的なJSONを利用する傾向が増えています。よってJSONとシリアライズのどちらを用いるかに明確な正解はなく、「目的に合わせて適切に使おう」かと思います。
 

シリアライズ例

Java 基本(primitive)タイプやjava.io.Serializableインターフェースを継承したオブジェクトはシリアライズできます。

@Data
public static class Person implements Serializable {

    private static final long serialVersionUID = -6989571048017962213L;
    private String name;
    private String email;
    private int age;

    public Person(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
}

シリアライズ

シリアライズ(streamにオブジェクトをアウトプット)するためにjava.io.ObjectOutputStreamを使います。
オブジェクトがシリアライズされるとき、インスタンスフィールドのみ保存されます。staticやメソッドはシリアライズして保存しません。

public void serializeTest() {
    Person person = new Person("しろ", "white@cocone.com", 25);

    String fileName = "Person.obj";

    try (
        FileOutputStream fos = new FileOutputStream(fileName);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
    ) {

        // シリアライズ可能オブジェクトをbyte streamに変換してファイルとして保存
        oos.writeObject(person);

    } catch(IOException e) {
        e.printStackTrace();
    }
}

メソッドを実行するとファイルとして保存されることを確認できます。
ここではPerson.objとして保存しましたが、拡張子は”.txt”でも問題ありません。

 
ファイルを確認すると以下のように読めない文字形になっていることを確認できます。

デシリアライズ

デシリアライズ(streamからオブジェクトをインプット)するためにjava.io.ObjectInputStreamを使います。
ただし、シリアライズされたオブジェクトのクラスが外部クラスの場合、classpathに存在し、importされた状態である必要があります。

public void deserializeTest() {
    String fileName = "Person.obj";

    try (
        FileInputStream fis = new FileInputStream(fileName);
        ObjectInputStream ois = new ObjectInputStream(fis)
    ) {
        // byte streamをJavaオブジェクトに変換(キャスト)
        Person deserializedPerson = (Person) ois.readObject();
        System.out.println(deserializedPerson);

    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

デシリアライズでオブジェクトの初期化をせずにオブジェクトの情報をインスタンス化して使えるようになります。
実行結果は以下のようになります。




 

Javaシリアライズの注意点

シリアライズはメリットよりデメリットが多いので、使う時に考慮しないといけないことがあります。

  • シリアライズは容量が大きい
  • シリアライズはオブジェクトに保存されたデータだけではなく、タイプ、クラスメタデータを持っているので容量が大きいです。同じデータをJSONで保存するときより2倍以上の差が出ることがあります。
    よってDB、Cacheなどの外部に保存するとき、長期間保存する必要があるデータは避けた方が良いと思います。
  • セキュリティ観点でデシリアライズは危険
  • byte streamをデシリアライズするObjectInputStreamのreadObject()メソッドを呼ぶと、オブジェクトグラフがデシリアライズされてclasspath中にある全てのタイプのオブジェクトを作りますが、該当タイプのオブジェクトの中のコードを実行するので、第三者の悪意によってプログラムが攻撃範囲内に入ることになります。
    またはシリアライズで外部に送る過程で誰かによりbyteが変更され、デシリアライズする過程でインスタンスに危険な値を入れて不変性を破るような攻撃も可能です。デシリアライズはコンストラクターを使用せずにインスタンス化ができるためです。
    よって信頼できないデータをデシリアライズすることは危険なので、気を付ける必要があります。
     
    デシリアライズが必要な時はデシリアライズフィルターリング(ObjectInputFilter)を使用することも危険を防ぐための一つの方法だと思います。
    これはbyte streamがデシリアライズされる前にフィルターリングする機能で、クラス単位で特定クラスを受け入れるか拒否することができます。

 

最後に

いかがでしたでしょうか。
残念ながら、シリアライズはまだJavaエコシステムのいたるところで使用されています。
Javaシリアライズを使用するシステムを管理しなければならない場合、クリティカルなデメリットがあるため、JSONのようなクロスプラットフォームの構造化されたデータ表現にマイグレーションすることを検討しても良いと思います。
ただ、現実的な理由から、まだシリアライズできるクラスを作成したり、メンテナンスしたりしなければならない場合があるかもしれません。
シリアライズできるクラスを正しく、安全に、効率的に作成するには、かなりの注意が必要です。
 
本記事は以下を参考にして作成しました。
Effective Java 第3版
The Anatomy of Deserialization Attacks
Java Serialization and Deserialization

ENSAPIA Engineeringでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ENSAPIA Engineering 株式会社エンジニアの求人一覧

Tag

Category

Tag