Fastjson avanza en los registros "pit" y el aprendizaje "profundo"

Autor: equipo técnico comercial de la estación internacional Tao Zhengce Ali

Fastjson es una biblioteca JSON de alto rendimiento escrita en lenguaje Java desarrollada por Ali. Este artículo resume algunas precauciones al usar Fastjson y analiza brevemente el principio de funcionamiento subyacente de Fastjson, combinado con códigos de verificación específicos y una comparación con Jackson, con la esperanza de ayudar a todos. comprender el uso de Fastjson.

1. ¿Por qué escribiste este artículo?

Fastjson es una biblioteca JSON de alto rendimiento escrita en lenguaje Java desarrollada por Ali. Se utiliza para convertir datos entre JSON y Java Object. Proporciona dos interfaces principales JSON.toJSONString y JSON.parseObject para implementar operaciones de serialización y deserialización respectivamente. , muy conveniente de usar.

Recientemente, estoy actualizando la arquitectura de caché de un sistema antiguo, usando Fastjson para serializar objetos y almacenarlos en el caché, y usarlos después de la deserialización en el lado del cliente para reducir la cantidad de solicitudes al backend hsf. En el proceso de usar Fastjson, pisé algunas "trampas", lo que supuso muchos problemas, y aproveché esta oportunidad para aprender "profundamente" FastJson. Estos "trampas" no son errores de Fastjson en sí mismos, sino algunas precauciones de uso que generalmente se pasan por alto fácilmente. Por lo tanto, se resumen y registran algunas precauciones para referencia futura. También espero que más estudiantes puedan entender cómo usar Fastjson correctamente.

Jackson es otra librería JSON potente, que se utiliza con frecuencia en el trabajo diario, por lo que, para estas precauciones de uso de Fastjson, se ha añadido una comparación horizontal con Jackson. La versión de Fastjson utilizada en este artículo es 1.2.68.noneautotype, que no es compatible con AutoType.

Segundo, saberlo, cómo usarlo correctamente.

Nota 1: Deserialización de propiedades/valores nulos

Cuando se serializa Fastjson, de forma predeterminada, no generará el atributo nulo en el objeto y la clave cuyo valor es nulo en el Mapa. ​​Si desea generar el atributo nulo, puede agregar el parámetro SerializerFeature.WriteMapNullValue al llamar al JSON Función .toJSONString (habilitar función).

En general (por ejemplo, cuando se imprimen registros), si el atributo/valor nulo se serializa o no tiene poco efecto. Pero si la cadena serializada debe deserializarse en un objeto, se debe prestar especial atención en este momento, especialmente cuando se trata de la serialización de estructuras de datos como Java Map/JSONObject. El código se muestra a continuación.

@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");
        }
    }
}

El resultado después de ejecutar el código anterior es el siguiente:

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

Como puede ver, el objeto original no es el mismo que el objeto deserializado. El motivo es que el tamaño del objeto original (mapWithNullValue, jsonWithNullValue) es 2 y el tamaño del objeto deserializado es 0.

En algunos escenarios comerciales (como el almacenamiento en caché de objetos en Tair después de la serialización), es necesario asegurarse de que el objeto original y el objeto deserializado sean estrictamente iguales, por lo que debemos prestar especial atención a este problema. Este problema se puede evitar agregando el parámetro SerializerFeature.WriteMapNullValue durante la serialización.

Comparación con Jackson

El mismo objeto, si usamos Jackson para serializar y deserializar, el resultado es el mismo. El código Jackson es el siguiente:

@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");
        }
    }
}

Los resultados se muestran de la siguiente manera: puede ver que Jackson generará valores nulos de forma predeterminada.

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

Recomendaciones

  • Si se trata de la deserialización de objetos, es mejor agregar el parámetro SerializerFeature.WriteMapNullValue al llamar a JSON.toJSONString.

Nota 2: deserialización de objetos de colección

Este es otro "pozo" que encontré cuando lo estaba usando. Me ha preocupado durante mucho tiempo y es difícil de encontrar. En un objeto Java, si se incluye una variable miembro del tipo Collection, es posible que el objeto original no sea exactamente igual al objeto deserializado. Continúe mirando el código de verificación a continuación.

Class ObjectWithCollection es una clase POJO que define dos propiedades de tipo Collection.

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);
    }
}

El siguiente código intenta serializar el objeto objectWithCollection. La propiedad col1 del objeto objectWithCollection se establece en una variable ArrayList y la propiedad col2 se establece en una variable 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");
    }
}

El resultado después de ejecutar el código anterior es el siguiente:

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

Después de una "investigación en profundidad", se encuentra que en la mayoría de los casos (cuando ASM está habilitado, la función ASM está habilitada de manera predeterminada), Fastjson usa el tipo HashSet para deserializar Json Array del tipo de cadena y usa el tipo ArrayList para deserializar Json Array de otros tipos (largo/entero/doble/tipos de Java personalizados, etc.). El tipo real de la variable col1 del objeto deserializedObj deserializado arriba es HashSet, lo que genera una diferencia con el objeto original.

¿Por qué ocurre este problema? ¿Es porque el tipo específico correspondiente a la variable Collection no se genera durante la serialización? Si un tipo de objeto específico se serializa y genera (Fastjson admite esta función, pero está desactivada de manera predeterminada), ¿Fastjson puede deserializarlo correctamente? Actualice el código y agregue el parámetro SerializerFeature.WriteClassName cuando llame al método JSON.toJSONString.

@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");
    }
}

