Fastjson踩“坑”记录和“深度”学习

作者:陶征策   阿里国际站商家技术团队

Fastjson是阿里开发的Java语言编写的高性能JSON库,本文总结了Fastjson使用时的一些注意事项,并简单分析了Fastjson的底层工作原理,结合具体的验证代码以及跟Jackson的对比,希望能帮助大家充分理解Fastjson的使用。

一、为什么写这篇?

Fastjson是阿里开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject来分别实现序列化和反序列化操作,使用起来很方便。

最近在升级一个老系统的缓存架构,使用Fastjson将对象序列化后存入缓存,并在client端反序列化后使用,以减少对后端hsf的请求次数。在使用Fastjson的过程中踩了几个“坑”,颇费了一番周折,也趁此机会“深入”学习了下FastJson。这些“坑”本身并不是Fastjson的bug,更多的是一些平时容易被忽略的使用注意事项。因此总结了几个注意事项记录下来,方便后续学习参考。也希望能让更多同学理解如何去正确的使用Fastjson。

Jackson是另一个强大的JSON库,平时工作中使用也较为频繁,因此针对Fastjson的这些使用注意事项,增加了跟Jackson的横向比较。本文使用的Fastjson版本是1.2.68.noneautotype, 不支持AutoType。

二、知其然,怎么正确使用

注意点1:Null属性/值的反序列化

Fastjson在序列化时,默认是不会输出对象中的null属性和Map中value为null的key的, 如果想要输出null属性,可以在调用JSON.toJSONString函数时,加上SerializerFeature.WriteMapNullValue参数(开启Feature)。

一般情况下(比如在日志打印时),null属性/值是否被序列化输出其实影响不大。但是如果序列化的字符串需要再被反序列化成对象,这时候就需要格外注意了,尤其是涉及到Java Map/JSONObject等数据结构的序列化。下面通过代码展示下。

@Test
public void testFastjsonDeserializeNullFields() {
    {
        Map mapWithNullValue = new HashMap<>();
        mapWithNullValue.put("a", null);
        mapWithNullValue.put("b", null);
        String mapSerializeStr = JSON.toJSONString(mapWithNullValue);
​
        Map deserializedMap = JSON.parseObject(mapSerializeStr,
                                                               new TypeReference>() {});
        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() {});
        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)的size是2,反序列化后的对象的size是0。

在一些业务场景中(比如对象序列化后缓存到tair中),需要保证原对象和反序列化的对象严格相同,则就要格外注意这个问题。在序列化时加上SerializerFeature.WriteMapNullValue参数,就能避免这个问题。

与Jackson的对比

相同的对象,如果我们换成使用Jackson来序列化和反序列化,则得到的结果是一致的。Jackson代码如下:

@Test
public void testJacksonDeserializeNullFields() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    {
        Map mapWithNullValue = new HashMap<>();
        mapWithNullValue.put("a", null);
        mapWithNullValue.put("b", null);
        String mapSerializeStr = objectMapper.writeValueAsString(mapWithNullValue);
        System.out.println("Jackson: mapSerializeStr: " + mapSerializeStr);
​
        Map deserializedMap = objectMapper.readValue(
            mapSerializeStr,
            new com.fasterxml.jackson.core.type.TypeReference>() {});
        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() {});
        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类型的成员变量,可能会遇到原对象和反序列化之后的对象不完全一样的问题。继续看下面的验证代码。

Class ObjectWithCollection是一个POJO类,定义了两个Collection类型的属性。

