Fastjson は「ピット」レコードと「ディープ」ラーニングをステップ実行します

著者: 陶正哲アリ国際駅業務技術チーム

Fastjson は、Ali によって開発された Java 言語で書かれた高パフォーマンスの JSON ライブラリです。この記事では、Fastjson を使用する際のいくつかの注意事項を要約し、Fastjson の基礎となる動作原理を簡単に分析し、特定の検証コードと Jackson との比較を組み合わせて、皆様のお役に立てることを願っています。 Fastjson の使用法を理解する。

1. この記事を書いた理由は何ですか?

Fastjson は、Ali によって開発された Java 言語で書かれた高性能 JSON ライブラリです。JSON と Java オブジェクトの間でデータを変換するために使用されます。シリアル化操作と逆シリアル化操作をそれぞれ実装するための 2 つの主要なインターフェイス JSON.toJSONString と JSON.parseObject を提供します。使いやすい。

最近、古いシステムのキャッシュ アーキテクチャをアップグレードし、Fastjson を使用してオブジェクトをシリアル化してキャッシュに保存し、クライアント側で逆シリアル化した後にそれらを使用して、バックエンド hsf へのリクエストの数を減らしています。Fastjson を使用する過程で、いくつかの「落とし穴」を踏んで大変苦労したため、この機会に FastJson を「深く」学びました。これらの「落とし穴」自体は Fastjson のバグではなく、通常は見落とされやすい使用上の注意事項です。したがって、将来の参照のために、いくつかの注意事項が要約され、記録されます。また、より多くの学生が Fastjson の正しい使い方を理解できることを願っています。

Jackson も強力な JSON ライブラリであり、日常業務で頻繁に使用されるため、Fastjson の使用上の注意事項として、Jackson との水平比較を追加しました。この記事で使用されている Fastjson のバージョンは 1.2.68.noneautotype であり、AutoType をサポートしていません。

第二に、それを知り、正しく使用する方法

注 1: Null プロパティ/値の逆シリアル化

Fastjson をシリアル化すると、デフォルトではオブジェクトの null 属性や Map の値が null のキーは出力されません。null 属性を出力したい場合は、JSON 呼び出し時に SerializerFeature.WriteMapNullValue パラメーターを追加します。 .toJSONString 関数 (機能を有効にする)。

一般に (ログを出力する場合など)、null 属性/値がシリアル化されているかどうかはほとんど影響しません。ただし、シリアル化された文字列をオブジェクトに逆シリアル化する必要がある場合、特に Java Map/JSONObject などのデータ構造のシリアル化が関係する場合は、この時点で特別な注意を払う必要があります。コードを以下に示します。

@Test
public void testFastjsonDeserializeNullFields() {
    {
        Map<String, String> mapWithNullValue = new HashMap<>();
        mapWithNullValue.put("a", null);
        mapWithNullValue.put("b", null);
        String mapSerializeStr = JSON.toJSONString(mapWithNullValue);
​
        Map<String, String> deserializedMap = JSON.parseObject(mapSerializeStr,
                                                               new TypeReference<Map<String, String>>() {});
        if (mapWithNullValue.equals(deserializedMap)) {
            System.out.println("Fastjson: mapWithNullValue is the same after deserialization");
        } else {
            System.out.println("Fastjson: mapWithNullValue is NOT the same after deserialization");
        }
    }
​
    {
        JSONObject jsonWithNullValue = new JSONObject();
        jsonWithNullValue.put("a", null);
        jsonWithNullValue.put("b", null);
        String jsonSerializeStr = JSON.toJSONString(jsonWithNullValue);
​
        JSONObject deserializedJson = JSON.parseObject(jsonSerializeStr,
                                                       new TypeReference<JSONObject>() {});
        if (jsonWithNullValue.equals(deserializedJson)) {
            System.out.println("Fastjson: jsonWithNullValue is the same after deserialization");
        } else {
            System.out.println("Fastjson: jsonWithNullValue is NOT the same after deserialization");
        }
    }
}

上記のコードを実行した後の出力は次のようになります。

Fastjson: mapWithNullValue is NOT the same after deserialization
Fastjson: jsonWithNullValue is NOT the same after deserialization

ご覧のとおり、元のオブジェクトは逆シリアル化されたオブジェクトと同じではありません。その理由は、元のオブジェクト (mapWithNullValue、jsonWithNullValue) のサイズが 2 で、逆シリアル化されたオブジェクトのサイズが 0 であるためです。

一部のビジネス シナリオ (シリアル化後に Tair にオブジェクトをキャッシュするなど) では、元のオブジェクトと逆シリアル化されたオブジェクトが厳密に同じであることを確認する必要があるため、この問題には特別な注意を払う必要があります。この問題は、シリアル化中に SerializerFeature.WriteMapNullValue パラメーターを追加することで回避できます。

ジャクソンとの比較

同じオブジェクトを、Jackson を使用してシリアル化および逆シリアル化しても、結果は同じになります。ジャクソンのコードは次のとおりです。

@Test
public void testJacksonDeserializeNullFields() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    {
        Map<String, String> mapWithNullValue = new HashMap<>();
        mapWithNullValue.put("a", null);
        mapWithNullValue.put("b", null);
        String mapSerializeStr = objectMapper.writeValueAsString(mapWithNullValue);
        System.out.println("Jackson: mapSerializeStr: " + mapSerializeStr);
​
        Map<String, String> deserializedMap = objectMapper.readValue(
            mapSerializeStr,
            new com.fasterxml.jackson.core.type.TypeReference<Map<String, String>>() {});
        if (mapWithNullValue.equals(deserializedMap)) {
            System.out.println("Jackson: mapWithNullValue is the same after deserialization");
        } else {
            System.out.println("Jackson: mapWithNullValue is NOT the same after deserialization");
        }
    }
​
    {
        JSONObject jsonWithNullValue = new JSONObject();
        jsonWithNullValue.put("a", null);
        jsonWithNullValue.put("b", null);
        String jsonSerializeStr = objectMapper.writeValueAsString(jsonWithNullValue);
        System.out.println("Jackson: jsonSerializeStr: " + jsonSerializeStr);
​
        JSONObject deserializedJson = objectMapper.readValue(
            jsonSerializeStr, new com.fasterxml.jackson.core.type.TypeReference<JSONObject>() {});
        if (jsonWithNullValue.equals(deserializedJson)) {
            System.out.println("Jackson: jsonWithNullValue is the same after deserialization");
        } else {
            System.out.println("Jackson: jsonWithNullValue is NOT the same after deserialization");
        }
    }
}

結果は以下のように出力されますが、Jackson はデフォルトで null 値を出力することがわかります。

Jackson: mapSerializeStr: {"a":null,"b":null}
Jackson: mapWithNullValue is the same after deserialization
​
Jackson: jsonSerializeStr: {"a":null,"b":null}
Jackson: jsonWithNullValue is the same after deserialization

推奨事項

  • オブジェクトの逆シリアル化が関係する場合は、JSON.toJSONString を呼び出すときに SerializerFeature.WriteMapNullValue パラメーターを追加することをお勧めします。

注 2: Collection オブジェクトの逆シリアル化

これも使用中に遭遇した「落とし穴」で、長い間私を悩ませてきましたが、見つけるのが困難です。Java オブジェクトに Collection 型のメンバ変数が含まれる場合、元のオブジェクトと逆シリアル化されたオブジェクトが完全に同じではない可能性があります。引き続き以下の確認コードを見てください。

ObjectWithCollection クラスは、Collection 型の 2 つのプロパティを定義する POJO クラスです。

public class ObjectWithCollection {
    private Collection<String> col1;
    private Collection<Long> col2;
​
    ...setter...
    ...getter...
​
    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (!(o instanceof ObjectWithCollection)) { return false; }
        ObjectWithCollection that = (ObjectWithCollection) o;
        return Objects.equals(col1, that.col1) &&
            Objects.equals(col2, that.col2);
    }
​
    @Override
    public int hashCode() {
        return Objects.hash(col1, col2);
    }
}

以下のコードは、objectWithCollection オブジェクトをシリアル化しようとします。objectWithCollection オブジェクトのcol1 プロパティは ArrayList 変数に設定され、col2 プロパティは HashSet 変数に設定されます。

@Test
public void testFastJsonDeserializeCollectionFields() {
    ObjectWithCollection objectWithCollection = new ObjectWithCollection();
    List<String> col1 = new ArrayList<>();
    col1.add("str1");
    Set<Long> col2 = new HashSet<>();
    col2.add(22L);
    objectWithCollection.setCol1(col1);
    objectWithCollection.setCol2(col2);
    String objectWithCollectionStr = JSON.toJSONString(objectWithCollection);
    System.out.println("FastJson: objectWithCollectionStr: " + objectWithCollectionStr);
​
    ObjectWithCollection deserializedObj = JSON.parseObject(objectWithCollectionStr,
                                                            ObjectWithCollection.class);
    if (objectWithCollection.equals(deserializedObj)) {
        System.out.println("FastJson: objectWithCollection is the same after deserialization");
    } else {
        System.out.println("FastJson: objectWithCollection is NOT the same after deserialization");
    }
}