Intente ejecutarlo de nuevo, y el resultado es el siguiente. Se puede ver que Fastjson genera correctamente el tipo del objeto objectWithCollection y genera correctamente el tipo de la variable miembro col2 ("col2":Set[22L], preste atención a la palabra clave Set), pero no ingresa el tipo específico de la variable miembro col1, por lo que la secuencia inversa El objeto transformado sigue siendo diferente del objeto original.

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

Comparación con Jackson

Para el mismo objeto, intentemos serializarlo/deserializarlo con Jackson. El código específico es el siguiente:

@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");
    }
}

Los resultados de la ejecución del código son los siguientes, y se encuentra que los objetos deserializados también son diferentes.

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

Vuelva a intentar generar el tipo de objeto cuando Jacskon serialice. Para generar el tipo, debemos configurar objectMapper, agregar una línea de código de la siguiente manera.

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

Ejecutar de nuevo, muy sorprendido, el objeto deserializado es el mismo. La salida es la siguiente. Se puede ver que Jackson también puede generar un tipo específico para las variables de tipo Colección, que es la principal diferencia con FastJson. Según este tipo, Jackson puede deserializar correctamente el objeto, asegurándose de que sea coherente con el objeto original.

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

Recomendaciones

  • Al definir objetos (POJO) que deben serializarse, evite usar el tipo Colección y use List/Set en su lugar, lo que puede evitar muchos "problemas".

  • Para código/objetos históricos antiguos de otras bibliotecas de terceros que dependen de él, si se ha utilizado el tipo Colección, se recomienda utilizar Jackson para serializar/deserializar para evitar "trampas" innecesarias.

  • El comportamiento de deserialización de Fastjson para variables miembro del tipo Collection no es consistente bajo diferentes condiciones, esto es un poco difícil de comprender, este problema se explicará en detalle en los siguientes capítulos.

Nota 3: falta el constructor/setter predeterminado, no se puede deserializar

En el proceso de usar Fastjson, otro "poco" encontrado es que para un objeto, la serialización es exitosa, pero la deserialización siempre falla. Después de estudiar el código correspondiente a este objeto, encontré algunas diferencias con otros objetos: la falta de constructores y setters predeterminados correspondientes a las variables. Sin el soporte de estas funciones, Fastjson no puede deserializar con éxito, pero no arrojará una excepción (un poco extraño, no tiene éxito en silencio) . Vea el código de verificación a continuación:

La clase ObjectWithOutSetter es una clase sin un constructor predeterminado (pero tiene otros constructores con parámetros) y un setter.El código específico es el siguiente:

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");
    }
}

Después de ejecutar el código de verificación anterior, los resultados de salida son los siguientes. Puede ver que las variables var1 y var2 del deserializedObj son nulas y la deserialización no se realizó correctamente.

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

Si elimina los comentarios del código constructor y setter predeterminado del tipo ObjectWithOutSetter anterior y luego vuelve a ejecutar el código de prueba anterior, encontrará que los objetos deserializados son consistentes. La salida específica es la siguiente:

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

La deserialización de Fastjson debe basarse en el constructor predeterminado del objeto de clase y el definidor de la variable miembro; de lo contrario, fallará. En resumen, hay varios escenarios que "pueden" fallar:

  • Si el objeto de clase carece de un constructor predeterminado, la deserialización definitivamente fallará (no se lanzará ninguna excepción, pero el valor de la variable miembro será nulo).

  • Si la variable miembro privada del objeto de clase carece de un setter, la deserialización definitivamente fallará, a menos que se agregue el parámetro Feature.SupportNonPublicField cuando la deserialización llame a JSON.parseObject. En un caso especial, para las variables miembro del tipo AtomicInteger/AtomicLong/AtomicBoolean/Map/Collection, si falta el setter correspondiente, la deserialización puede ser exitosa.

  • De manera similar, si un objeto de clase no tiene un getter, la serialización fallará (no se lanzará ninguna excepción y se generará una cadena vacía "{}").

Para las variables miembro públicas de los objetos de clase, incluso si no hay un setter, se pueden deserializar correctamente.

Comparación con Jackson

El mismo objeto ObjectWithOutSetter (sin setter) se reemplaza con Jackson para la serialización/deserialización, y el código de verificación es el siguiente:

@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");
    }
}


La salida es la siguiente. Como puede ver, incluso sin un setter, Jackson puede deserializar correctamente.

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

Después de varias verificaciones repetidas, la serialización/deserialización de Jackson se resume de la siguiente manera:

  • Si el objeto de clase no tiene ninguna variable anotada getter/@JsonProperty, la serialización fallará. Se lanzará una excepción com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

  • Si el objeto de clase carece de un constructor predeterminado, la deserialización fallará. Se lanzará una excepción com.fasterxml.jackson.databind.exc.InvalidDefinitionException. Personalmente, creo que este diseño es más razonable y puede ayudarnos a encontrar problemas lo antes posible.

  • Si el objeto de clase carece de la función de establecimiento correspondiente, la deserialización seguirá teniendo éxito. Jackson usa de forma predeterminada Java Reflection para establecer variables miembro. En este punto, siento que Jackson todavía es muy fuerte.

Tanto Fastjson como Jackson necesitan confiar en la función getter del objeto para completar la serialización.

Recomendaciones

  • Para los objetos que necesitan ser serializados por Fastjson, se debe definir la función getter de la variable miembro. De lo contrario, no se puede serializar.

  • Para los objetos que deben deserializarse con Fastjson, asegúrese de definir un constructor predeterminado y una función de establecimiento para las variables miembro; de lo contrario, fallará. Al definir un objeto, es mejor definir el constructor, el setter y el getter predeterminados, lo que evitará muchos problemas.

  • Para el código antiguo histórico, si no hay una función de establecimiento correspondiente, puede considerar usar Jackson para deserializar.

  • Para el código antiguo histórico, si faltan el constructor predeterminado y la función getter, la deserialización fallará independientemente de si se usa Fastjson o Jackson, y el código solo se puede cambiar.