public class ObjectWithCollection {
    private Collection col1;
    private Collection 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 col1 = new ArrayList<>();
    col1.add("str1");
    Set 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 Array,使用ArrayList类型来反序列化其他类型(Long/Integer/Double/自定义Java类型等等)的Json Array。上面反序列化的对象deserializedObj的变量col1的实际类型是HashSet,这就导致了与原对象不相同了。

为什么会出现这个问题?是不是因为序列化时,Collection变量所对应的具体类型没有输出。如果将具体对象类型序列化输出(Fastjson支持此功能,但是默认关闭),Fastjson能否正确的反序列化呢?更新代码,在调用JSON.toJSONString方法时,加上SerializerFeature.WriteClassName参数。

@Test
public void testFastJsonDeserializeCollectionFields() {
    ObjectWithCollection objectWithCollection = new ObjectWithCollection();
    Collection col1 = new ArrayList<>();
    col1.add("str1");
    Collection 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的对比

同样的对象,我们试下用Jackson来序列化/反序列化。具体代码如下:

@Test
public void testJacksonDeserializeCollectionFields() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    ObjectWithCollection objectWithCollection = new ObjectWithCollection();
    Collection col1 = new ArrayList<>();
    col1.add("str1");
    Collection 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等类型代替,这样可以避免很多的“麻烦”。

  • 针对历史老代码/依赖的其他二方库的对象,如果已经使用了Collection类型,则推荐使用Jackson来序列化/反序列化,避免不必要的“坑”。

  • Fastjson针对Collection类型的成员变量的反序列化行为在不同条件下不太一致,这个确实有点难掌握,这个问题会在下面的章节中详细解释。

注意点3:缺少默认构造函数/setter,无法反序列化

在使用Fastjson的过程中,遇到的另一个“坑”是,针对一个对象,序列化是成功的,但是反序列化始终是不成功。在研究了下该对象所对应的代码后,发现了一些和其他对象的不同点:缺少默认的构造函数和变量所对应的setter。缺少这些函数的支撑,Fastjson无法成功反序列化,但是也不会抛出异常(有点奇怪,默默的就是不成功)。看下面的验证代码:

类ObjectWithOutSetter是一个没有默认构造函数(但是有其他带参数的构造函数)和setter的类,具体代码如下:

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类种的默认构造函数和setter代码的注释去掉,再重新执行上面的测试代码,就会发现反序列化的对象是一致的了。具体输出如下:

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

Fastjson的反序列化,需要依赖于类对象的默认构造函数和成员变量的setter,不然会失败。总结起来有如下的几种“可能”会失败的场景:

  • 如果类对象缺少默认构造函数,反序列化肯定失败(不会抛异常,但是成员变量的值是null)。

  • 如果类对象的private成员变量缺少setter,反序列化肯定失败,除非在反序列化调用JSON.parseObject时,加上参数Feature.SupportNonPublicField。特殊情况是,针对AtomicInteger/AtomicLong/AtomicBoolean/Map/Collection类型的成员变量,如果缺少对应的setter,也是能反序列化成功的。

  • 同样的,如果一个类对象没有getter,则序列化也会失败的(不会抛异常,会输出空的“{}”字符串)。

针对类对象的public的成员变量,就算是没有setter,也能反序列化成功。

与Jackson的对比

同样的ObjectWithOutSetter对象(没有setter),换成用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");
    }
}


输出结果如下。可以看到,就算没有setter,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异常。个人觉得这个设计更加合理,能帮助我们尽快发现问题。

  • 如果类对象缺少对应的setter函数,反序列化还是会成功的。Jackson默认会使用Java Reflection来设置成员变量。这一点感觉Jackson还是很强大的。

Fastjson和Jackson,都需要依赖于对象的getter函数来完成序列化。

使用建议

  • 针对需要被Fastjson序列化的对象,一定要定义好成员变量的getter函数。否则无法序列化。

  • 针对需要使用Fastjson反序列化的对象,一定要定义默认构造函数和成员变量的setter函数,否则会失败。定义对象时,最好定义好默认构造函数、setter和getter,这样会避免很多麻烦。

  • 针对历史老代码,如果缺少对应的setter函数,可以考虑是用Jackson来反序列化。

  • 针对历史老代码,如果缺少默认构造函数和getter函数,无论使用Fastjson还是Jackson,反序列化的都会失败,只能改代码了。

注意点4:抽象类/接口的反序列化

