Jackson でハイパフォーマンスな JSON 処理をするためのベストプラクティス (1)

More than 1 year has passed since last update.

Java で POJO などを JSON にシリアライズ、もしくは JSON からデシリアライズする場合は Jackson を使うのがもはや golden standard となっている今日この頃ですが、みなさまいかような Java JSON ライフをお過ごしでしょうか?

今回はそんな Jackson を速度性能的により効率的に扱う必要があり、その際に以下のサイトをだいぶ参考にさせていただいたので、今後のためにもその内容をざっくりとメモしておきます。

基本

1. 生成処理などが重たいオブジェクトは再利用しよう

データバインディングで利用する ObjectMapper や、ストリーミング処理で利用する JsonFactory は、特に再利用をするべきオブジェクトです。これらはそのオブジェクト生成が重い、というのが再利用をするべき理由の一つとなります。

また ObjectMapper においては、型ごとのシリアライザやデシリアライザのインスタンスを内部にキャッシュしています。このシリアライザ・デシリアライザのオブジェクトは、対象の型を最初に取り扱おうとするタイミングでオブジェクトを生成してキャッシュし、二回目以降はキャッシュされたオブジェクトを利用します。

そのため、JSON <-> POJO の変換処理の都度 ObjectMapper オブジェクトを生成していると、このキャッシュが有効利用されることなく毎回シリアライザ・デシリアライザのオブジェクトが生成されてしまいます。

幸いなことに ObjectMapperJsonFactory も、そのオブジェクトはスレッドセーフに扱える特徴を持っています。なので、これらのオブジェクトは何も気兼ねすることなく再利用をしていきましょう。

なお、ObjectReaderObjectWriter のオブジェクトも再利用できますが、これらは先の二つと比較すると、再利用によって得られるメリットは大きくありません。

2. クローズが必要なものは使い終わったらクローズしよう

JsonParserJsonGenerator のオブジェクトは、Closeable インタフェースを実装していることから分かるとおり、そのオブジェクトが不要になった際に close() メソッドを呼び出すことが勧められています。

なぜ close() メソッドを呼び出すべきなのかというと、これらのオブジェクトは内部に再利用可能なバッファを保持しており、close() メソッドを呼び出すことで、このバッファをリリースして他のオブジェクトが再利用できるようになるからです。

3. 入出力のオブジェクトはなるべく「生」に近い表現方法で取り扱おう

Jackson には、さまざまな種類の入出力オブジェクトを受け入れられるよう、多様なインタフェースが提供されています。でも、速度性能を追求する場合は、なるべく「生」に近い状態の入出力オブジェクトを取り扱うことが推奨されています。

例えば JSON -> POJO へのデシリアライズであれば、効率の良い順に入力オブジェクトの表現方法を列挙すると以下のようになります。

  1. byte[]
  2. InputStream
  3. Reader
  4. String

デシリアライズしようとしている JSON が byte[] の状態で用意されている… ということはあまりないかと思いますが、JSON が記録されているファイルを処理する場合であれば次のように、FileInputStream オブジェクトの状態で Jackson (ObjectMapper#readValue(InputStream) メソッド) に渡すのがよい、となります。

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.FileInputStream;
import java.util.Map;

public class JsonDeserializerDemo {
    public static void main(String[] args) throws Exception {
        try (FileInputStream is = new FileInputStream("path/to/file.json")) {
            new ObjectMapper().readValue(is, Map.class);
        }
    }
}

ここでポイントは、FileInputStream オブジェクトを InputStreamReader クラスや BufferedReader クラスで包んでいない点です。文字コードのデコードやバッファリングは、Jackson 内部で処理した方が JDK のそれよりも効率的だし、ましてやファイルの内容を文字列として読み込んでから ObjectMapper に渡すのは愚かだ、ということになります。

シリアライズの場合は、次の順に出力オブジェクトを取り扱うべき、となります。

  1. OutputStream
  2. Writer
  3. String

ここでも、シリアライズ結果を文字列として受け取るのはよろしくない、とされています。とは言えども、シリアライズした結果を SLF4J などのインタフェースを経由してログファイルに記録する… なんてケースだと、文字列化する以外に他ないと思うんですけどね。

4. 既定の設定からの変更は、本当に必要な場合にのみ変更しよう

Jackson は ON/OFF 設定可能なオプションが豊富に取り揃えられていますが、初期状態で速度性能が十分出るように、既定のオプション設定がなされています。特に速度性能を低下させるようなオプションは明示的に有効にする処理が必要となります。

5. 同じ JSON を何度も処理する場合でも、JSON のパース処理は 1 回に留めよう

ある一つの JSON に対して、その JSON を段階的に処理したい、という状況を考えます。例えば、とある JSON の構造が、データの書式的なものを定義的するセクションと、データそのもののセクションの二つからなると仮定します。

この場合、最初に書式が定義されているセクションだけを処理し、その後に書式情報を参考にしながらデータをデシリアライズするように二段階の処理構成をとりますが、この段階それぞれで同じ JSON をデシリアライズしたり、前段階で処理して残った部分だけを中間データ的に JSON にシリアライズし直したりするのは無駄になるわけです。

Jackson では、JSON を POJO にマッピングするだけでなく、JSON を中間データで表現する機能も有しています。特に JSON の構成要素をストリーム的に処理できればよい場合は TokenBuffer クラスを使うのが適しています。一方で、JSON を木構造的に操作したい場合は JsonNode クラスを利用するのがよいでしょう。JsonNode クラスの利用は TokenBuffer クラスに比べて速度性能的に若干劣るようですが、それでも二度三度同じ JSON をデシリアライズするよりはよい性能になります。

6. 連続する同じ POJO を読み込む場合は ObjectReader#readValues() を利用しよう

ObjectReader#readValue() のメソッドを複数回呼び出すよりも効率的です。

7. ObjectMapper よりも ObjectReader / ObjectWriter を利用しよう

ObjectReaderObjectWriter はどちらもスレッドセーフであり、また ObjectMapper が内部で処理しているような探索処理を回避するなどのおかげで ObjectMapper を使うよりも少し効率的です。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.