GSON それに少し厳密を追加し、大きなJSONファイルを処理するときのメモリオーバーフローの問題を解決します

多くの場合、多くの人がGoogleからGSONライブラリに出会いました。これはJSONファイルをJavaオブジェクトに、またはその逆に簡単に変換します。



それに出会わなかった人のために、私はネタバレの下に短い説明を用意しました。 彼はまた、彼の仕事で実際に遭遇した2つの問題のGSONに関する解決策について説明しました(解決策は必ずしも最適でも最良でもありませんが、誰かにとって役に立つかもしれません)。



1)JSONファイルから単一のフィールドが失われていないことを確認し、Javaクラスのすべての必須フィールドが埋められていることを確認します(GSONをより厳密にします)。

2)メモリ不足エラーを回避するために非常に大きなJSONファイルを処理する必要がある場合、GSONを使用した手動解析。



だから、手始めに、あなたの指のGSONは何ですか:...

GSONについてすでに知っている人はおそらく興味がないでしょう。スキップできます。
... GSONでは、文字通り2行でJSONをJavaオブジェクトに変換できます。 さまざまなプラットフォームとシステム間の統合、シリアライゼーションとデシリアライゼーション、およびJavaScript WebパーツとJavaバックエンド間の相互作用に非常によく使用されます。



そのため、このようなjsonは別のアプリケーションから受信したと言います。



{ "summary": { "test1_id": "1444415", "test2_id": "4444935" }, "results": { "details": [ { "test1_id": "1444415", "test2_id": "4444935" }, { "test1_id": "1444415", "test2_id": "4444935" } ] } }
      
      





Javaオブジェクトで同様の構造を説明します(ゲッターやセッターなど。簡単にするために記述しません)。



  static class JsonContainer { DataContainer summary; ResultContainer results; } static class ResultContainer { List<DataContainer> details; } static class DataContainer { String test1_id; String test1_id; }
      
      







文字通り2行で、一方を他方に変換します。



  Gson gson = new GsonBuilder().create(); JsonContainer jsonContainer = gson.fromJson(json, JsonContainer.class);//  Json  Java ... // -  String json = gson.toJson(jsonContainer);//   Java  json
      
      







ご覧のとおり、すべてが非常に簡単です。 Javaに不適切な名前が気に入らない場合は、SerializedNameアノテーションを使用します。つまり、次のように記述します。



  static class JsonContainer { DataContainer summary; ResultContainer results; } static class ResultContainer { List<DataContainer> details; } static class DataContainer { @SerializedName("test1_id") String test1Id; @SerializedName("test2_id") String test2Id; }
      
      