上記のコードを実行した結果は次のようになります。

FastJson: objectWithCollectionStr: {"col1":["str1"],"col2":[22]}
FastJson: objectWithCollection is NOT the same after deserialization

「徹底的な調査」の結果、ほとんどの場合 (ASM が有効な場合、ASM 機能はデフォルトで有効になります)、Fastjsonは HashSet 型を使用して文字列型の Json 配列を逆シリアル化し、ArrayList 型を使用することがわかりました。他の型 (Long/Integer/Double/カスタム Java 型など) の Json 配列を逆シリアル化します。上記で逆シリアル化されたdeserializedObjオブジェクトの変数col1の実際の型はHashSetであるため、元のオブジェクトとは異なります。

この問題はなぜ起こるのでしょうか? Collection変数に対応する特定の型がシリアル化時に出力されないためでしょうか。特定のオブジェクト タイプがシリアル化されて出力される場合 (Fastjson はこの機能をサポートしていますが、デフォルトではオフになっています)、Fastjson はそれを正しく逆シリアル化できますか? コードを更新し、JSON.toJSONString メソッドを呼び出すときに SerializerFeature.WriteClassName パラメーターを追加します。

@Test
public void testFastJsonDeserializeCollectionFields() {
    ObjectWithCollection objectWithCollection = new ObjectWithCollection();
    Collection<String> col1 = new ArrayList<>();
    col1.add("str1");
    Collection<Long> col2 = new HashSet<>();
    col2.add(22L);
    objectWithCollection.setCol1(col1);
    objectWithCollection.setCol2(col2);
    String objectWithCollectionStr = JSON.toJSONString(objectWithCollection,
                                                       SerializerFeature.WriteClassName);
    System.out.println("FastJson: objectWithCollectionStr: " + objectWithCollectionStr);
​
    ObjectWithCollection deserializedObj = JSON.parseObject(objectWithCollectionStr,
                                                            ObjectWithCollection.class);
    if (objectWithCollection.equals(deserializedObj)) {
        System.out.println("FastJson: objectWithCollection is the same after deserialization");
    } else {
        System.out.println("FastJson: objectWithCollection is NOT the same after deserialization");
    }
}

再度実行してみると、出力は次のようになります。Fastjson はオブジェクト objectWithCollection の型を正しく出力し、col2 メンバー変数の型 ("col2":Set[22L]、Set キーワードに注意してください) を正しく出力しますが、特定の型の入力に失敗していることがわかります。メンバー変数col1の逆の順序になります。変換されたオブジェクトは、依然として元のオブジェクトとは異なります。

FastJson: objectWithCollectionStr: {"@type":"com.test.utils.ObjectWithCollection","col1":["str1"],"col2":Set[22L]}
FastJson: objectWithCollection is NOT the same after deserialization

ジャクソンとの比較

同じオブジェクトについて、Jackson を使用してシリアル化/逆シリアル化を試してみましょう。具体的なコードは次のとおりです。

@Test
public void testJacksonDeserializeCollectionFields() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    ObjectWithCollection objectWithCollection = new ObjectWithCollection();
    Collection<String> col1 = new ArrayList<>();
    col1.add("str1");
    Collection<Long> col2 = new HashSet<>();
    col2.add(22L);
    objectWithCollection.setCol1(col1);
    objectWithCollection.setCol2(col2);
    String objectWithCollectionStr = objectMapper.writeValueAsString(objectWithCollection);
    System.out.println("Jackson: objectWithCollectionStr: " + objectWithCollectionStr);
​
    ObjectWithCollection deserializedObj = objectMapper.readValue(objectWithCollectionStr,
                                                                  ObjectWithCollection.class);
    if (objectWithCollection.equals(deserializedObj)) {
        System.out.println("Jackson: objectWithCollection is the same after deserialization");
    } else {
        System.out.println("Jackson: objectWithCollection is NOT the same after deserialization");
    }
}

コードの実行結果は以下の通りで、デシリアライズされたオブジェクトも異なっていることが分かります。

Jackson: objectWithCollectionStr: {"col1":["str1"],"col2":[22]}
Jackson: objectWithCollection is NOT the same after deserialization

Jacskon がシリアル化するときに、オブジェクト タイプの出力を再試行します。型を出力するには、objectMapper を設定し、次のようにコード行を追加する必要があります。

objectMapper.enableDefaultTypingAsProperty(
    ObjectMapper.DefaultTyping.NON_FINAL, "$type");

再度実行すると、非常に驚​​きました。逆シリアル化されたオブジェクトは同じです。出力は次のとおりです。Jackson は Collection 型変数の特定の型を出力することもできます。これが FastJson との主な違いです。この型に応じて、Jackson はオブジェクトを正常に逆シリアル化し、元のオブジェクトとの一貫性を確保できます。

Jackson: objectWithCollectionStr: {"$type":"com.test.utils.ObjectWithCollection","col1":["java.util.ArrayList",["str1"]],"col2":["java.util.HashSet",[22]]}
Jackson: objectWithCollection is the same after deserialization

推奨事項

  • シリアル化する必要があるオブジェクト (POJO) を定義する場合は、Collection 型の使用を避け、代わりに List/Set を使用すると、多くの「トラブル」を回避できます。

  • 依存する他のセカンドパーティ ライブラリの歴史的な古いコード/オブジェクトの場合、コレクション型が使用されている場合は、不必要な「落とし穴」を避けるために、Jackson を使用してシリアル化/逆シリアル化することをお勧めします。

  • Collection 型のメンバー変数に対する Fastjson の逆シリアル化動作は、条件が異なると一貫性がありません。これは確かに理解するのが少し難しいです。この問題については、次の章で詳しく説明します。

注 3: デフォルトのコンストラクター/セッターが欠落しているため、逆シリアル化できません

Fastjson を使用する過程で、オブジェクトのシリアル化は成功するが、逆シリアル化は常に失敗するという別の「落とし穴」が発生します。このオブジェクトに対応するコードを調べた結果、他のオブジェクトとのいくつかの違いがわかりました。それは、変数に対応するデフォルトのコンストラクターとセッターが存在しないことです。これらの関数のサポートがなければ、Fastjson は正常に逆シリアル化できませんが、例外はスローされません(少し奇妙ですが、黙って失敗します)以下の確認コードを参照してください。

クラス ObjectWithOutSetter は、デフォルトのコンストラクター (ただしパラメーターを持つ他のコンストラクター) とセッターを持たないクラスであり、具体的なコードは次のとおりです。

public class ObjectWithOutSetter {
    private String var1;
    private Long var2;
​
    /*
    public ObjectWithOutSetter() {
    }
    */
​
    public ObjectWithOutSetter(String v1, Long v2) {
        var1 = v1;
        var2 = v2;
    }
​
    public String getVar1() {
        return var1;
    }
​
    public Long getVar2() {
        return var2;
    }
​
    /*
    public void setVar1(String var1) {
        this.var1 = var1;
    }
    public void setVar2(Long var2) {
        this.var2 = var2;
    }
    */
​
    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (!(o instanceof ObjectWithOutSetter)) { return false; }
        ObjectWithOutSetter that = (ObjectWithOutSetter) o;
        return Objects.equals(var1, that.var1) &&
            Objects.equals(var2, that.var2);
    }
​
    @Override
    public int hashCode() {
        return Objects.hash(var1, var2);
    }
}
​
@Test
public void testFastJsonDeserializeObjectWithoutDefaultConstructorAndSetter() {
    ObjectWithOutSetter objectWithOutSetter = new ObjectWithOutSetter("StringValue1", 234L);
    String objectWithOutSetterStr = JSON.toJSONString(objectWithOutSetter,
                                                      SerializerFeature.WriteMapNullValue);
    System.out.println("FastJson: objectWithOutSetterStr: " + objectWithOutSetterStr);
​
    ObjectWithOutSetter deserializedObj = JSON.parseObject(objectWithOutSetterStr,
                                                           ObjectWithOutSetter.class);
    System.out.println("FastJson: deserializedObj Str: " +
                           JSON.toJSONString(deserializedObj, SerializerFeature.WriteMapNullValue));
    if (objectWithOutSetter.equals(deserializedObj)) {
        System.out.println("FastJson: objectWithOutSetter is the same after deserialization");
    } else {
        System.out.println("FastJson: objectWithOutSetter is NOT the same after deserialization");
    }
}

上記検証コードを実行すると、出力結果は以下のようになります。deserializedObj の変数 var1 と var2 が両方とも null であり、逆シリアル化が失敗していることがわかります。

FastJson: objectWithOutSetterStr: {"var1":"StringValue1","var2":234}
FastJson: deserializedObj Str: {"var1":null,"var2":null}
FastJson: objectWithOutSetter is NOT the same after deserialization

上記の ObjectWithOutSetter 型のデフォルトのコンストラクターとセッター コードのコメントを削除し、上記のテスト コードを再実行すると、逆シリアル化されたオブジェクトが一貫していることがわかります。具体的な出力は次のとおりです。