这个注意点,跟上面的注意点3,其实是有点关联的。因为Fastjson在反序列化的时候需要依赖于默认构造函数和setter函数,如果无法“构造”出类对象,则反序列化肯定会失败的。比如在对象类型是接口(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,而interface类型变量interfaceObject竟然不为null。判断是Fastjson可以根据interface,自动创建代理类(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)

与Jackson的对比

相同的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的原生类,不再支持自定义Java类根据AutoType来反序列化。

  • 针对历史老代码,已经使用了接口/抽象类/基类类型来定义成员变量,则可以考虑使用Jackson来序列化,但是必须输出对象的具体类型。

注意点5:“虚假”getter/setter的序列化/反序列化

通过上面的一些注意点分析,我们已经知道,Fastjson的序列化/反序列化是依赖对对象的默认构造函数、getter和setter函数的。但是在一些类对象中,经常能看到setXXX()/getXXX()等样式的函数,这些函数本身并不直接返回/设置一个对象,而是有一些内部处理逻辑(逻辑可复杂、可简单),只是刚好以set/get开头命名。这些函数往往会导致序列化/反序列化的失败,更严重的情况下甚至会造成系统安全漏洞(Fastjson的一些安全漏洞就是因为AutoType功能,同时配合特定的setter函数造成的,比如很常见的com.sun.rowset.JdbcRowSetImpl)。因为这些函数不直接对应到一个成员变量,我姑且称之为“虚假”getter/setter。

一个很典型的例子是阿里MetaQ的消息对象com.alibaba.rocketmq.common.message.MessageExt(这是个基类,实际会指向com.alibaba.rocketmq.common.message.MessageExtBatch)是无法被序列化的,会抛出BufferUnderflowException异常。应该很多人踩过这个坑,为此阿里的开发规约中,已经明确规定:日志打印时禁止直接用JSON工具将对象转换成String。

下面通过代码来验证下这个问题。下面的类中,定义了一个“虚假”的getter: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);
}

上面的验证代码执行后,Fastjson在执行getWired()的逻辑时,直接抛出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()函数只是一个简单的demo。再延伸一下,针对有“复杂”处理逻辑的getter函数(比如在getter中调用hsf,写数据库等操作),Fastjson在序列化的时候,往往会有“意想不到”的结果发生,这通常不是我们所期望的。

怎么解决这个问题?在调用JSON.toJSONString的时候,加上SerializerFeature.IgnoreNonFieldGetter参数,忽略掉所有没有对应成员变量(Field)的getter函数,就能正常序列化。通过在getWired函数上增加@JSONField(serialize = false)注解,也能达到同样的效果。

@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的对比

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默认也不能很好的处理这种“复杂”的getter函数的序列化。

[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也能通过配置化的方式,很方便的解决这个问题。直接在getWired函数上加上@JsonIgnore注解,就能成功序列化了。

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

使用建议

  • Fastjson和Jackson,默认都不能很好的处理“虚假”getter函数的序列化(虚假setter函数所对应的反序列化也是一样的)。需要被序列化/反序列化的对象,尽量不要定义有“虚假”的getter/setter,也不要让getter/setter包含“复杂”的处理逻辑。纯POJO会更好, Make it simple。

  • 针对“虚假”getter/setter(没有与成员变量与之对应的setter/getter函数),一般不需要参与序列化/反序列化。可以通过SerializerFeature.IgnoreNonFieldGetter参数在序列化阶段将它们过滤掉。绝大多数场景下,加上SerializerFeature.IgnoreNonFieldGetter参数会更安全,能够避免一些“奇怪”的问题。也可以通过@JSONField注解的形式,来忽略掉它们。

注意点6:同引用对象的序列化

Fastjson有一个很强大的功能:循环引用检测(默认是开启的)。比如说有两个对象A和B (如下代码所示)。A包含了指向B对象的引用,B包含了指向A对象的引用,A/B两个对象就变成了循环引用了。这时候如果序列化,一般会遇到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种对象引用,分别如下。

Fastjson踩“坑”记录和“深度”学习_第1张图片

循环引用检测的能力确实很强大,但有时候“能力”过于强大,就会“殃及无辜”,造成一些误杀。比如对象明明没有“循环引用”,却还是按照引用模式进行序列化了。看如下的代码。

public class RefObject {
    private String var1;
    private Long data1;
    ...setter/getter....
}
​
public class SameRefObjectDemo {
    private List refObjectList;
    private Map refObjectMap;
    ...setter/getter....
}
​
@Test
public void testFastJsonSerializeSameReferenceObject() {
    RefObject refObject = new RefObject();
    refObject.setVar1("Value1");
    refObject.setData1(9875L);
    
    SameRefObjectDemo sameRefObjectDemo = new SameRefObjectDemo();
    List refObjects = new ArrayList<>();
    refObjects.add(refObject);
    refObjects.add(refObject);
    sameRefObjectDemo.setRefObjectList(refObjects);
    
    Map 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框架,会造成彼此之间的通信失败。

为什么同引用对象也要按照“循环引用”的模式来序列化,我能想到的就是为了减少序列化的结果输出长度,降低网络传输开销,有利有弊。

如果在调用JSON.toJSONString函数是加上SerializerFeature.DisableCircularReferenceDetect参数,就能禁用“循环引用”检测功能,得到正常的输出如下:

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

与Jackson的对比

Jackson默认是不支持“循环引用”的对象序列化的,会抛出StackOverflowError错误(具体如下):

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

但是它提供了很多的注解,可以解决这个问题。这篇文章《Jackson – Bidirectional Relationships》详细解释了多种方案。这里我们验证一种方法: 使用@JsonManagedReference和@JsonBackReference注解。具体代码如下:

《Jackson – Bidirectional Relationships》参考链接:

https://www.baeldung.com/jackson-bidirectional-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)"));
    }
}