Nota 4: Deserialización de clases/interfaces abstractas

Este punto de atención en realidad está algo relacionado con el punto de atención 3 anterior. Debido a que Fastjson necesita confiar en el constructor predeterminado y la función de establecimiento al deserializar, si no puede "construir" un objeto de clase, la deserialización definitivamente fallará. Por ejemplo, cuando el tipo de objeto es una interfaz (Interfaz) o una clase abstracta (Clase abstracta), el objeto correspondiente no se puede construir y, naturalmente, la deserialización fallará. Consulte la verificación del código a continuación.

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());
    }
}

El "punto clave" del código anterior son los tipos de las variables interfaceObject y abstractClass en la clase CompositeObject, que son interfaz y clase abstracta (ambos tipos de clase base) respectivamente. Después de ejecutar el código de verificación, el resultado es el siguiente y se puede ver que la deserialización falló.

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

Del resultado anterior, también podemos encontrar que para las variables de interfaz/clase abstracta, el comportamiento de la deserialización sigue siendo diferente. En el objeto deserializado deserializedObj, el valor de la variable de clase abstracta abstractClass es nulo, pero la variable de tipo de interfaz interfaceObject no es nulo. El juicio es que Fastjson puede crear automáticamente clases de proxy (com.sun.proxy.*) de acuerdo con la interfaz para admitir la deserialización.

Si cambio las variables interfaceObject y abstractClass en la clase CompositeObject al tipo de subclase, la serialización/deserialización funciona bien.

También puede agregar el parámetro SerializerFeature.WriteClassName durante la serialización, de modo que la cadena serializada contenga información de clase específica, pero se generará una excepción de "modo seguro no compatible con autotipo" durante la deserialización. Fastjson ya no admite la deserialización de clases personalizadas basadas en autotipo.

[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)

Comparación con Jackson

Si Jackson serializa el mismo objeto CompositeObject, también fallará y se lanzará una InvalidDefinitionException directamente (ver más abajo). Jackson tampoco admite la deserialización de interfaces/clases abstractas.

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

Pero si agrega información de clase específica al serializar, Jackson puede funcionar normalmente. Vea el código de verificación a continuación:

@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());
    }
}

La salida es la siguiente:

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

Recomendaciones

  • Al definir un objeto que Fastjson debe serializar, no utilice el tipo de interfaz (Interfaz)/clase abstracta (Clase abstracta) para definir las variables miembro, de lo contrario, la deserialización fallará. No utilice el tipo de clase base (Base Class) para definir variables miembro, porque la información de la subclase se perderá cuando se deserialice.

  • Ya no se recomienda la función AutoType de Fastjson. De forma predeterminada, solo se admiten clases nativas de JDK y ya no se admite la deserialización de clases Java personalizadas basadas en AutoType.

  • Para el código antiguo histórico, si el tipo de interfaz/clase abstracta/clase base se ha utilizado para definir variables miembro, puede considerar usar Jackson para la serialización, pero se debe generar el tipo específico del objeto.

Nota 5: Serialización/Deserialización de getters/setters "falsos"

A través del análisis de algunos puntos de atención anteriores, ya sabemos que la serialización/deserialización de Fastjson depende de las funciones predeterminadas de constructor, captador y definidor de objetos. Sin embargo, en algunos objetos de clase, a menudo puede ver funciones como setXXX()/getXXX(). Estas funciones en sí mismas no devuelven/establecen un objeto directamente, pero tienen alguna lógica de procesamiento interno (la lógica puede ser compleja o simple), simplemente lleva el nombre de set/get. Estas funciones a menudo conducen a fallas en la serialización/deserialización e incluso causan fallas en la seguridad del sistema en casos más serios (algunas fallas en la seguridad en Fastjson son causadas por la función AutoType y funciones específicas del setter, como el muy común com .sun.rowset. JdbcRowSetImpl). Dado que estas funciones no se corresponden directamente con una variable miembro, las llamaré captadores/establecedores "falsos".

Un ejemplo típico es que el objeto de mensaje com.alibaba.rocketmq.common.message.MessageExt de Ali MetaQ (esta es una clase base que en realidad apunta a com.alibaba.rocketmq.common.message.MessageExtBatch) no se puede serializar. será arrojado. Mucha gente debería haber pisado esta trampa, por eso, las normas de desarrollo de Ali han estipulado claramente que está prohibido usar herramientas JSON directamente para convertir objetos en Strings al imprimir registros.

Verifiquemos este problema a través del código. En la siguiente clase, se define un getter "falso": 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);
}

Después de ejecutar el código de verificación anterior, Fastjson lanza directamente una ArithmeticException al ejecutar la lógica de getWired() y la serialización falla.

[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)

La función getWired() anterior es solo una demostración simple. Para ampliarlo aún más, para las funciones de captador con lógica de procesamiento "compleja" (como llamar a hsf en el captador, escribir en la base de datos, etc.), Fastjson a menudo tiene resultados "inesperados" al serializar, lo que generalmente no es lo esperado.

¿Cómo resolver este problema? Al llamar a JSON.toJSONString, agregue el parámetro SerializerFeature.IgnoreNonFieldGetter para ignorar todas las funciones getter que no tienen variables miembro correspondientes (Field) y luego serialice normalmente. También se puede lograr el mismo efecto agregando la anotación @JSONField(serialize = false) a la función 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);
}