FastJson: objectWithOutSetterStr: {"var1":"StringValue1","var2":234}
FastJson: deserializedObj Str: {"var1":"StringValue1","var2":234}
FastJson: objectWithOutSetter is the same after deserialization

Fastjson の逆シリアル化は、クラス オブジェクトのデフォルトのコンストラクターとメンバー変数のセッターに依存する必要があります。そうでない場合は失敗します。要約すると、失敗する「可能性がある」シナリオがいくつかあります。

  • クラス オブジェクトにデフォルトのコンストラクターがない場合、逆シリアル化は確実に失敗します (例外はスローされませんが、メンバー変数の値は null になります)。

  • クラス オブジェクトのプライベート メンバー変数にセッターがない場合、逆シリアル化で JSON.parseObject を呼び出すときにパラメーター Feature.SupportNonPublicField が追加されない限り、逆シリアル化は確実に失敗します。特殊なケースとして、AtomicInteger/AtomicLong/AtomicBoolean/Map/Collection 型のメンバー変数の場合、対応するセッターが欠落していても、逆シリアル化は成功する可能性があります。

  • 同様に、クラス オブジェクトにゲッターがない場合、シリアル化は失敗します (例外はスローされず、空の "{}" 文字列が出力されます)。

クラス オブジェクトのパブリック メンバー変数の場合、セッターがなくても正常に逆シリアル化できます。

ジャクソンとの比較

同じ ObjectWithOutSetter オブジェクト (セッターなし) がシリアル化/逆シリアル化のために Jackson に置き換えられます。検証コードは次のとおりです。

@Test
public void testJacksonDeserializeObjectWithoutDefaultConstructorAndSetter() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    ObjectWithOutSetter objectWithOutSetter = new ObjectWithOutSetter("StringValue1", 234L);
    String objectWithOutSetterStr = objectMapper.writeValueAsString(objectWithOutSetter);
    
    System.out.println("Jackson: objectWithOutSetterStr: " + objectWithOutSetterStr);
​
    ObjectWithOutSetter deserializedObj = objectMapper.readValue(objectWithOutSetterStr,
                                                                 ObjectWithOutSetter.class);
    System.out.println("Jackson: deserializedObj Str: "
                           + objectMapper.writeValueAsString(deserializedObj));
    if (objectWithOutSetter.equals(deserializedObj)) {
        System.out.println("Jackson: objectWithOutSetter is the same after deserialization");
    } else {
        System.out.println("Jackson: objectWithOutSetter is NOT the same after deserialization");
    }
}


出力は次のとおりです。ご覧のとおり、セッターがなくても、Jackson は正しく逆シリアル化できます。

Jackson: objectWithOutSetterStr: {"var1":"StringValue1","var2":234}
Jackson: deserializedObj Str: {"var1":"StringValue1","var2":234}
Jackson: objectWithOutSetter is the same after deserialization

何度か検証を繰り返した結果、Jackson のシリアル化/逆シリアル化は次のように要約されます。

  • クラス オブジェクトに getter/@JsonProperty アノテーションが付けられた変数がない場合、シリアル化は失敗します。com.fasterxml.jackson.databind.exc.InvalidDefinitionException がスローされます。

  • クラス オブジェクトにデフォルトのコンストラクターがない場合、逆シリアル化は失敗します。com.fasterxml.jackson.databind.exc.InvalidDefinitionException がスローされます。個人的には、この設計の方が合理的であり、問​​題をできるだけ早く発見できると思います。

  • クラス オブジェクトに対応するセッター関数がない場合でも、逆シリアル化は成功します。Jackson はデフォルトで Java Reflection を使用してメンバー変数を設定します。この時点では、ジャクソンはまだ非常に強いと感じます。

Fastjson と Jackson はどちらも、シリアル化を完了するためにオブジェクトの getter 関数に依存する必要があります。

推奨事項

  • Fastjson でシリアル化する必要があるオブジェクトの場合は、メンバー変数の getter 関数を定義する必要があります。そうしないとシリアル化できません。

  • Fastjson を使用して逆シリアル化する必要があるオブジェクトの場合は、必ずデフォルトのコンストラクターとメンバー変数のセッター関数を定義してください。定義しないと失敗します。オブジェクトを定義するときは、多くの問題を回避するために、デフォルトのコンストラクター、セッター、およびゲッターを定義することが最善です。

  • 歴史的な古いコードの場合、対応するセッター関数がない場合は、Jackson を使用して逆シリアル化することを検討できます。

  • 歴史的な古いコードの場合、デフォルトのコンストラクターとゲッター関数が欠落している場合、Fastjson または Jackson のどちらが使用されているかに関係なく、逆シリアル化は失敗し、コードは変更することしかできません。

注 4: 抽象クラス/インターフェイスの逆シリアル化

この注目ポイント、実は上記の注目ポイント3とも少し関連しています。Fastjson は逆シリアル化するときにデフォルトのコンストラクターとセッター関数に依存する必要があるため、クラス オブジェクトを「構築」できない場合、逆シリアル化は確実に失敗します。例えば、オブジェクトの種類がインターフェース(Interface)や抽象クラス(Abstract Class)の場合、対応するオブジェクトを構築できず、当然デシリアライズは失敗します。以下のコード検証を参照してください。

public class InterfaceObject implements TestInterface {
    private String var1;
    private Long data1;
    ...
}
​
public abstract class AbstractClass {
    private String abStr1;
}
​
public class AbstractDemoObject extends AbstractClass {
    private String var2;
    private Long data2;
    ...
}
public class CompositeObject {
    private TestInterface interfaceObject;
    private AbstractClass abstractClass;
    private Long data2;
    ...
}
@Test
public void testFastJsonDeserializeObjectWithInterface() {
    CompositeObject compositeObject = new CompositeObject();
    compositeObject.setData2(123L);
​
    InterfaceObject interfaceObject = new InterfaceObject();
    interfaceObject.setData1(456L);
    interfaceObject.setVar1("StringValue1");
    compositeObject.setInterfaceObject(interfaceObject);
​
    AbstractDemoObject demoObject = new AbstractDemoObject();
    demoObject.setVar2("StringValue2");
    demoObject.setData2(789L);
    demoObject.setAbStr1("abStr1");
    compositeObject.setAbstractClass(demoObject);
​
    String compositeObjectStr = JSON.toJSONString(compositeObject,
                                                  SerializerFeature.WriteMapNullValue);
    System.out.println("FastJson: compositeObjectStr: " + compositeObjectStr);
​
    CompositeObject deserializedObj = JSON.parseObject(compositeObjectStr,
                                                       CompositeObject.class);
    System.out.println("FastJson: deserializedObj Str: " +
                           JSON.toJSONString(deserializedObj, SerializerFeature.WriteMapNullValue));
    if (deserializedObj.getAbstractClass() == null) {
        System.out.println("FastJson: deserializedObj.abstractClass is null");
    }
    if (deserializedObj.getInterfaceObject() == null) {
        System.out.println("FastJson: deserializedObj.interfaceObject is null");
    } else {
        System.out.println("FastJson: deserializedObj.interfaceObject is not null. ClassName: "
                               + deserializedObj.getInterfaceObject().getClass().getName());
    }
}

上記のコードの「重要なポイント」は、CompositeObject クラスのinterfaceObject 変数と abstractClass 変数の型であり、それぞれインターフェイスと抽象クラス (両方とも基本クラス型) です。検証コードを実行すると結果出力は以下のようになり、デシリアライズが失敗したことが分かります。

FastJson: compositeObjectStr: {"abstractClass":{"data2":789,"var2":"StringValue2"},"data2":123,"interfaceObject":{"data1":456,"var1":"StringValue1"}}
FastJson: deserializedObj Str: {"abstractClass":null,"data2":123,"interfaceObject":{}}
FastJson: deserializedObj.abstractClass is null
FastJson: deserializedObj.interfaceObject is not null. ClassName: com.sun.proxy.$Proxy15

上記の出力から、インターフェイス/抽象クラス変数の場合、逆シリアル化の動作が依然として異なることもわかります。逆シリアル化されたオブジェクト deserializedObj では、抽象クラス変数 abstractClass の値は null ですが、インターフェイス型変数interfaceObject は null ではありません。Fastjson はインターフェイスに従ってプロキシ クラス (com.sun.proxy.*) を自動的に作成し、デシリアライゼーションをサポートできると判断されます。

CompositeObject クラスのinterfaceObject 変数と abstractClass 変数の両方をサブクラス型に変更すると、シリアル化/逆シリアル化は正常に機能します。

シリアル化中に SerializerFeature.WriteClassName パラメーターを追加すると、シリアル化された文字列に特定のクラス情報が含まれますが、逆シリアル化中に「safeMode not support autoType」という例外がスローされます。Fastjson は、autoType ベースのカスタム クラスの逆シリアル化をサポートしなくなりました。

[ERROR] testFastJsonDeserializeObjectWithInterface(com.test.utils.FastjsonTest)  Time elapsed: 0.673 s  <<< ERROR!
com.alibaba.fastjson.JSONException: safeMode not support autoType : com.test.utils.AbstractDemoObject
  at com.test.utils.FastjsonTest.testFastJsonDeserializeObjectWithInterface(FastjsonTest.java:343)