输出结果如下。可以看到demoA对象能被正确的序列化,且反序列化的对象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使用起来会更方便。

三、知其所以然,打破砂锅问到底

上面的很大篇幅中,列举了使用Fastjson的一些注意事项,介绍了怎么去更合理的使用它,但是我们还处在“知其然”的阶段。上面的注意事项中也遗留了一些问题,还没有解答。为了解答这些问题,同时也为了更加深刻的理解Fastjson的底层工作原理,我们还需要做到“知其所以然”,这样使用起来才能得心应手,灵活应变。

3.1 对象是怎么被序列化的?

Fastjson踩“坑”记录和“深度”学习_第2张图片

以我们常用的JSON.toJSONString来分析Java对象的序列化,这里涉及到Fastjson的几个关键对象, 他们之间的关系如下所示:

Fastjson踩“坑”记录和“深度”学习_第3张图片

上面的类图中,最核心的类是SerializeConfig和JavaBeanSerializer。SerializeConfig的主要职责有两点:

  • 维护了一个IdentityHashMap,里面存储了不同的Java类及其对应的Serializer之间的关系。每次调用JSON.toJSONString序列化时,都会从中查找对应的Serializer。

  • 如果找不到一个类对象的Serializer(一般是自定义Java对象),则会重新创建一个JavaBeanSerializer,并放入IdentityHashMap中,以供下次使用。

JavaBeanSerializer主要是用来序列化自定义Java对象的。Fastjson在从SerializeConfig找到对应类的Serializer后,直接调用Serializer的write接口,即可完成序列化。一个自定义类对象,它的每一个成员变量(field)对象都会有它对应的FieldSerializer。在类对象的序列化过程中会依次调用FieldSerializer,下图展示了一个简单的Java POJO对象在序列化时会需要用到的Serializer。

Fastjson踩“坑”记录和“深度”学习_第4张图片

3.1.1 有多少种Serializer?

一个Java自定义对象的序列化还是很复杂的,因为涉及到很多的其他Java自定义对象和Java的primitive类型。针对Java的primitive类型,Fastjson基本都定义了对应的Serializer,可以很好的支持它们的序列化工作。所有实现了com.alibaba.fastjson.serializer.ObjectSerializer接口的类,都是Fastjson默认已经定义好的Serializer,粗略看了下,大概有44个。

Fastjson踩“坑”记录和“深度”学习_第5张图片

3.1.2 JavaBeanSerializer是怎么创建的?