Después de agregar los parámetros, ejecute el código de verificación nuevamente, el resultado es el siguiente y la serialización es exitosa.

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

Comparación con Jackson

La serialización de Jackson también depende del captador.Si el mismo objeto se serializa con Jackson, vea qué sucede.

@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);
}

Después de que se ejecuta el código de verificación, Jackson lanza directamente una excepción JsonMappingException, de la siguiente manera. De forma predeterminada, Jackson no puede manejar bien la serialización de funciones captadoras tan "complejas".

[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)

Por supuesto, Jackson también puede resolver fácilmente este problema a través de la configuración. Agregue la anotación @JsonIgnore directamente a la función getWired y se serializará correctamente.

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

Recomendaciones

  • Ni Fastjson ni Jackson, por defecto, pueden manejar muy bien la serialización de las funciones getter "falsas" (la deserialización correspondiente a la función setter falsa también es la misma). Para los objetos que deben serializarse o deserializarse, intente no definir captadores/establecedores "falsos" y no permita que los captadores/establecedores contengan una lógica de procesamiento "compleja". Puro POJO sería mejor, hazlo simple.

  • Para getters/setters "falsos" (sin funciones setter/getter correspondientes a variables miembro), generalmente no es necesario participar en la serialización/deserialización. Se pueden filtrar durante la fase de serialización a través del parámetro SerializerFeature.IgnoreNonFieldGetter. En la mayoría de los escenarios, agregar el parámetro SerializerFeature.IgnoreNonFieldGetter será más seguro y evitará algunos problemas "extraños". También se pueden ignorar en forma de anotación @JSONField.

Nota 6: Serialización del mismo objeto de referencia

Fastjson tiene una función muy potente: detección de referencia circular (habilitada de forma predeterminada). Por ejemplo, hay dos objetos A y B (como se muestra en el siguiente código). A contiene una referencia al objeto B, B contiene una referencia al objeto A y los dos objetos A/B se convierten en referencias circulares. Si serializa en este momento, generalmente encontrará el problema de StackOverflowError. Precisamente porque Fastjson tiene la capacidad de detectar referencias circulares, el objeto A puede serializarse "con éxito". Vamos a verificarlo con el siguiente código:

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);
}

La salida después de la ejecución es la siguiente. El objeto A se serializa "con éxito" en una representación de cadena "extraña". Esto es exactamente lo que hace la detección de referencia circular. Si se detecta una referencia circular, el objeto al que se hace referencia se representa con "$ref".

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

Fastjson admite un total de 4 referencias a objetos, de la siguiente manera.

La capacidad de detectar referencias circulares es realmente muy poderosa, pero a veces la "capacidad" es demasiado fuerte, "destrozará a los inocentes" y provocará algunos homicidios involuntarios. Por ejemplo, el objeto obviamente no tiene una "referencia circular", pero todavía está serializado de acuerdo con el modo de referencia. Vea el código a continuación.

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");
    }
}

La salida es la siguiente. Se puede ver que el mismo objeto RefObjectDemo se serializa en una cadena de referencia.

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

El objeto sameRefObjectDemo no contiene referencias circulares, solo hace referencia repetidamente al mismo objeto "refObject" (lo llamo el mismo objeto de referencia). Esta forma de objeto Java es bastante común en la lógica comercial diaria. Si se serializa de acuerdo con el modo de "referencia", causará algunos impactos, como interacciones de front-end y back-end, y el front-end no puede analizar esta cadena Json "extraña". Por ejemplo, en la interacción entre sistemas heterogéneos se utilizan diferentes frameworks Json, lo que provocará fallas en la comunicación entre ellos.

¿Por qué debería serializarse el mismo objeto de referencia de acuerdo con el modo de "referencia circular"? Todo lo que puedo pensar es reducir la longitud de salida del resultado serializado y reducir la sobrecarga de transmisión de la red. Hay ventajas y desventajas.

Si el parámetro SerializerFeature.DisableCircularReferenceDetect se agrega a la función JSON.toJSONString, la función de detección de "referencia circular" se puede deshabilitar y la salida normal es la siguiente:

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

Comparación con Jackson

De forma predeterminada, Jackson no admite la serialización de objetos de "referencia circular" y arrojará un error StackOverflowError (como se muestra a continuación):

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

Pero proporciona muchas anotaciones que pueden resolver este problema. Este artículo "Jackson – Relaciones bidireccionales" explica varias opciones en detalle. Aquí verificamos un método: use las anotaciones @JsonManagedReference y @JsonBackReference. El código específico es el siguiente:

Enlace de referencia "Jackson – Relaciones bidireccionales":

https://www.baeldung.com/jackson-bidireccional-relaciones-y-recursión-infinita

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)"));
    }
}

La salida es la siguiente. Se puede ver que el objeto demoA se puede serializar correctamente, y en el objeto deserializado deserializedObj, la variable b tiene el valor correcto.

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

Recomendaciones

  • Al interactuar con el sistema front-end a través de la interfaz HTTP, si se usa la serialización Fastjson, intente configurar el parámetro SerializerFeature.DisableCircularReferenceDetect para deshabilitar la capacidad de detección de "referencia circular".

  • Las "referencias circulares" generalmente indican un mal diseño y deben refactorizarse. Use el parámetro SerializerFeature.DisableCircularReferenceDetect para deshabilitar la capacidad de detección de "referencia circular", de modo que el problema pueda exponerse y descubrirse lo antes posible.

  • Para escenarios que realmente necesitan lidiar con "referencias circulares", Fastjson será más conveniente de usar.