ジャクソンとの比較

同じ CompositeObject オブジェクトが Jackson によってシリアル化された場合も失敗し、InvalidDefinitionException が直接スローされます (以下を参照)。Jackson はインターフェイス/抽象クラスの逆シリアル化もサポートしていません。

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `com.test.utils.TestInterface` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

ただし、シリアル化するときに特定のクラス情報を追加すると、Jackson は正常に動作します。以下の確認コードを参照してください。

@Test
public void testJacksonDeserializeObjectWithInterface() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    // 增加这一行,输出具体类信息
    objectMapper.enableDefaultTypingAsProperty(
        ObjectMapper.DefaultTyping.NON_FINAL, "$type");
​
    CompositeObject compositeObject = new CompositeObject();
    compositeObject.setData2(123L);
​
    InterfaceObject interfaceObject = new InterfaceObject();
    interfaceObject.setData1(456L);
    interfaceObject.setVar1("StringValue1");
    compositeObject.setInterfaceObject(interfaceObject);
​
    AbstractDemoObject demoObject = new AbstractDemoObject();
    demoObject.setVar2("StringValue2");
    demoObject.setData2(789L);
    demoObject.setAbStr1("abStr1");
    compositeObject.setAbstractClass(demoObject);
​
    String compositeObjectStr = objectMapper.writeValueAsString(compositeObject);
    System.out.println("Jackson: compositeObjectStr: " + compositeObjectStr);
​
    CompositeObject deserializedObj = objectMapper.readValue(compositeObjectStr,
                                                             CompositeObject.class);
    System.out.println("Jackson: deserializedObj Str: " +
                           objectMapper.writeValueAsString(deserializedObj));
    if (deserializedObj.getAbstractClass() == null) {
        System.out.println("Jackson: deserializedObj.abstractClass is null");
    }
    if (deserializedObj.getInterfaceObject() == null) {
        System.out.println("Jackson: deserializedObj.interfaceObject is null");
    } else {
        System.out.println("Jackson: deserializedObj.interfaceObject is not null. ClassName: "
                               + deserializedObj.getInterfaceObject().getClass().getName());
    }
}

出力は次のとおりです。

Jackson: compositeObjectStr: {"$type":"com.test.utils.CompositeObject","interfaceObject":{"$type":"com.test.utils.InterfaceObject","var1":"StringValue1","data1":456},"abstractClass":{"$type":"com.test.utils.AbstractDemoObject","abStr1":"abStr1","var2":"StringValue2","data2":789},"data2":123}
Jackson: deserializedObj Str: {"$type":"com.test.utils.CompositeObject","interfaceObject":{"$type":"com.test.utils.InterfaceObject","var1":"StringValue1","data1":456},"abstractClass":{"$type":"com.test.utils.AbstractDemoObject","abStr1":"abStr1","var2":"StringValue2","data2":789},"data2":123}
Jackson: deserializedObj.interfaceObject is not null. ClassName: com.test.utils.InterfaceObject

推奨事項

  • Fastjson でシリアル化する必要があるオブジェクトを定義する場合、インターフェイス (Interface)/抽象クラス (Abstract Class) 型を使用してメンバー変数を定義しないでください。そうしないと、逆シリアル化が失敗します。逆シリアル化するとサブクラス情報が失われるため、基本クラス (Base Class) 型を使用してメンバー変数を定義しないでください。

  • Fastjson の AutoType 機能は推奨されなくなりました。デフォルトでは、JDK のネイティブ クラスのみがサポートされ、AutoType に基づくカスタム Java クラスの逆シリアル化はサポートされなくなりました。

  • 歴史的な古いコードの場合、インターフェイス/抽象クラス/基本クラス型がメンバー変数の定義に使用されている場合、シリアル化に Jackson の使用を検討できますが、オブジェクトの特定の型を出力する必要があります。

注 5: 「偽の」ゲッター/セッターのシリアル化/逆シリアル化

上記のいくつかの注意点の分析を通じて、Fastjson のシリアル化/逆シリアル化がオブジェクトのデフォルトのコンストラクター、ゲッター、およびセッター関数に依存していることがすでにわかりました。ただし、一部のクラス オブジェクトでは、setXXX()/getXXX() などの関数がよく見られますが、これらの関数自体はオブジェクトを直接返したり設定したりするのではなく、内部処理ロジック (ロジックは複雑または単純な場合があります) を持っています。たまたま set/get にちなんだ名前が付けられているだけです。これらの関数はシリアル化/逆シリアル化の失敗につながることが多く、より深刻な場合にはシステム セキュリティ ホールを引き起こすこともあります (Fastjson の一部のセキュリティ ホールは、AutoType 関数や、非常に一般的な com .sun.rowset などの特定のセッター関数によって引き起こされます)。 JdbcRowSetImpl)。これらの関数はメンバー変数に直接対応しないため、「偽の」ゲッター/セッターと呼びます。

典型的な例は、Ali MetaQ のメッセージ オブジェクト com.alibaba.rocketmq.common.message.MessageExt (これは、実際には com.alibaba.rocketmq.common.message.MessageExtBatch を指す基本クラスです) をシリアル化できないことです。投げられるだろう。この罠を踏んだ人も多いはずで、そのためAliの開発規定では、ログ出力時にJSONツールを直接使用してオブジェクトをStringに変換する行為を禁止することが明確に規定されている。

コードを通してこの問題を検証してみましょう。次のクラスでは、「false」ゲッターが定義されています: getWired()。

public class ObjectWithWiredGetter {
    private String var1;
    private Long data1;
​
    public String getVar1() {
        return var1;
    }
​
    public void setVar1(String var1) {
        this.var1 = var1;
    }
​
    public Long getData1() {
        return data1;
    }
​
    public void setData1(Long data1) {
        this.data1 = data1;
    }
​
    /**
     * 注意这个函数
     *
     * @return
     */
    public String getWired() {
        return String.valueOf(1 / 0);
    }
}
​
@Test
public void testFastJsonSerializeObjectWithWiredGetter() {
    ObjectWithWiredGetter objectWithWiredGetter = new ObjectWithWiredGetter();
​
    String objectWithWiredGetterStr = JSON.toJSONString(objectWithWiredGetter,
                                                        SerializerFeature.WriteMapNullValue);
    System.out.println("FastJson: objectWithWiredGetter: " + objectWithWiredGetterStr);
}

上記の検証コードを実行すると、getWired() のロジック実行時に Fastjson が直接 ArithmeticException をスローし、シリアル化に失敗します。

[ERROR] testFastJsonSerializeObjectWithWiredGetter(com.test.utils.FastjsonTest)  Time elapsed: 0.026 s  <<< ERROR!
java.lang.ArithmeticException: / by zero
  at com.test.utils.FastjsonTest.testFastJsonSerializeObjectWithWiredGetter(FastjsonTest.java:399)

上記の getWired() 関数は単なる単純なデモです。さらに拡張すると、「複雑な」処理ロジック (ゲッターでの hsf の呼び出し、データベースへの書き込みなど) を備えたゲッター関数の場合、Fastjson はシリアル化時に「予期しない」結果を引き起こすことがよくありますが、これは通常は予期されません。

この問題をどうやって解決すればいいでしょうか?JSON.toJSONString を呼び出すときに、SerializerFeature.IgnoreNonFieldGetter パラメーターを追加して、対応するメンバー変数 (フィールド) を持たないすべてのゲッター関数を無視し、通常どおりシリアル化します。@JSONField(serialize = false) アノテーションを getWired 関数に追加することによっても、同じ効果を実現できます。

@Test
public void testFastJsonSerializeObjectWithWiredGetter() {
    ObjectWithWiredGetter objectWithWiredGetter = new ObjectWithWiredGetter();
    objectWithWiredGetter.setVar1("StringValue1");
    objectWithWiredGetter.setData1(100L);
​
    String objectWithWiredGetterStr = JSON.toJSONString(objectWithWiredGetter,
                                                        SerializerFeature.WriteMapNullValue,
                                                        SerializerFeature.IgnoreNonFieldGetter);
    System.out.println("FastJson: objectWithWiredGetter: " + objectWithWiredGetterStr);
}

パラメータを追加した後、再度検証コードを実行すると、次のような結果が出力され、シリアル化が成功しました。

FastJson: objectWithWiredGetter: {"data1":100,"var1":"StringValue1"}

ジャクソンとの比較

Jackson のシリアル化も getter に依存します。同じオブジェクトが Jackson でシリアル化された場合に何が起こるかを確認してください。

@Test
public void testJacksonSerializeObjectWithWiredGetter() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
​
    ObjectWithWiredGetter objectWithWiredGetter = new ObjectWithWiredGetter();
    objectWithWiredGetter.setVar1("StringValue1");
    objectWithWiredGetter.setData1(100L);
​
    String objectWithWiredGetterStr = objectMapper.writeValueAsString(objectWithWiredGetter);
    System.out.println("Jackson: objectWithWiredGetter: " + objectWithWiredGetterStr);
}