JavaBeanSerializer是Fastjson序列化的核心类。在默认情况下,Fastjson会给每一个需要被序列化的自定义Java类创建一个JavaBeanSerializer对象或者通过ASM(字节码修改)技术动态生成一个JavaBeanSerializer的子类对象,由他们来完成核心的序列化工作。JavaBeanSerializer子类的命名规范是:ASMSerializer__, 例如:ASMSerializer_4_ObjectWithWiredGetter。

为什么需要通过ASM技术创建JavaBeanSerializer的子类对象?主要还是为了效率和性能,这也是Fastjson为什么快的原因之一。通过ASM技术创建的JavaBeanSerializer的子类对象,是高度定制化的,是跟需要被序列化的自定义Java类紧密绑定的,所以可以取得最佳的性能。

是否通过ASM技术创建JavaBeanSerializer的子类对象,主要取决于“类品”。人有人品,类当然也有“类品”。人品决定了我们做事结果的好坏,“类品”决定了在序列化时是否能使用ASM功能。“类品”简单理解就是类的一个综合属性,会根据类的基类特点、类的名称,成员变量个数、getter/setter、是否是interface、是否有使用JSONField注解等等信息来综合决定。下面的代码片段展示了JavaBeanSerializer的创建过程,asm变量默认是true的。Fastjson会做一系列的判断,来决定asm变量的值,进而决定是否创建ASMSerializer。

Fastjson踩“坑”记录和“深度”学习_第6张图片

Fastjson踩“坑”记录和“深度”学习_第7张图片

下图展示了JavaBeanSerializer及其依赖的SerializeBeanInfo、FieldSerializer之间的关系。

Fastjson踩“坑”记录和“深度”学习_第8张图片

JavaBeanSerializer的创建,依赖于SerializeBeanInfo。SerializeBeanInfo针对一个Java类,只会被创建一次(调用com.alibaba.fastjson.util.TypeUtils#buildBeanInfo方法来创建,跟随JavaBeanSerializer一起创建),主要包含了以下的信息:

  • beanType:也就是Java类对象的真实类型。

  • jsonType:如果一个Java类对象被@JSONType注解修饰,则有值,否则为null。

  • typeName:依赖于上面的jsonType。如果jsonType中有指定序列化输出的类型,则有值。

  • features:依赖于上面的jsonType。如果jsonType中有指定序列化输出的SerializerFeature,则有值。

  • fields:Java类对象中需要被序列化的get方法/field信息等等,都包含在其中,这个字段是比较关键的。

SerializeBeanInfo对象决定了一个Java对象序列化的输出。JavaBeanSerializer会根据SerializeBeanInfo对象中的fields字段,创建对应的成员变量的FieldSerializer。

FieldSerializer中的RuntimeSerializerInfo在创建的时候为null,在真正使用的时候才被初始化,指向具体的Serializer(Lazy initialization模式)。

3.1.3 怎么自定义Serializer?

在搞清楚了Serializer的查找方式和工作原来之后,我们就能“照葫芦画瓢”,写自己的Serializer了。下面通过DummyEnum类,来展示下怎么写一个Serializer。

先创建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,表示新的Serializer成功执行了。

FastJson: dummyEnumStr: 2

3.2 字符串是怎么被反序列化的?

Fastjson支持反序列化的核心函数有三个,他们的主要功能和区别如下:

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: can not cast to JSONObject.”。

3)JSON.parseObject(String,Clazz): 根据Java Clazz类型,将字符串反序列化成一个自定义的Java对象。也可以支持List/Set/Map等Java原生对象的生成。

因为Fastjson默认已经禁用了AutoType(在1.2.68版本中),在不考虑AutoType功能影响的情况下,这三个接口之间的对比如下:

Fastjson踩“坑”记录和“深度”学习_第9张图片

 

3.2.1 parseObject(String)的工作原理

这三个接口中,parse(String)和parseObject(String)接口的功能实现非常的类似。后者会调用前者,并将后者的结果转成对应的JSONObject。这里重点分析下parseObject(String)接口的工作原理。

Fastjson踩“坑”记录和“深度”学习_第10张图片