3. Saber por qué, romper la cazuela y pedir el final

En el espacio grande de arriba, se enumeran algunas precauciones para usar Fastjson y se presenta cómo usarlo de manera más razonable, pero todavía estamos en la etapa de "saberlo". También quedan algunas preguntas en las precauciones anteriores, que aún no han sido respondidas. Para responder a estas preguntas y comprender más profundamente el principio de funcionamiento subyacente de Fastjson, también necesitamos "saber por qué", para que podamos usarlo de manera práctica y flexible.

3.1 ¿Cómo se serializan los objetos?

Utilice nuestro JSON.toJSONString de uso común para analizar la serialización de objetos Java, que involucra varios objetos clave de Fastjson, y la relación entre ellos es la siguiente:

En el diagrama de clases anterior, las clases principales son SerializeConfig y JavaBeanSerializer. SerializeConfig tiene dos responsabilidades principales:

  • Se mantiene un IdentityHashMap que almacena la relación entre las diferentes clases de Java y sus correspondientes Serializadores. Cada vez que se llame a la serialización JSON.toJSONString, se encontrará el serializador correspondiente.

  • Si no se puede encontrar el serializador de un objeto de clase (generalmente un objeto Java personalizado), se recreará un JavaBeanSerializer y se colocará en IdentityHashMap para su próximo uso.

JavaBeanSerializer se utiliza principalmente para serializar objetos Java personalizados. Después de que Fastjson encuentra el serializador de la clase correspondiente de SerializeConfig, llama directamente a la interfaz de escritura del serializador para completar la serialización. Un objeto de clase personalizado, cada uno de sus objetos de variable miembro (campo) tendrá su FieldSerializer correspondiente. FieldSerializer se llama a su vez durante la serialización de objetos de clase.La siguiente figura muestra el Serializador que un simple objeto Java POJO necesitará serializar.

3.1.1 ¿Cuántos tipos de serializadores hay?

La serialización de un objeto personalizado de Java sigue siendo muy complicada, porque involucra muchos otros objetos personalizados de Java y tipos primitivos de Java. Para los tipos primitivos de Java, Fastjson básicamente define el Serializador correspondiente, que bien puede soportar su trabajo de serialización. Todas las clases que implementan la interfaz com.alibaba.fastjson.serializer.ObjectSerializer son serializadores que han sido definidos por Fastjson de forma predeterminada.Después de una mirada aproximada, hay alrededor de 44 de ellos.

3.1.2 ¿Cómo se crea JavaBeanSerializer?

JavaBeanSerializer es la clase principal de serialización Fastjson. De forma predeterminada, Fastjson creará un objeto JavaBeanSerializer para cada clase Java personalizada que deba serializarse o generará dinámicamente un objeto de subclase JavaBeanSerializer a través de la tecnología ASM (modificación de código de bytes), y completarán el trabajo de serialización central. La convención de nomenclatura de la subclase JavaBeanSerializer es: ASMSerializer_<Número aleatorio>_<Nombre de clase original> , por ejemplo: ASMSerializer_4_ObjectWithWiredGetter.

¿Por qué necesitamos crear objetos de subclase de JavaBeanSerializer a través de la tecnología ASM? Principalmente por eficiencia y rendimiento, que es una de las razones por las que Fastjson es rápido. El objeto de la subclase de JavaBeanSerializer creado por la tecnología ASM está altamente personalizado y estrechamente vinculado con la clase Java personalizada que debe serializarse, para que pueda lograr el mejor rendimiento.

La creación de un objeto de subclase de JavaBeanSerializer a través de la tecnología ASM depende principalmente de la "clase". Todo el mundo tiene carácter, y las clases también tienen "clases". El carácter determina la calidad de los resultados de nuestro trabajo y la "categoría" determina si la función ASM se puede utilizar durante la serialización. La simple comprensión de "categoría" es un atributo integral de la clase, que se determinará de manera integral en función de las características de la clase base de la clase, el nombre de la clase, el número de variables miembro, getter/setter, si es una interfaz, si se utilizan anotaciones JSONField, etc. El siguiente fragmento de código muestra el proceso de creación de JavaBeanSerializer, la variable asm es verdadera de forma predeterminada. Fastjson hará una serie de juicios para determinar el valor de la variable asm y luego decidirá si crear ASMSerializer.

La siguiente figura muestra la relación entre JavaBeanSerializer y sus dependientes SerializeBeanInfo y FieldSerializer.

La creación de JavaBeanSerializer depende de SerializeBeanInfo. SerializeBeanInfo solo se creará una vez para una clase Java (llame al método com.alibaba.fastjson.util.TypeUtils#buildBeanInfo para crear, seguido de JavaBeanSerializer), que incluye principalmente la siguiente información:

  • beanType: Es decir, el tipo real del objeto de la clase Java.

  • jsonType: si un objeto de clase Java está decorado con la anotación @JSONType, tiene un valor; de lo contrario, es nulo.

  • typeName: depende del jsonType anterior. Si hay un tipo especificado en la salida serializada en jsonType, tiene un valor.

  • características: depende del jsonType anterior. Tiene un valor si hay un SerializerFeature que especifica la salida serializada en jsonType.

  • campos: la información de campo/método de obtención que debe serializarse en el objeto de clase Java está incluida en él, y este campo es más crítico.

El objeto SerializeBeanInfo determina la salida de una serialización de objetos Java. JavaBeanSerializer creará la variable miembro correspondiente FieldSerializer de acuerdo con el campo de campos en el objeto SerializeBeanInfo.

El RuntimeSerializerInfo en FieldSerializer es nulo cuando se crea y se inicializa cuando realmente se usa, apuntando al serializador específico (modo de inicialización Lazy).

3.1.3 ¿Cómo personalizar el serializador?

Después de descubrir cómo se busca el serializador y cómo funciona, podemos "seguir la calabaza" y escribir nuestro propio serializador. Lo siguiente usa la clase DummyEnum para mostrar cómo escribir un serializador.

Primero cree la clase DummyEnum, de la siguiente manera:

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
}