検証コードが実行された後、Jackson は次のように JsonMappingException を直接スローします。デフォルトでは、Jackson はそのような「複雑な」ゲッター関数のシリアル化を適切に処理できません。

[ERROR] testJacksonSerializeObjectWithWiredGetter(com.test.utils.FastjsonTest)  Time elapsed: 0.017 s  <<< ERROR!
com.fasterxml.jackson.databind.JsonMappingException: / by zero (through reference chain: com.test.utils.ObjectWithWiredGetter["wired"])
  at com.test.utils.FastjsonTest.testJacksonSerializeObjectWithWiredGetter(FastjsonTest.java:431)
Caused by: java.lang.ArithmeticException: / by zero
  at com.test.utils.FastjsonTest.testJacksonSerializeObjectWithWiredGetter(FastjsonTest.java:431)

もちろん、Jackson は設定を通じてこの問題を簡単に解決することもできます。@JsonIgnore アノテーションを getWired 関数に直接追加すると、正常にシリアル化されます。

/**
 * 注意这个函数。注意@JsonIgnore注解
 *
 * @return
 */
@JsonIgnore
public String getWired() {
    return String.valueOf(1 / 0);
}

推奨事項

  • Fastjson も Jackson も、デフォルトでは、「偽の」ゲッター関数のシリアル化をあまりうまく処理できません (偽のセッター関数に対応する逆シリアル化も同様です)。シリアル化/逆シリアル化する必要があるオブジェクトの場合は、「偽」のゲッター/セッターを定義しないようにしてください。また、ゲッター/セッターに「複雑な」処理ロジックを含めないようにしてください。純粋な POJO の方が良いので、シンプルにしてください。

  • 「偽」のゲッター/セッター (メンバー変数に対応するセッター/ゲッター関数がない) の場合、通常、シリアル化/逆シリアル化に参加する必要はありません。これらは、SerializerFeature.IgnoreNonFieldGetter パラメーターを使用して、シリアル化フェーズ中にフィルターで除外できます。ほとんどのシナリオでは、SerializerFeature.IgnoreNonFieldGetter パラメーターを追加する方が安全であり、いくつかの「奇妙な」問題を回避できます。@JSONField アノテーションの形式で無視することもできます。

注6: 同じ参照オブジェクトのシリアル化

Fastjson には、循環参照検出 (デフォルトで有効) という非常に強力な機能があります。たとえば、(以下のコードに示すように) 2 つのオブジェクト A と B があります。A には B オブジェクトへの参照が含まれ、B には A オブジェクトへの参照が含まれ、A/B 2 つのオブジェクトは循環参照になります。この時点でシリアル化すると、通常、StackOverflowError の問題が発生します。Fastjson には循環参照を検出する機能があるため、オブジェクト A を「正常に」シリアル化できます。次のコードで確認してみましょう。

public class DemoA {
    private DemoB b;
}
​
public class DemoB {
    private DemoA a;
}
​
@Test
public void testFastJsonSerializeCircularObject() {
    DemoA A = new DemoA();
    DemoB B = new DemoB();
    A.setB(B);
    B.setA(A);
    String demoAStr = JSON.toJSONString(A,
                                        SerializerFeature.WriteMapNullValue);
    System.out.println("FastJson: demoA serialization str: " + demoAStr);
}

実行後の出力は以下の通りです。オブジェクト A は、「奇妙な」文字列表現に「正常に」シリアル化されます。これはまさに循環参照検出が行うことです。循環参照が検出された場合、参照先のオブジェクトは「$ref」で表されます。

FastJson: demoA serialization str: {"b":{"a":{"$ref":".."}}}

Fastjson は、次のように合計 4 つのオブジェクト参照をサポートします。

循環参照を検出する能力は確かに非常に強力ですが、場合によってはその「能力」が強すぎると、「無実の人々に災いを与え」、過失致死を引き起こす可能性があります。たとえば、オブジェクトには明らかに「循環参照」がありませんが、参照モードに従ってシリアル化されています。以下のコードを参照してください。

public class RefObject {
    private String var1;
    private Long data1;
    ...setter/getter....
}
​
public class SameRefObjectDemo {
    private List<RefObject> refObjectList;
    private Map<String, RefObject> refObjectMap;
    ...setter/getter....
}
​
@Test
public void testFastJsonSerializeSameReferenceObject() {
    RefObject refObject = new RefObject();
    refObject.setVar1("Value1");
    refObject.setData1(9875L);
    
    SameRefObjectDemo sameRefObjectDemo = new SameRefObjectDemo();
    List<RefObject> refObjects = new ArrayList<>();
    refObjects.add(refObject);
    refObjects.add(refObject);
    sameRefObjectDemo.setRefObjectList(refObjects);
    
    Map<String, RefObject> refObjectMap = new HashMap<>();
    refObjectMap.put("key1", refObject);
    refObjectMap.put("key2", refObject);
    sameRefObjectDemo.setRefObjectMap(refObjectMap);
    
    String sameRefObjectDemoStr = JSON.toJSONString(sameRefObjectDemo,
                                                    SerializerFeature.WriteMapNullValue);
    System.out.println("FastJson: sameRefObjectDemoStr: " + sameRefObjectDemoStr);
​
    SameRefObjectDemo deserializedObj = JSON.parseObject(sameRefObjectDemoStr,
                                                         SameRefObjectDemo.class);
    System.out.println("FastJson: deserializedObj Str: " +
                           JSON.toJSONString(deserializedObj, SerializerFeature.WriteMapNullValue));
    if (sameRefObjectDemo.equals(deserializedObj)) {
        System.out.println("FastJson: sameRefObjectDemo is the same after deserialization");
    } else {
        System.out.println("FastJson: sameRefObjectDemo is NOT the same after deserialization");
    }
}

出力は次のとおりです。SameRefObjectDemo オブジェクトが実際に参照文字列にシリアル化されていることがわかります。

FastJson: sameRefObjectDemoStr: {"refObjectList":[{"data1":9875,"var1":"Value1"},{"$ref":"$.refObjectList[0]"}],"refObjectMap":{"key1":{"$ref":"$.refObjectList[0]"},"key2":{"$ref":"$.refObjectList[0]"}}}
FastJson: deserializedObj Str: {"refObjectList":[{"data1":9875,"var1":"Value1"},{"$ref":"$.refObjectList[0]"}],"refObjectMap":{"key1":{"$ref":"$.refObjectList[0]"},"key2":{"$ref":"$.refObjectList[0]"}}}
FastJson: sameRefObjectDemo is the same after deserialization

SameRefObjectDemo オブジェクトには循環参照は含まれておらず、同じオブジェクト "refObject" (私はこれを同じ参照オブジェクトと呼びます) を繰り返し参照するだけです。この形式の Java オブジェクトは、日常のビジネス ロジックで非常に一般的です。「参照」モードに従ってシリアル化された場合、フロントエンドとバックエンドの相互作用などの影響が発生し、フロントエンドはこの「奇妙な」Json 文字列を解析できません。たとえば、異種システム間の対話では、異なる Json フレームワークが使用されるため、相互に通信障害が発生します。

なぜ同じ参照オブジェクトを「循環参照」モードに従ってシリアル化する必要があるのでしょうか? 私が思いつくのは、シリアル化された結果の出力長を減らし、ネットワーク送信のオーバーヘッドを減らすことだけです。メリットとデメリットがあります。

SerializerFeature.DisableCircularReferenceDetect パラメーターを JSON.toJSONString 関数に追加すると、「循環参照」検出機能を無効にすることができ、通常の出力は次のようになります。

FastJson: sameRefObjectDemoStr: {"refObjectList":[{"data1":9875,"var1":"Value1"},{"data1":9875,"var1":"Value1"}],"refObjectMap":{"key1":{"data1":9875,"var1":"Value1"},"key2":{"data1":9875,"var1":"Value1"}}}

ジャクソンとの比較

デフォルトでは、Jackson は「循環参照」オブジェクトのシリアル化をサポートしておらず、(次のような) StackOverflowError エラーをスローします。

com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)

ただし、この問題を解決できる注釈が多数提供されています。この記事「ジャクソン – 双方向関係」では、さまざまなオプションについて詳しく説明します。ここでは、@JsonManagedReference アノテーションと @JsonBackReference アノテーションを使用するメソッドを検証します。具体的なコードは次のとおりです。

「ジャクソン – 双方向の関係」参考リンク:

https://www.baeldung.com/jackson-bidirection-relationships-and-infinite-recursion

public class DemoA {
    @JsonManagedReference
    private DemoB b;
    ...
}
​
public class DemoB {
    @JsonBackReference
    private DemoA a;
    private String str1;
    ...
}
​
@Test
public void testJacksonSerializeCircularObject() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    DemoA A = new DemoA();
    DemoB B = new DemoB();
    A.setB(B);
    B.setA(A);
    B.setStr1("StringValue1");
    String demoAStr = objectMapper.writeValueAsString(A);
    System.out.println("Jackson: demoA serialization str: " + demoAStr);