当然、Stringはフィールドタイプとして自動的に使用できるだけでなく、プリミティブ型とそのラッパー、列挙型、日付(日付形式を設定可能)、ジェネリックを持つオブジェクトなども使用できます。 jsonの値が列挙定数名と一致しない場合、列挙値にSerializedNameを指定することもできます。 もちろん、次のように、個々のクラスに独自のハンドラーを追加することもできます。



 Gson gson = new GsonBuilder().registerTypeAdapter(DataContainer.class, new DataContainerDeserializer<DataContainer>()).create(); class DataContainerDeserializer<T> implements JsonDeserializer<T> { @Override public T deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { ... //   JsonElement     return /*   Java  */ } }  ,     JsonDeserializer .  ,   GSON'        .
      
      









問題番号1.ライト変換



jsonからJavaに変換するとき、GSONはJavaクラスにないすべてのフィールドを無視し、NotNullアノテーションに注意を払いません。 多くの目的(たとえば、シリアル化/逆シリアル化中のクラスの進化)のために、特に問題はないように見えますが、無視して無視します。 はい、時々本当に便利です。 しかし、別の会社のシステムと統合し、オブジェクトフィールドが突然オブジェクトに変わったと想像してみてください(開発者の誤った「反対側」、レジストリシステムの設計で「夜12時以降にキャリッジがカボチャ、つまりフィールドフィールド1に変わったため」他の百万の理由により、field2になります)。 または彼らは重要なフィールドを追加しましたが、私たちに伝えるのを忘れていました。 さらに悪いことに、統合が両方の方法で機能する場合:システムAが余分なフィールドを持つオブジェクトを送信しました(これについては知りませんでした)。データベースに追加し、理由により余分なフィールドを削除したと判断しました。 すべてが稼働中の台無しにされた電話であり、QAや分析を片側でキャッチできるか、またはキャッチできない場合があります。



GSON自体で通常の解決策を見つけることができませんでした。それをより厳密にする方法。 はい、jsonスキームを使用して個別の検証を固定するか、何らかの方法で検証を手動で行うことができましたが、GSON自体の機能、つまりJsonDeserializerをValidatorに変えて使用する方が良いと思われました(誰かがあなたに最善の方法を教えてくれるかもしれません)クラス自体:



素晴らしいソースコード
 package com.test; import com.google.common.collect.ObjectArrays; import com.google.gson.*; import com.google.gson.annotations.SerializedName; import gnu.trove.set.hash.THashSet; import javax.validation.constraints.NotNull; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.*; public class TestGson { private static String json = "{\n" + " \"summary\": {\n" + " \"test1_id\": \"1444415\",\n" + " \"test2_id\": \"4444935\"\n" + " },\n" + " \"results\": {\n" + " \"details\": [\n" + " {\n" + " \"test1_id\": \"1444415\",\n" + " \"test2_id\": \"4444935\"\n" + " },\n" + " {\n" + " \"test1_id\": \"1444415\",\n" + " \"test2_id\": \"4444935\"\n" + " }\n" + " ]\n" + " }\n" + "}"; public static void main(String [ ] args) { Gson gson = new GsonBuilder() .registerTypeAdapter(DataContainer.class, new VaidateDeserializer<DataContainer>()) //     DataContainer .create(); JsonContainer jsonContainer = gson.fromJson(json, JsonContainer.class); } static class JsonContainer { DataContainer summary; ResultContainer results; } static class ResultContainer { List<DataContainer> details; } static class DataContainer { @NotNull @SerializedName("test1_id") String test1Id; @SerializedName("test2_id") String test2Id; } static class VaidateDeserializer<T> implements JsonDeserializer<T> { private Set<String> fields = null; //      private Set<String> notNullFields = null; //       NotNull private void init(Type type) { Class cls = (Class) type; Field[] fieldsArray = ObjectArrays.concat(cls.getDeclaredFields(), cls.getFields(), Field.class); //     (, ,        fields = new THashSet<String>(fieldsArray.length); notNullFields = new THashSet<String>(fieldsArray.length); for(Field field: fieldsArray) { String name = field.getName().toLowerCase(); //     Annotation[] annotations = field.getAnnotations(); //     boolean isNotNull = false; for(Annotation annotation: annotations) { if(annotation instanceof NotNull) { //     NotNull isNotNull = true; } else if(annotation instanceof SerializedName) { name = ((SerializedName) annotation).value().toLowerCase(); //   SerializedName        fields  notNullFields } } fields.add(name); if(isNotNull) { notNullFields.add(name); } } } @Override public T deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { if(fields == null) { init(type); //            } Set<Map.Entry<String, JsonElement>> entries = json.getAsJsonObject().entrySet(); Set<String> keys = new THashSet<String>(entries.size()); for (Map.Entry<String, JsonElement> entry : entries) { if(!entry.getValue().isJsonNull()) { //   json,    null keys.add(entry.getKey().toLowerCase()); //       json } } if (!fields.containsAll(keys)) { //    json,    Java  -  throw new JsonParseException("Parse error! The json has keys that isn't found in Java object:" + type); } if (!keys.containsAll(notNullFields)) { //   Java    NotNull,   json   -  throw new JsonParseException("Parse error! The NotNull fields is absent in json for object:" + type); } return new Gson().fromJson(json, type); //     GSON } } }
      
      







実際に私たちがしていること。 コメントはすべてを十分詳細に説明していますが、一番下の行は、チェックするクラス(またはすべてのクラス)にJsonDeserializerを割り当てることです。 リフレクションを使用して初めてア​​クセスするとき、jsonに余分なフィールドがあるか、NotNullとしてマークされたフィールドがないことをJsonParseExceptionから見つけた場合、クラスの構造とフィールドへの注釈を上げます(それらは既に保存されており、リフレクションに時間を無駄にしません)。 当然、本番環境では、ログまたは別のコレクションにエラーを書き込むと、より穏やかに落ちる可能性があります。 いずれにせよ、私たちはすぐに「彼らは間違った蜂であり、間違った蜂蜜を与えている」ことを発見し、重要なデータを失う時間があるまで何かを変えることができます。 しかし、GSONは厳密に機能します。



問題番号2。大きなファイルとメモリオーバーフロー



私の知る限り、GSONはメモリ内のすべてのデータを一度に受信します。つまり、fromJsonを作成すると、メモリ内のjson構造全体を含む重いオブジェクトが取得されます。 jsonファイルは小さいですが、これは問題ではありませんが、数百万個のオブジェクトの配列が突然現れた場合、メモリ不足になる危険があります。 もちろん、GSONを放棄し、2つの異なるJSON解析ライブラリを使用してプロジェクトで作業することは可能ですが(何らかの理由で私はしたくないでしょう)、幸いなことにgson.stream.JsonReaderがあり、一度にすべてをダウンロードせずにトークンでJSONを解析できますメモリ内(および何らかの形式でディスクにドロップしたり、結果をデータベースに定期的に書き込んだりしましょう)。 実際、GSON自体はJsonReaderで動作します。 JsonReaderを使用するための一般的なアルゴリズムも非常に簡単です(特にjavadoc'e JsonReaderには優れた使用例があるため、ここではすべてが特定のjsonの構造に依存するため、作業の本質のみを簡単に説明します)。



 JsonReader jsonReader = new JsonReader(reader); //   reader,  fileReader,     json   ,
      
      





jsonReaderには次のメソッドがあります。



 - hasNext() -      (, ,   ..) - peek() -    (, ,       ..) - skipValue -   - beginObject(), beginArray() -    /      - endObject(), endArray() -   /      - nextString() -       -  ..
      
      







hasNext()は、ファイル全体ではなく、現在のオブジェクト/配列の値のみを返すことに注意してください(これは私にとっては予想外のことでした)。また、常にpeek()を使用してトークンのタイプを慎重に確認する必要があります。 そうしないと、この方法で大きなファイルを解析することは、単にfromJson()コマンドの1つよりも多少不便になりますが、それにもかかわらず、単純なjson構造の場合、文字通り数時間で記述されます。 メモリに重いオブジェクトをロードせずにGSONを部分的にファイルで動作させる最良の方法を知っている場合は、コメントを書いてください、私は非常に感謝します(JsonDeserializerで逆アセンブルされたオブジェクトを保存してnullを与えるだけでしたが、このソリューションはあまり美しくありませんトークンの正直な解析よりも)。 この場合、いくつかの理由で他のライブラリを使用したくないとすぐに答えますが、これらの問題をより簡単に解決できるライブラリのアドバイスも役立ちます。



ご清聴ありがとうございました。



* PS便利なJavaライブラリ、フレームワーク、ロシア語の教育ビデオ。 このプロジェクトの同様の[英語版](https://github.com/Vedenin/useful-java-links/)もあり、オープンソースサブプロジェクト[Hello world](https://github.com/Vedenin/useful-javaを開始します。 -links / tree / master / helloworlds)を使用して、1つのMavenプロジェクト内のさまざまなJavaライブラリの簡単なサンプルのコレクションを準備します(サポートに感謝します)。



All Articles