上图展示了parseObject(String)接口主要的代码,其主要分成两步:

  • 调用parse(String)接口,将字符串反序列化成对象

  • 调用JSON.toJSON接口,将步骤1中的对象转成JSONObject

parse(String)接口的代码很少,如以下截图所示,它的核心是DefaultJSONParser。

Fastjson踩“坑”记录和“深度”学习_第11张图片

Fastjson踩“坑”记录和“深度”学习_第12张图片

DefaultJSONParser是反序列化的主要驱动类,它包含两个核心的对象:

  • ParseConfig:反序列化的核心配置类,其中有一个IdentityHashMap,维护了一些列的对象类型和deserializers的对应关系。当没有对应的deserializer时,会创建新的,并放入IdentityHashMap中。默认情况下,使用的是全局的配置(com.alibaba.fastjson.parser.ParserConfig#global)。

  • JSONLexter:是一个基类,真实指向一个JSONScanner对象。是JSON字符串解析的核心对象。根据字符串的解析结果,逐个字符往前移动,直到结尾。

DefaultJSONParser会判断字符串是列表型(以"["开头的)还是对象型(以"{"开头的)

  • 如果是列表型,则解析字符串,反序列化返回一个JSONArray。

  • 如果是对象型,则反序列化返回一个JSONObject。在反序列化的过程中,基本思路是在一个for循环中,调用JSONLexter,先解析出JSON key,接着再解析出JSON value。把key/value存入JSONObject的内部map中。

parse(String)在解析的过程中,因为不涉及到具体的类对象的构建,所以一般不涉及到deserializer的调用。

3.2.2 parseObject(String, Clazz)的工作原理

JavaBeanDeserializer是怎么创建的?

相比于上面的parse(String)接口,这个接口的工作原理是要更复杂些,因为涉及到Clazz所对应的JavaBeanDeserializer的创建,这个主要是在ParseConfig类中完成的。

与JavaBeanSerializer的创建过程类似,Fastjson会给每一个需要被序列化的自定义Java类创建一个JavaBeanDeserializer对象或者通过ASM技术动态生成一个JavaBeanDeserializer的子类对象(也是为了效率和性能),由他们来完成核心的反序列化工作。JavaBeanDeserializer子类的命名规范是:FastjsonASMDeserializer__, 例如:FastjsonASMDeserializer_1_ObjectWithCollection。

是否通过ASM技术创建JavaBeanDeserializer的子类对象,同样主要取决于“类品”。下面的代码清晰展示了JavaBeanDeserializer的创建过程。asmEnable变量默认是true,Fastjson会根据“类品”做一系列判断,决定是否使用ASM功能。

Fastjson踩“坑”记录和“深度”学习_第13张图片

Fastjson踩“坑”记录和“深度”学习_第14张图片

Fastjson踩“坑”记录和“深度”学习_第15张图片

下图展示了JavaBeanDeserializer及其依赖的JavaBeanInfo、FieldDeserializer之间的关系。

Fastjson踩“坑”记录和“深度”学习_第16张图片

创建JavaBeanDeserializer的主要步骤可以概括为如下:

1)根据Clazz类类型,在ParseConfig中的IdentityHashMap查找对应的Deserializer,如果有直接返回。

2)如果没有,则继续下面的创建步骤:

  • 调用com.alibaba.fastjson.util.JavaBeanInfo#build(...) 创建JavaBeanInfo。这一步极为关键,正是在这一步中会提取Java类的“元信息”:

    1. 根据反射,找到Clazz类型所对应的默认构造函数,setter函数,getter函数等信息。

    2. 从setter/getter函数出发,找到对应成员变量(field),以及判断setter/getter/field上是否有被JSONField注解修饰。

    3. 针对每一个getter/setter,会创建一个FieldInfo(会去重的),并构建FieldInfo的数组fields。FieldInfo包含了每个成员变量的name(变量名称),field(Java反射字段信息),method(访问变量的Java函数反射信息),fieldClass(变量的类型),fieldAnnotation(成员变量是否被JSONField修饰,及其信息)等诸多“元信息”。

  • 如果ASM功能开启,则通过ASMDeserializerFactory创建JavaBeanDeserializer。在JavaBeanDeserializer构造函数中,根据JavaBeanInfo中的fields列表,创建fieldDeserializers数组,用于反序列化成员变量。

  • FieldDeserializer中的ObjectDeserializer变量,默认是null,直到第一次使用的时候才被初始化。