Luego cree DummyEnumSerializer, Serializer necesita implementar la interfaz ObjectSerializer, el código es el siguiente:.

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());
        }
    }
}

Finalmente, simplemente cree un objeto DummyEnumSerializer y regístrelo en 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();
    }
}

La serialización predeterminada de la clase Enum es generar el nombre de la variable de enumeración (nombre). Después de ejecutar el código de verificación anterior, se emite el número 2, lo que indica que el nuevo serializador se ha ejecutado correctamente.

FastJson: dummyEnumStr: 2

3.2 ¿Cómo se deserializa la cadena?

Hay tres funciones principales que Fastjson admite la deserialización. Sus principales funciones y diferencias son las siguientes:

1) JSON.parse(String):  serializa directamente desde una cadena y devuelve un objeto Java.

  1. Si la cadena es generada por serialización de objetos Java de tipo Collection (Set/List, etc.) (similar a "[...]"), esta función devolverá un objeto JSONArray .

  2. Si la cadena se generó a partir de la serialización de objetos personalizados (como "{...}"), esta función devuelve un objeto JSONObject.

  3. Si el tipo de objeto (como Set/TreeSet, etc.) se especifica en la cadena, el objeto Java Set/TreeSet correspondiente se devolverá directamente.

2) JSON.parseObject(String): serialice directamente desde una cadena y devuelva un objeto JSONObject.

  1. Si la cadena se genera mediante la serialización de objetos Java de tipo Colección (Conjunto/Lista, etc.),  esta función arrojará una excepción : "com.alibaba.fastjson.JSONException: no se puede convertir a JSONObject".

3) JSON.parseObject(String, Clazz): según el tipo Java Clazz, deserialice la cadena en un objeto Java personalizado. También puede admitir la generación de objetos nativos de Java como List/Set/Map.

Dado que Fastjson ha desactivado AutoType de forma predeterminada (en la versión 1.2.68), sin considerar el impacto de las funciones de AutoType, la comparación entre estas tres interfaces es la siguiente:

 

3.2.1 Cómo funciona parseObject(String)

Entre las tres interfaces, las funciones de parse(String) y parseObject(String) son muy similares. Este último llamará al primero y convertirá el resultado del último en el JSONObject correspondiente. Aquí nos enfocamos en analizar el principio de funcionamiento de la interfaz parseObject(String).

La figura anterior muestra el código principal de la interfaz parseObject(String), que se divide principalmente en dos pasos:

  • Llame a la interfaz parse(String) para deserializar la cadena en un objeto

  • Llame a la interfaz JSON.toJSON para convertir el objeto del paso 1 en JSONObject

La interfaz parse(String) tiene muy poco código, como se muestra en la siguiente captura de pantalla, y su núcleo es DefaultJSONParser.

DefaultJSONParser es la clase de controlador principal para la deserialización, que contiene dos objetos principales:

  • ParseConfig: la clase de configuración central para la deserialización, que tiene un IdentityHashMap que mantiene la correspondencia entre algunos tipos de objetos de columna y deserializadores. Cuando no haya un deserializador correspondiente, se creará uno nuevo y se colocará en IdentityHashMap. De forma predeterminada, se utiliza la configuración global (com.alibaba.fastjson.parser.ParserConfig#global).

  • JSONLexter: es una clase base que en realidad apunta a un objeto JSONScanner. Es el objeto principal del análisis de cadenas JSON. De acuerdo con el resultado del análisis de la cadena, avance carácter por carácter hasta el final.

DefaultJSONParser determinará si la cadena es un tipo de lista (que comienza con "[") o un tipo de objeto (que comienza con "{")

  • Si es un tipo de lista, analice la cadena y devuelva un JSONArray después de la deserialización.

  • Si es un tipo de objeto, la deserialización devuelve un JSONObject. En el proceso de deserialización, la idea básica es llamar a JSONLexter en un bucle for, primero analizar la clave JSON y luego analizar el valor JSON. Almacene la clave/valor en el mapa interno de JSONObject.

En el proceso de análisis, parse(String) generalmente no involucra la llamada del deserializador porque no involucra la construcción de objetos de clase específicos.

3.2.2 Cómo funciona parseObject(String, Clazz)

¿Cómo se crea JavaBeanDeserializer?

En comparación con la interfaz parse(String) anterior, el principio de funcionamiento de esta interfaz es más complicado, porque implica la creación del JavaBeanDeserializer correspondiente a Clazz, que se completa principalmente en la clase ParseConfig.

De forma similar al proceso de creación de JavaBeanSerializer, Fastjson creará un objeto JavaBeanDeserializer para cada clase personalizada de Java que deba serializarse o generará dinámicamente un objeto de subclase JavaBeanDeserializer a través de la tecnología ASM (también por eficiencia y rendimiento), y completarán el núcleo. La deserialización obras. La convención de nomenclatura de la subclase JavaBeanDeserializer es: FastjsonASMDeserializer_<Número aleatorio>_<Nombre de clase original>,  por ejemplo: FastjsonASMDeserializer_1_ObjectWithCollection.

La creación de un objeto de subclase de JavaBeanDeserializer a través de la tecnología ASM también depende principalmente de la "categoría". El siguiente código muestra claramente el proceso de creación de JavaBeanDeserializer. La variable asmEnable es verdadera por defecto, y Fastjson hará una serie de juicios basados ​​en la "categoría" para decidir si usar la función ASM.

La siguiente figura muestra la relación entre JavaBeanDeserializer y su JavaBeanInfo y FieldDeserializer dependientes.

Los pasos principales para crear un JavaBeanDeserializer se pueden resumir de la siguiente manera:

1) De acuerdo con el tipo de clase Clazz, busque el deserializador correspondiente en IdentityHashMap en ParseConfig y devuélvalo directamente si hay uno.