​
    DemoA deserializedObj = objectMapper.readValue(demoAStr, DemoA.class);
    if (deserializedObj.getB() != null) {
        System.out.println("Jackson: demoB object is not null. "
                               + "B.str1: " + deserializedObj.getB().getStr1()
                               + ", B.a: "
                               + ((deserializedObj.getB().getA() == null) ? "(null)" : "(not null)"));
    }
}

出力は次のとおりです。DemonA オブジェクトは正しくシリアル化でき、逆シリアル化されたオブジェクト deserializedObj では変数 b が正しい値になっていることがわかります。

Jackson: demoA serialization str: {"b":{"str1":"StringValue1"}}
Jackson: demoB object is not null. B.str1: StringValue1, B.a: (not null)

推奨事項

  • HTTP インターフェイスを介してフロントエンド システムと対話するときに、Fastjson シリアル化が使用されている場合は、SerializerFeature.DisableCircularReferenceDetect パラメーターを設定して「循環参照」検出機能を無効にしてみてください。

  • 「循環参照」は通常、不適切な設計を示しており、リファクタリングして除去する必要があります。SerializerFeature.DisableCircularReferenceDetect パラメーターを使用して「循環参照」検出機能を無効にし、問題をできるだけ早く公開して発見できるようにします。

  • 本当に「循環参照」を処理する必要があるシナリオでは、Fastjson を使用すると便利です。

3.理由を知り、キャセロールを割って終わりを尋ねる

上の大きなスペースには、Fastjsonを使用する際の注意点がいくつか記載されており、より合理的に使用する方法が紹介されていますが、まだ「知っている」段階です。上記の注意事項にはまだ答えられていない疑問がいくつか残っています。これらの質問に答え、Fastjson の基礎となる動作原理をより深く理解するには、Fastjson を便利で柔軟に使用できるように、「理由を知る」必要もあります。

3.1 オブジェクトはどのようにシリアル化されますか?

Java オブジェクトのシリアル化を分析するには、一般的に使用される JSON.toJSONString を使用します。これには、Fastjson のいくつかの主要なオブジェクトが含まれます。それらの間の関係は次のとおりです。

上記のクラス図では、コア クラスは SerializeConfig と JavaBeanSerializer です。SerializeConfig には 2 つの主な役割があります。

  • IdentityHashMap が維持され、さまざまな Java クラスとそれに対応するシリアライザー間の関係が保存されます。JSON.toJSONString シリアル化が呼び出されるたびに、対応するシリアライザーが見つかります。

  • クラス オブジェクト (通常はカスタム Java オブジェクト) のシリアライザーが見つからない場合、JavaBeanSerializer が再作成され、次回使用するために IdentityHashMap に追加されます。

JavaBeanSerializer は主にカスタム Java オブジェクトをシリアル化するために使用されます。Fastjson は、SerializeConfig から対応するクラスのシリアライザーを見つけた後、シリアライザーの書き込みインターフェイスを直接呼び出してシリアル化を完了します。カスタム クラス オブジェクト、その各メンバー変数 (フィールド) オブジェクトには、対応する FieldSerializer があります。FieldSerializer は、クラス オブジェクトのシリアル化中に順番に呼び出されます。次の図は、単純な Java POJO オブジェクトがシリアル化する必要がある Serializer を示しています。

3.1.1 シリアライザーの種類は何種類ありますか?

Java カスタム オブジェクトのシリアル化は、他の多くの Java カスタム オブジェクトと Java プリミティブ型が関与するため、依然として非常に複雑です。Java のプリミティブ型の場合、Fastjson は基本的に、シリアル化作業を適切にサポートできる対応するシリアライザーを定義します。com.alibaba.fastjson.serializer.ObjectSerializer インターフェイスを実装するクラスはすべてデフォルトで Fastjson で定義された Serializer で、ざっと見てみると 44 個ほどあります。

3.1.2 JavaBeanSerializer はどのように作成されますか?

JavaBeanSerializer は、Fastjson シリアル化のコア クラスです。デフォルトでは、Fastjson は、シリアル化する必要があるカスタム Java クラスごとに JavaBeanSerializer オブジェクトを作成するか、ASM (バイトコード変更) テクノロジを通じて JavaBeanSerializer サブクラス オブジェクトを動的に生成し、コアのシリアル化作業を完了します。JavaBeanSerializer サブクラスの命名規則は、ASMSerializer_<Random Number>_<Original Class Name>です(例: ASMSerializer_4_ObjectWithWiredGetter)。

なぜ ASM テクノロジーを通じて JavaBeanSerializer のサブクラス オブジェクトを作成する必要があるのでしょうか? 主に効率とパフォーマンスのためであり、これが Fastjson が速い理由の 1 つです。ASM テクノロジーによって作成された JavaBeanSerializer のサブクラス オブジェクトは高度にカスタマイズされており、シリアル化する必要があるカスタム Java クラスと密接にバインドされているため、最高のパフォーマンスを実現できます。

JavaBeanSerializer のサブクラス オブジェクトを ASM テクノロジを通じて作成するかどうかは、主に「クラス」に依存します。誰にでも個性があり、クラスにも「クラス」がある。キャラクターは作業結果の品質を決定し、「カテゴリ」はシリアライズ中に ASM 機能が使用できるかどうかを決定します。「カテゴリー」を簡単に理解すると、クラスの包括的な属性であり、クラスの基底クラスの特性、クラスの名前、メンバー変数の数、ゲッター/セッター、クラスの有無などによって総合的に決定されます。インターフェース、JSONField アノテーションが使用されるかどうかなど。次のコード スニペットは、JavaBeanSerializer の作成プロセスを示しています。asm 変数はデフォルトで true です。Fastjson は一連の判断を行って asm 変数の値を決定し、ASMSerializer を作成するかどうかを決定します。

次の図は、JavaBeanSerializer とそれに依存する SerializeBeanInfo および FieldSerializer の関係を示しています。

JavaBeanSerializer の作成は SerializeBeanInfo に依存します。SerializeBeanInfo は Java クラスに対して 1 回だけ作成されます (com.alibaba.fastjson.util.TypeUtils#buildBeanInfo メソッドを呼び出して作成し、続いて JavaBeanSerializer を呼び出します)。これには主に次の情報が含まれます。

  • beanType: つまり、Java クラス オブジェクトの実際の型。

  • jsonType: Java クラス オブジェクトが @JSONType アノテーションで修飾されている場合は値を持ち、それ以外の場合は null です。

  • typeName: 上記の jsonType に依存します。jsonType のシリアル化された出力に型が指定されている場合、その型には値があります。

  • 機能: 上記の jsonType に依存します。jsonType でシリアル化された出力を指定する SerializerFeature がある場合、値を持ちます。

  • フィールド: Java クラス オブジェクトでシリアル化する必要がある get メソッド/フィールド情報が含まれており、このフィールドはより重要です。

SerializeBeanInfo オブジェクトは、Java オブジェクトのシリアル化の出力を決定します。JavaBeanSerializer は、SerializeBeanInfo オブジェクトのフィールド フィールドに従って、対応するメンバー変数 FieldSerializer を作成します。

FieldSerializer の RuntimeSerializerInfo は、作成時には null ですが、実際に使用されるときに初期化され、特定のシリアライザーを指します (遅延初期化モード)。

3.1.3 シリアライザーをカスタマイズするにはどうすればよいですか?

シリアライザーがどのように検索され、どのように機能するかを理解したら、「ひょうたんに従って」独自のシリアライザーを作成できます。以下では、DummyEnum クラスを使用してシリアライザーの作成方法を示します。

まず、次のように DummyEnum クラスを作成します。

public enum DummyEnum {
    /**
     * 停用
     */
    DISABLED(0, "Disabled"),
    /**
     * 启用
     */
    ENABLED(1, "Enabled"),
    /**
     * 未知
     */
    UNKNOWN(2, "Don't Known");
​
    private int code;
    private String desc;
​
    DummyEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
​
    public static DummyEnum from(int value) {
        for (DummyEnum dummyEnum : values()) {
            if (value == dummyEnum.getCode()) {
                return dummyEnum;
            }
        }
        return null;
    }
    
    ...getter
    ...setter
}

次に、DummyEnumSerializer を作成します。Serializer は ObjectSerializer インターフェイスを実装する必要があります。コードは次のとおりです。

public class DummyEnumSerializer implements ObjectSerializer {
    @Override
    public void write(JSONSerializer serializer, Object object, 
                      Object fieldName, Type fieldType, int features)
        throws IOException {
        if (object == null) {
            serializer.out.writeNull();
        } else {
            DummyEnum myEnum = (DummyEnum) object;
            // 序列化时调用getCode方法
            serializer.out.writeInt(myEnum.getCode());
        }
    }
}

最後に、DummyEnumSerializer オブジェクトを作成し、Global SerializeConfig に登録するだけです。

@Test
public void testFastJsonSerializeDummyEnum() {
    try {
        DummyEnum dummyEnum = DummyEnum.UNKNOWN;
​
        // 把DummyEnumSerializer插入到全局的SerializeConfig中
        SerializeConfig globalConfig = SerializeConfig.getGlobalInstance();
        globalConfig.put(DummyEnum.class, new DummyEnumSerializer());
​
        String dummyEnumStr = JSON.toJSONString(dummyEnum,
                                                SerializerFeature.WriteMapNullValue);
        System.out.println("FastJson: dummyEnumStr: " + dummyEnumStr);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Enum クラスのデフォルトのシリアル化では、列挙型変数の名前 (name) が出力されます。上記の検証コードが実行されると、数値 2 が出力され、新しいシリアライザーが正常に実行されたことを示します。

FastJson: dummyEnumStr: 2

3.2 文字列はどのように逆シリアル化されますか?

Fastjson が逆シリアル化をサポートするコア関数は 3 つあり、それぞれの主な機能と違いは次のとおりです。

1) JSON.parse(String): 文字列から直接シリアル化し、Java オブジェクトを返します。

  1. 文字列が Collection 型 (Set/List など) の Java オブジェクト シリアル化によって生成された場合 (「[...]」と同様)、この関数は JSONArray オブジェクトを返します

  2. 文字列がカスタム オブジェクトのシリアル化 (「{...}」など) から生成された場合、この関数は JSONObject オブジェクトを返します。

  3. オブジェクト タイプ (Set/TreeSet など) が文字列で指定されている場合、対応する Set/TreeSet Java オブジェクトが直接返されます。

2) JSON.parseObject(String):文字列から直接シリアル化し、JSONObject オブジェクトを返します。

  1. 文字列が Collection 型 (Set/List など) の Java オブジェクト シリアル化によって生成された場合、 この関数は例外「com.alibaba.fastjson.JSONException: JSONObject にキャストできません。」をスローします。

3) JSON.parseObject(String, Clazz): Java Clazz タイプに従って、文字列をカスタム Java オブジェクトに逆シリアル化します。List/Set/Map などの Java ネイティブ オブジェクトの生成もサポートできます。