3)将创建好的Deserializer放入ParseConfig中,以供下次使用。

反序列化Deserializer的执行

一旦根据Clazz信息,找到/创建了对应的Deserializer,那就很简单了,直接调用Deserializer的deserialze(...)完成序列化。Fastjson已经内置了30多种的Deserializer,常用的Java对象,基本都已经默认支持了。

Fastjson踩“坑”记录和“深度”学习_第17张图片

FastjsonASMDeserializer的执行:

通过ASM字节码技术动态创建的FastjsonASMDeserializer,因为和具体的Java自定义类(以上面的ObjectWithCollection类举例)直接相关,执行流程也是“千类千面”,总结起来大致如下:

  • 直接new一个ObjectWithCollection对象(假设是对象T)。

  • 逐个根据已经生成好的JSON key(这里是指"col1:", "col2:"),匹配JSON字符串,尽力去反序列化每个JSON key所对应的Java对象,并设置到对象T上。下图展示的是反编译的代码。

 

Fastjson踩“坑”记录和“深度”学习_第18张图片

 

  • 调用JavaBeanDeserializer的parseRest(...)接口,继续反序列化剩余的JSON字符串直到字符串结束,设置并返回对象T。

JavaBeanDeserializer的执行:

JavaBeanDeserializer的执行,因为涉及到要对每一个成员变量执行FieldDeserializer,会相对复杂一些。主要流程概括起来如下(在代码com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze中):

  • 通过Java反射,调用类对象的默认构造函数创建类对象(比如对象T)。如果没有默认构造函数,则先创建一个Map对象(M),保存创建的成员变量对象(field value)。

  • 在for循环中,遍历获取每一个FieldDeserializer(FieldDeserializer是在一个数组中,经过排序的),及其所关联的fieldInfo。从fieldInfo获取每一个成员变量的类型(fieldClass)和对应的JSON key name(比如是"ABC":)。通过JSONLexer判断当前的JSON字符串(比如字符串是"ABC":123)中的key是否等于JSON key name。如果相等,则直接解析出成员变量对象(field value),并设置到对象T上或者存储到M中。

  • 当所有的FieldDeserializer都遍历完之后,如果JSON字符串还没有解析完,则驱动JSONLexer解析出当前字符串(比如"XYZ":123)对应的JSON key("XYZ":)。通过JSON key反查到fieldDeserializer,通过fieldDeserializer继续解析当前的字符串。

  • 继续for循环,直到JSON字符串解析结束或者抛出异常。

下面通过FastJsonDemoObject类,展示了该类对象及其成员变量反序列化时所分别调用的Deserializers。

Fastjson踩“坑”记录和“深度”学习_第19张图片

3.2.3 怎么自定义Deserializer?

从上面的分析可知,FastJson的反序列化deserializer,都是实现了ObjectDeserializer接口的,并且是从PaserConfig中获取的。在掌握这些原理后,定义自己的deserializer就变的“信手拈来”了。继续以上文的DummyEnum为例子,我们定义的反序列化DummyEnumDeserializer如下:

public class DummyEnumDeserializer implements ObjectDeserializer {
    /**
     * 从int值反序列化Dummy Enum
     *
     * @param parser
     * @param type
     * @param fieldName
     * @param 
     * @return
     */
    @Override
    public  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,使用起来更加方便。如下图所示。

Fastjson踩“坑”记录和“深度”学习_第20张图片

如果类上有指定JSONType的注解,则在创建Deserializer的时候,优先通过注解上指定的类来创建。

Fastjson踩“坑”记录和“深度”学习_第21张图片

3.2.4 为什么反序列化的时候会调用getter函数?

前面我们提到,parseObject(String, Clazz)接口的反序列化,在特定情况下会调用类对象的getter函数。反序列化的时候,因为要设置类对象的成员变量,所以会调用setter函数,这是可以理解的。但是为什么也会调用getter函数,始终没想明白。后来在代码中找到了一点线索,在每次调用FieldDeserializer反序列化完一个成员变量时,会调用setValue函数将成员变量的value设置到对象上。在满足以下两个条件时,会调用该成员变量所对应的getter函数:

  • 该成员变量没有对应的setter函数(如果有setter就会直接调用setter了,不会走这招“曲线救国”)。

  • 该成员变量的类型是AtomicInteger/AtomicLong/AtomicBoolean/Map/Collection(我理解这些类型的对象都是mutable的,有通用的接口来改变它们的值)。

 

Fastjson踩“坑”记录和“深度”学习_第22张图片

 

3.2.5 为什么Collection类型的反序列化结果取决于“类品”?

在上面的注意点2中提到,Fastjson针对Collection类型的成员变量的反序列化行为在不同条件下不太一致。Collection类型的成员变量在反序列化后,具体是指向ArrayList还是HashSet,这个要看“类品”了。Fastjson在反序列化时,会根据具体类的“类品”来判断是否开启ASM功能。如果判断能开启ASM功能,则会使用ASM字节码操作技术生成一个特定的ASM Deserializer对象,否则使用默认的JavaBeanDeserializer对象。不同Deserializer的使用,造成了反序列化结果的不同。

使用ASM Deserializer

在ASM功能开启的情况下,Fastjson会动态创建特定的ASM Deserializer。此处以FastjsonASMDeserializer_1_ObjectWithCollection举例(它也是继承于JavaBeanDeserializer的),代码如下。

Fastjson踩“坑”记录和“深度”学习_第23张图片

上述代码在解析ObjectWithCollection类的col1成员变量时(Collection类型),默认会调用JSONLexer的scanFieldStringArray(...)函数。scanFieldStringArray函数会调用newCollectionByType(...)来创建具体的Collection类型。

newCollectionByType默认第一个返回的HashSet类型的对象。这也就解释了上面的注意点2中的问题。

Fastjson踩“坑”记录和“深度”学习_第24张图片

Fastjson踩“坑”记录和“深度”学习_第25张图片

在ASM Deserializer中,其他非字符串类型的Collection成员变量,默认是使用ArrayList来反序列化的,除非明确指定了Json Array的类型(比如指定了类型是Set,则Fastjson的token值是21)。

Fastjson踩“坑”记录和“深度”学习_第26张图片

使用默认JavaBeanDeserializer

如果是使用默认的JavaBeanDeserializer,针对Json Array,一般是会调用CollectionCodec Deserializer来反序列化,createCollection函数默认返回ArrayList类型。这是符合我们认知的,这里不再赘述。

Fastjson踩“坑”记录和“深度”学习_第27张图片

Fastjson踩“坑”记录和“深度”学习_第28张图片

四、写在最后

整体来说,Fastjson的功能还是很强大的,使用也比较简单。可能也正是因为Fastjson的容易使用,让我们忽略了对其内部工作原理的研究,未能关注到在特定使用场景下可能造成的“问题”。本文第一部分花了很大的篇幅去总结Fastjson的一些使用注意事项,并结合具体的验证代码以及跟Jackson的对比,希望能帮助大家充分理解Fastjson的使用,在具体场景下调整使用方法,灵活运用。第二部分简单分析了一下Fastjson的底层工作原理,关键点在于掌握Serializer/Deserializer的创建和执行。整体的代码结构还是比较统一和清晰的,设计巧妙,支持的功能很丰富。希望能帮助大家“深入”了解Fastjson,掌握更多的设计方法。

上面的学习和分析,也只是涵盖了Fastjson的少部分功能,很多的功能和细节还没有涉及到。以上这些都是根据自己的研究和理解所写,一家之言,如有不实之处,欢迎指正和交流,一起学习。

Fastjson踩“坑”记录和“深度”学习_第29张图片

你可能感兴趣的:(java,json,前端)