2) Si no, continúe con los pasos de creación a continuación:

  • Llame a com.alibaba.fastjson.util.JavaBeanInfo#build(...) para crear JavaBeanInfo. Este paso es extremadamente crítico, y es en este paso que se extraerá la "metainformación" de la clase Java:

    1. De acuerdo con la reflexión, busque el constructor predeterminado, la función setter, la función getter y otra información correspondiente al tipo Clazz.

    2. A partir de la función setter/getter, encuentre la variable miembro correspondiente (campo) y juzgue si el setter/getter/field está decorado con la anotación JSONField.

    3. Para cada getter/setter, se creará un FieldInfo (que se deduplicará) y se construirán los campos de matriz de FieldInfo. FieldInfo contiene el nombre (nombre de variable) de cada variable miembro, campo (información del campo de reflexión de Java), método (información de reflexión de la función Java para acceder a las variables), clase de campo (tipo de variable), anotación de campo (si la variable miembro es modificada por JSONField y su información) y muchas otras "metainformación".

  • Si la función ASM está habilitada, cree un JavaBeanDeserializer a través de ASMDeserializerFactory. En el constructor JavaBeanDeserializer, de acuerdo con la lista de campos en JavaBeanInfo, cree una matriz de fieldDeserializers para deserializar las variables miembro.

  • La variable ObjectDeserializer en FieldDeserializer es nula de forma predeterminada y no se inicializa hasta la primera vez que se usa.

3) Coloque el deserializador creado en ParseConfig para su próximo uso.

Ejecución de deserialización Deserializer

Una vez que se encuentra/crea el deserializador correspondiente de acuerdo con la información de Clazz, es muy simple, y se llama directamente al deserialze(...) del deserializador para completar la serialización. Fastjson tiene más de 30 deserializadores incorporados, y los objetos Java de uso común son básicamente compatibles de forma predeterminada.

Ejecución de FastjsonASMDeserializer:

El FastjsonASMDeserializer creado dinámicamente a través de la tecnología de código de bytes ASM está directamente relacionado con la clase personalizada de Java específica (tome la clase ObjectWithCollection anterior como ejemplo), y el proceso de ejecución también es "miles de clases y caras". El resumen es más o menos el siguiente:

  • Directamente nuevo un objeto ObjectWithCollection (asumiendo que es un objeto T).

  • Haga coincidir la cadena JSON una por una de acuerdo con la clave JSON generada (aquí se refiere a "col1:", "col2:"), intente deserializar el objeto Java correspondiente a cada clave JSON y configúrelo en el objeto T. La siguiente figura muestra el código descompilado.

 

 

  • Llame a la interfaz parseRest(...) de JavaBeanDeserializer, continúe deserializando la cadena JSON restante hasta el final de la cadena, configure y devuelva el objeto T.

Ejecución de JavaBeanDeserializer:

La ejecución de JavaBeanDeserializer será relativamente complicada porque implica ejecutar FieldDeserializer para cada variable miembro. El proceso principal se resume de la siguiente manera (en el código com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze):

  • A través de la reflexión de Java, llame al constructor predeterminado del objeto de clase para crear el objeto de clase (como el objeto T). Si no hay un constructor predeterminado, primero cree un objeto de mapa (M) y guarde el objeto de variable miembro creado (valor de campo).

  • En el bucle for, recorra para obtener cada FieldDeserializer (FieldDeserializer está en una matriz, ordenado) y su información de campo asociada. Obtenga el tipo de cada variable miembro (fieldClass) y el nombre de clave JSON correspondiente (como "ABC":) de fieldInfo. Utilice JSONLexer para juzgar si la clave en la cadena JSON actual (por ejemplo, la cadena es "ABC":123) es igual al nombre de la clave JSON. Si son iguales, analice directamente el objeto de la variable miembro (valor de campo) y configúrelo en el objeto T o guárdelo en M.

  • Después de que se hayan recorrido todos los FieldDeserializers, si la cadena JSON no se ha analizado, el JSONLexer será impulsado a analizar la clave JSON ("XYZ":) correspondiente a la cadena actual (como "XYZ":123). Busque fieldDeserializer a través de la clave JSON y continúe analizando la cadena actual a través de fieldDeserializer.

  • Continúe con el bucle for hasta que finalice el análisis de la cadena JSON o se produzca una excepción.

A continuación, se usa la clase FastJsonDemoObject para mostrar los deserializadores que se llaman cuando se deserializan el objeto de esta clase y sus variables miembro.

3.2.3 ¿Cómo personalizar Deserializer?