Fastjson はデフォルトで AutoType を無効にしているため (バージョン 1.2.68)、AutoType 関数の影響を考慮しないと、これら 3 つのインターフェイス間の比較は次のようになります。

 

3.2.1 parseObject(String) の仕組み

3 つのインターフェイスのうち、parse(String) と parseObject(String) の機能は非常に似ています。後者は前者を呼び出し、後者の結果を対応する JSONObject に変換します。ここでは、parseObject(String) インターフェイスの動作原理の分析に焦点を当てます。

上の図は、parseObject(String) インターフェイスのメイン コードを示しています。これは主に 2 つのステップに分かれています。

  • parse(String) インターフェイスを呼び出して、文字列をオブジェクトに逆シリアル化します。

  • JSON.toJSON インターフェイスを呼び出して、手順 1 のオブジェクトを JSONObject に変換します。

以下のスクリーンショットに示すように、parse(String) インターフェイスにはコードがほとんどなく、そのコアは DefaultJSONParser です。

DefaultJSONParser は逆シリアル化のメイン ドライバー クラスであり、次の 2 つのコア オブジェクトが含まれています。

  • ParseConfig:デシリアライゼーションのためのコア構成クラス。これには、一部の列オブジェクト タイプとデシリアライザーの間の対応を維持する IdentityHashMap があります。対応するデシリアライザーがない場合は、新しいデシリアライザーが作成され、IdentityHashMap に配置されます。デフォルトでは、グローバル設定 (com.alibaba.fastjson.parser.ParserConfig#global) が使用されます。

  • JSONLexter:実際に JSONScanner オブジェクトを指す基本クラスです。これは、JSON 文字列解析の中心となるオブジェクトです。文字列の解析結果に従って、最後まで 1 文字ずつ進みます。

DefaultJSONParser は、文字列がリスト型 (「[」で始まる) であるかオブジェクト型 (「{」で始まる) であるかを判断します。

  • リスト型の場合は、文字列を解析し、逆シリアル化後に JSONArray を返します。

  • オブジェクト型の場合、逆シリアル化は JSONObject を返します。逆シリアル化のプロセスにおける基本的な考え方は、for ループで JSONLexter を呼び出し、最初に JSON キーを解析し、次に JSON 値を解析することです。キー/値を JSONObject の内部マップに保存します。

解析のプロセスでは、parse(String) には特定のクラス オブジェクトの構築が含まれないため、通常、デシリアライザーの呼び出しは含まれません。

3.2.2 parseObject(String, Clazz) の仕組み

JavaBeanDeserializer はどのように作成されますか?

上記の parse(String) インターフェイスと比較すると、このインターフェイスの動作原理はより複雑です。これは、主に ParseConfig クラスで完了する Clazz に対応する JavaBeanDeserializer の作成が含まれるためです。

JavaBeanSerializer の作成プロセスと同様に、Fastjson は、シリアル化する必要があるカスタム Java クラスごとに JavaBeanDeserializer オブジェクトを作成するか、ASM テクノロジを通じて (効率とパフォーマンスのため) JavaBeanDeserializer サブクラス オブジェクトを動的に生成し、コアの逆シリアル化を完了します。動作します。JavaBeanDeserializer サブクラスの命名規則は、FastjsonASMDeserializer_<乱数>_<元のクラス名>です (例: FastjsonASMDeserializer_1_ObjectWithCollection)。

JavaBeanDeserializerのサブクラスオブジェクトをASM技術で作成するかどうかも主に「カテゴリ」に依存します。次のコードは、JavaBeanDeserializer の作成プロセスを明確に示しています。asmEnable 変数はデフォルトで true であり、Fastjson は「カテゴリ」に基づいて一連の判断を行い、ASM 関数を使用するかどうかを決定します。

次の図は、JavaBeanDeserializer とそれに依存する JavaBeanInfo および FieldDeserializer の関係を示しています。

JavaBeanDeserializer を作成する主な手順は、次のように要約できます。

1) Clazz クラス タイプに従って、ParseConfig の IdentityHashMap で対応する Deserializer を見つけ、存在する場合はそれを直接返します。

2) そうでない場合は、以下の作成手順に進みます。

  • com.alibaba.fastjson.util.JavaBeanInfo#build(...) を呼び出して JavaBeanInfo を作成します。このステップは非常に重要であり、Java クラスの「メタ情報」が抽出されるのはこのステップです。

    1. 反映に従って、Clazz 型に対応するデフォルトのコンストラクター、セッター関数、ゲッター関数、およびその他の情報を見つけます。

    2. setter/getter関数から、対応するメンバ変数(フィールド)を探し、setter/getter/フィールドがJSONFieldアノテーションで修飾されているかどうかを判定します。

    3. ゲッター/セッターごとに、FieldInfo (重複排除されます) が作成され、FieldInfo フィールドの配列が構築されます。FieldInfo には、各メンバー変数の名前 (変数名)、フィールド (Java リフレクション フィールド情報)、メソッド (変数にアクセスするための Java 関数リフレクション情報)、fieldClass (変数の型)、fieldAnnotation (メンバー変数が JSONField によって変更されているかどうか、その情報)やその他多くの「メタ情報」。

  • ASM 機能が有効な場合は、ASMDeserializerFactory を通じて JavaBeanDeserializer を作成します。JavaBeanDeserializer コンストラクターで、JavaBeanInfo のフィールド リストに従って、メンバー変数を逆シリアル化するための fieldDeserializer の配列を作成します。

  • FieldDeserializer の ObjectDeserializer 変数はデフォルトでは null であり、初めて使用されるまで初期化されません。

3) 次回使用するために、作成したデシリアライザーを ParseConfig に入れます。

デシリアライズの実行 Deserializer

Clazz 情報に従って対応するデシリアライザーが見つかったら、それは非常に簡単で、デシリアライザーの deserialze(...) を直接呼び出してシリアル化を完了します。Fastjson には 30 を超えるデシリアライザーが組み込まれており、一般的に使用される Java オブジェクトは基本的にデフォルトでサポートされています。

FastjsonASMDeserializer の実行:

ASM バイトコード テクノロジを通じて動的に作成された FastjsonASMDeserializer は、特定の Java カスタム クラス (例として上記の ObjectWithCollection クラスを取り上げます) に直接関連しており、実行プロセスも「数千のクラスとフェイス」になります。

  • ObjectWithCollection オブジェクトを直接新規作成します (オブジェクト T であると仮定します)。

  • 生成された JSON キー (ここでは「col1:」、「col2:」を指します) に従って JSON 文字列を 1 つずつ照合し、各 JSON キーに対応する Java オブジェクトを逆シリアル化して、オブジェクト T に設定してみます。以下の図は、逆コンパイルされたコードを示しています。

 

 

  • JavaBeanDeserializer の parseRest(...) インターフェースを呼び出し、文字列の終わりまで残りの JSON 文字列の逆シリアル化を続け、オブジェクト T を設定して返します。

JavaBeanDeserializer の実行:

JavaBeanDeserializer の実行は、メンバー変数ごとに FieldDeserializer を実行する必要があるため、比較的複雑になります。主なプロセスは次のように要約されます (コード com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze)。

  • Java リフレクションを通じて、クラス オブジェクトのデフォルト コンストラクターを呼び出して、クラス オブジェクト (オブジェクト T など) を作成します。デフォルトのコンストラクタがない場合は、まずMapオブジェクト(M)を作成し、作成したメンバ変数オブジェクト(フィールド値)を保存します。

  • for ループで、各 FieldDeserializer (FieldDeserializer は配列内にあり、ソートされている) とそれに関連付けられた fieldInfo を取得するためにトラバースします。fieldInfoから各メンバー変数の型(fieldClass)と対応するJSONキー名(「ABC」:など)を取得します。JSONLexer を使用して、現在の JSON 文字列内のキー (たとえば、文字列は "ABC":123) が JSON キー名と等しいかどうかを判断します。等しい場合は、メンバー変数オブジェクト (フィールド値) を直接解析し、オブジェクト T に設定するか、M に格納します。

  • すべての FieldDeserializer が走査された後、JSON 文字列が解析されていない場合、JSONLexer は現在の文字列 (「XYZ」:123 など) に対応する JSON キー (「XYZ」:) を解析するように駆動されます。JSON キーを通じて fieldDeserializer を見つけ、fieldDeserializer を通じて現在の文字列の解析を続けます。

  • JSON 文字列の解析が終了するか例外がスローされるまで、for ループを続けます。

以下では、FastJsonDemoObject クラスを使用して、このクラスのオブジェクトとそのメンバー変数が逆シリアル化されるときに呼び出されるデシリアライザーを示しています。

3.2.3 デシリアライザーをカスタマイズするにはどうすればよいですか?

上記の分析から、FastJson のデシリアライザーはすべて ObjectDeserializer インターフェイスを実装しており、PaserConfig から取得されていることがわかります。これらの原則を習得すると、独自のデシリアライザーを定義することが「便利」になります。引き続き上記の DummyEnum を例として、デシリアライゼーション DummyEnumDeserializer を次のように定義します。

public class DummyEnumDeserializer implements ObjectDeserializer {
    /**
     * 从int值反序列化Dummy Enum
     *
     * @param parser
     * @param type
     * @param fieldName
     * @param <T>
     * @return
     */
    @Override
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        final JSONLexer lexer = parser.lexer;
        int intValue = lexer.intValue();
        lexer.nextToken(JSONToken.COMMA);
        DummyEnum dummyEnum = DummyEnum.from(intValue);
        System.out.println("DummyEnumDeserializer executed");
        if (dummyEnum != null) {
            return (T) dummyEnum;
        } else {
            return null;
        }
    }
​
    /**
     * 获取当前json字符串位置的token标志值
     *
     * @return
     */
    @Override
    public int getFastMatchToken() {
        return JSONToken.LITERAL_INT;
    }
}

最後に、DummyEnumDeserializer オブジェクトを作成し、ParserConfig に挿入します。

@Test
public void testFastJsonSerializeDummyEnum() {
    try {
        DummyEnum dummyEnum = DummyEnum.UNKNOWN;
​
        // 把DummyEnumSerializer插入到全局的SerializeConfig中
        SerializeConfig globalConfig = SerializeConfig.getGlobalInstance();
        globalConfig.put(DummyEnum.class, new DummyEnumSerializer());
​
        // 把DummyEnumDeserializer插入到全局的ParserConfig中
        ParserConfig parserConfig = ParserConfig.getGlobalInstance();
        parserConfig.putDeserializer(DummyEnum.class, new DummyEnumDeserializer());
​
        String dummyEnumStr = JSON.toJSONString(dummyEnum,
                                                SerializerFeature.WriteMapNullValue);
        System.out.println("FastJson: dummyEnumStr: " + dummyEnumStr);
​
        DummyEnum deserializedEnum = JSON.parseObject(dummyEnumStr, DummyEnum.class);
        System.out.println("FastJson: deserializedEnum desc: " + deserializedEnum.getDesc());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上記のテストケースを更新すると、実行結果出力は以下のようになります。DummyEnumDeserializer が正常に呼び出され、DummyEnum が正常にデシリアライズできることがわかります。

FastJson: dummyEnumStr: 2
DummyEnumDeserializer executed
FastJson: deserializedEnum desc: Don't Know

グローバル SerializeConfig/ParserConfig を直接変更して Serializer/Deserializer を登録するだけでなく、FastJson は JSONType アノテーションを通じて Serializer/Deserializer を指定する方法も提供しており、これはより使いやすいものです。以下に示すように。

クラスにJSONTypeを指定したアノテーションがある場合、デシリアライザーを作成する際にはアノテーションに指定されたクラスが優先して作成されます。

3.2.4 デシリアライズ時にゲッター関数が呼び出されるのはなぜですか?

parseObject(String, Clazz) インターフェイスの逆シリアル化では、特定の状況下でクラス オブジェクトのゲッター関数が呼び出されることを前に説明しました。デシリアライズする場合、クラスオブジェクトのメンバ変数を設定するため、setter関数が呼び出されることになりますが、これは当然です。しかし、なぜゲッター関数が呼び出されるのか、私はまだ理解したくありません。後でコードの中に小さなヒントを見つけました。メンバー変数を逆シリアル化するために FieldDeserializer が呼び出されるたびに、setValue 関数が呼び出され、メンバー変数の値がオブジェクトに設定されます。次の 2 つの条件が満たされると、メンバー変数に対応するゲッター関数が呼び出されます。

  • このメンバー変数に対応するセッター関数はありません (セッターがある場合、そのセッターが直接呼び出され、「救国曲線」は実行されません)。

  • メンバー変数の型は AtomicInteger/AtomicLong/AtomicBoolean/Map/Collection です (これらの型のオブジェクトは変更可能であり、値を変更するための共通のインターフェイスがあることを理解しています)。

 

 

3.2.5 Collection 型のデシリアライズ結果が「クラス」に依存するのはなぜですか?

上記の注 2 で述べたように、Collection 型のメンバー変数に対する Fastjson の逆シリアル化動作は、条件が異なると一貫性がありません。Collection型のメンバ変数はデシリアライズ後、ArrayListを指すのかHashSetを指すのかは「カテゴリ」に依存します。Fastjsonはデシリアライズする際に、特定のクラスの「カテゴリー」に応じてASM機能を有効にするかどうかを判断します。ASM 機能を有効にできると判断された場合は、ASM バイトコード操作テクノロジを使用して特定の ASM Deserializer オブジェクトが生成されます。それ以外の場合は、デフォルトの JavaBeanDeserializer オブジェクトが使用されます。異なるデシリアライザーを使用すると、異なるデシリアライズ結果が得られます。

ASM デシリアライザの使用

ASM 機能が有効になっている場合、Fastjson は特定の ASM デシリアライザーを動的に作成します。これはFastjsonASMDeserializer_1_ObjectWithCollection の例です (これも JavaBeanDeserializer から継承されています)。コードは次のとおりです。

上記のコードが ObjectWithCollection クラス (Collection<String> 型) のメンバー変数col1 を解析すると、デフォルトで JSONLexer の scanFieldStringArray(...) 関数が呼び出されます。scanFieldStringArray 関数は newCollectionByType(...) を呼び出して、特定のコレクション タイプを作成します。

newCollectionByType のデフォルトは、最初に返された HashSet タイプのオブジェクトです。これは、上記のポイント 2 の問題も説明します。

ASM デシリアライザーでは、Json 配列の型が明示的に指定されていない限り (たとえば、指定された型が Set の場合、Fastjson のトークン値は 21)、他の非文字列型のコレクション メンバー変数は、デフォルトで ArrayList を使用して逆シリアル化されます。

デフォルトの JavaBeanDeserializer を使用する

Json 配列の場合、デフォルトの JavaBeanDeserializer が使用される場合、通常、CollectionCodec Deserializer が呼び出されて逆シリアル化され、createCollection 関数はデフォルトで ArrayList タイプを返します。これは私たちの認識と一致するので、ここでは繰り返しません。

4番目、最後に書きます

全体として、Fastjson は依然として非常に強力で使いやすいです。また、Fastjson の使いやすさのせいで、その内部動作原理に関する研究が無視され、特定の使用シナリオで発生する可能性のある「問題」に注意が払われていない可能性があります。この記事の最初の部分では、Fastjson を使用する際のいくつかの注意事項を、特定の検証コードや Jackson との比較と組み合わせて要約するために多くのスペースを費やしています。これは、Fastjson の使用法を完全に理解し、特定のシナリオでの使用方法を調整し、柔軟に対応させていただきます。2 番目のパートでは、Fastjson の基礎となる動作原理を簡単に分析します。重要な点は、シリアライザー/デシリアライザーの作成と実行をマスターすることです。全体的なコード構造は比較的統一されていて明確で、設計は独創的で、サポートされている機能は非常に豊富です。Fastjson を「深く」理解し、より多くの設計方法を習得できるように支援できれば幸いです。

上記の調査と分析は Fastjson の機能のごく一部のみをカバーしており、多くの機能と詳細はまだカバーされていません。上記はすべて私自身の調査と理解に基づいて書かれていますが、ご家族の言葉に虚偽がある場合は、修正して伝え、一緒に学びましょう。

おすすめ

転載: blog.csdn.net/AlibabaTech1024/article/details/128966767