Del análisis anterior, podemos ver que todos los deserializadores de FastJson implementan la interfaz ObjectDeserializer y se obtienen de PaserConfig. Después de dominar estos principios, definir su propio deserializador se vuelve "práctico". Continuando con el DummyEnum anterior como ejemplo, definimos la deserialización DummyEnumDeserializer de la siguiente manera:

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;
    }
}

Finalmente, creamos un objeto DummyEnumDeserializer y lo insertamos en 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();
    }
}

Después de actualizar el caso de prueba anterior, el resultado de la ejecución es el siguiente. Se puede ver que DummyEnumDeserializer se llamó correctamente y DummyEnum se puede deserializar correctamente.

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

Además de modificar directamente el SerializeConfig/ParserConfig global para registrar Serializer/Deserializer, FastJson también proporciona la manera de especificar Serializer/Deserializer a través de la anotación JSONType, que es más conveniente de usar. Como se muestra abajo.

Si hay una anotación que especifica JSONType en la clase, al crear un deserializador, la clase especificada en la anotación tiene prioridad para crear.

3.2.4 ¿Por qué se llama a la función getter al deserializar?

Mencionamos anteriormente que la deserialización de la interfaz parseObject(String, Clazz) llamará a la función getter del objeto de clase bajo ciertas circunstancias. Al deserializar, debido a que se establecerán las variables miembro del objeto de clase, se llamará a la función de establecimiento, lo cual es comprensible. Pero por qué se llama a la función getter, todavía no quiero entender. Más tarde, encontré una pequeña pista en el código: cada vez que se llama a FieldDeserializer para deserializar una variable miembro, se llama a la función setValue para establecer el valor de la variable miembro en el objeto. Cuando se cumplan las dos condiciones siguientes, se llamará a la función getter correspondiente a la variable miembro:

  • No existe una función setter correspondiente para esta variable miembro (si hay un setter, se llamará directamente al setter y no se tomará la "curva para salvar el país").

  • El tipo de la variable miembro es AtomicInteger/AtomicLong/AtomicBoolean/Map/Collection (entiendo que este tipo de objetos son mutables y tienen una interfaz común para cambiar sus valores).

 

 

3.2.5 ¿Por qué el resultado de la deserialización del tipo Collection depende de la "clase"?

Como se mencionó en la Nota 2 anterior, el comportamiento de deserialización de Fastjson para las variables miembro del tipo Collection no es coherente en diferentes condiciones. Después de deserializar la variable miembro de tipo Colección, si apunta a ArrayList o HashSet depende de la "categoría". Cuando Fastjson se deserializa, juzgará si habilitar la función ASM de acuerdo con la "categoría" de la clase específica. Si se determina que la función ASM se puede habilitar, se generará un objeto Deserializer de ASM específico utilizando la tecnología de manipulación de bytecode de ASM; de lo contrario, se utilizará el objeto JavaBeanDeserializer predeterminado. El uso de diferentes deserializadores da como resultado diferentes resultados de deserialización.

Uso del deserializador ASM

Cuando la función ASM está habilitada, Fastjson creará dinámicamente un deserializador ASM específico. Aquí hay un ejemplo de FastjsonASMDeserializer_1_ObjectWithCollection (también se hereda de JavaBeanDeserializer), el código es el siguiente.

Cuando el código anterior analiza la variable miembro col1 de la clase ObjectWithCollection (tipo Collection<String>), se llamará de forma predeterminada a la función scanFieldStringArray(...) de JSONLexer. La función scanFieldStringArray llama a newCollectionByType(...) para crear un tipo de colección específico.

newCollectionByType tiene como valor predeterminado el primer objeto de tipo HashSet devuelto. Esto también explica el problema del punto 2 anterior.

En ASM Deserializer, otras variables miembro de la colección que no son de tipo cadena se deserializan de forma predeterminada mediante ArrayList, a menos que se especifique explícitamente el tipo de Json Array (por ejemplo, si el tipo especificado es Set, el valor del token de Fastjson es 21).

Utilice el JavaBeanDeserializer predeterminado

Si se usa el JavaBeanDeserializer predeterminado, para Json Array, generalmente se llama a CollectionCodec Deserializer para deserializar, y la función createCollection devuelve el tipo ArrayList de forma predeterminada. Esto está en línea con nuestra cognición y no se repetirá aquí.

Cuarto, escribe al final.

En general, Fastjson sigue siendo muy potente y fácil de usar. También puede deberse a la facilidad de uso de Fastjson que ignoramos la investigación sobre sus principios de funcionamiento internos y no prestamos atención a los "problemas" que pueden causarse en escenarios de uso específicos. La primera parte de este artículo dedica mucho espacio a resumir algunas precauciones para usar Fastjson, combinadas con códigos de verificación específicos y comparaciones con Jackson, con la esperanza de ayudarlo a comprender completamente el uso de Fastjson, ajustar el método de uso en escenarios específicos y usar es flexible. La segunda parte analiza brevemente el principio de funcionamiento subyacente de Fastjson.El punto clave es dominar la creación y ejecución de Serializer/Deserializer. La estructura general del código es relativamente unificada y clara, el diseño es ingenioso y las funciones compatibles son muy ricas. Espero ayudarlo a comprender "profundamente" Fastjson y dominar más métodos de diseño.

El estudio y análisis anteriores solo cubren una pequeña parte de las funciones de Fastjson, y muchas funciones y detalles aún no se han cubierto. Todo lo anterior está escrito en base a mi propia investigación y comprensión. Si hay alguna falsedad en las palabras de la familia, bienvenido a corregir y comunicar, y aprender juntos.

Supongo que te gusta

Origin blog.csdn.net/AlibabaTech1024/article/details/128966767
Recomendado
Clasificación