专栏名称: 阿里开发者
阿里巴巴官方技术号,关于阿里的技术创新均将呈现于此
目录
相关文章推荐
腾讯  ·  AI可以帮你在股市赚钱吗? ·  5 天前  
阿里开发者  ·  多模态大模型微调实践!PAI+LLaMA ... ·  5 天前  
51好读  ›  专栏  ›  阿里开发者

再也不用心惊胆战地使用FastJSON了——序列化篇

阿里开发者  · 公众号  · 科技公司  · 2024-11-04 08:30

正文

阿里妹导读


本篇将主要介绍json序列化的详细流程。本文阅读的FastJSON源码版本为2.0.31。

一、引言

在日常开发中,我们常用FastJSON进行序列化和反序列化。虽然它给我们带来了便捷,但其背后的原理往往被忽视,于是一个不小心就引发了很多血案,例如:

  • FastJSON 序列化踩坑记录 - 类中get方法莫名被执行;
  • 记一次FastJSON使用不当引起的线上Full GC问题排查;
  • FastJSON引发的Full GC问题排查;
  • 急速 24 小时 —— 记一个 FastJSON 引发的小程序 bug 排查;
  • FastJSON序列化机制 -- 排查JSON.toJSONString引发的bug;

在不知其所以然的情况下,我每次使用起来也是胆战心惊的,比如抛出我经常遇到的两个问题:

1.序列化操作:JSON.toJSONString()方法

代码中许多地方都使用了JSON.toJSONString()方法打印日志,可能会遇到转换失败的情况,比如下面一段报错:



注:阿里巴巴开发规约中已经明确禁止在日志打印中直接用JSON工具将对象转换成String,所以大家还是尽量避免使用。



2.反序列化操作:JSON.parseObject()方法

当一个类中嵌套了多层内部类时,JSON.parseObject() 方法是否能够准确转换?

因此,我决定遵循“深入了解才能安心使用”的原则,阅读一下 FastJSON 的源码,以便更好地理解其原理和使用时需要注意的事项。

由于内容较长,本篇将主要介绍json序列化的详细流程。本文阅读的FastJSON源码版本为2.0.31。

二、FastJSON源码阅读


1. FastJSON整体结构

FastJSON的源码结构如下,其中JSON类是源码的入口类,虽然看起来有很多方法,实则概括起来是四类方法:

  • 序列化方法:JSON.toJSONString(),返回字符串;JSON.toJSONBytes(),返回byte数组;
  • 反序列化方法:JSON.parseObject(),返回JsonObject;JSON.parse(),返回Object;JSON.parseArray(), 返回JSONArray;
  • 将JSON对象转换为java对象:JSON.toJavaObject();
  • 将JSON对象写入write流:JSON.writeJSONString();



FastJSON中几个关键的包为:annotation包、asm包、parser包、serializer包。

  • annotation包:该包主要定义了在使用FastJSON中用到的注解,如@JSONField注解定义元素序列化和反序列化的行为;@JSONType注解定义java类序列化和反序列化的行为。

  • asm包:一个字节码的文件,能够动态生成和修改 Java 字节码,通过使用 ASM,FastJSON 可以在运行时生成优化后的序列化和反序列化方法,从而提升性能。

  • parser包:反序列化时用到的类。所有的反序列化器都继承ObjectDeserializer

  • serializer包:序列化时用到的类。所有的序列化器都继承ObjectSerializer


2. 序列化流程

2.1 整体流程

以JSON.toJSONString(),并且序列化一个JavaBean为例,整个方法序列化的时序图如下:



首先,用户调用JSON.toJSONString() 方法并且传入待序列化对象,随后执行以下序列化流程:

1.创建SerializeWriter 对象:SerializeWriter对象类似于StringBuilder ,但性能上做了许多优化,用来存储序列化过程中产生的字符串。

2.创建JSONSerializer对象:JSONSerializer对象提供序列化的一个入口,持有所有具体负责对象序列化工作类的引用。然后调用具体序列化器的write()函数进行序列化,这也是序列化的正式开始:
  • 关键在于SerializeConfigSerializeConfig中维护一个IdentityHashMap存储不同的Java类及其对应的序列化器之间的关系,如果从IdentityHashMap中找到了对应的处理器则直接返回,否则执行第3步。

3.序列化器为 null 的情况:

  • 如果写入器为 null,进入创建 JavaBeanSerializer 的流程。
  • SerializeConfig 调用 TypeUtilsbuildBeanInfo() 方法来构建 Bean 信息。
  • TypeUtils 计算对应类的 getter 方法,并返回 SerializeBeanInfo 对象。
  • SerializeConfig 使用 createASMSerializer 创建 ASM 序列化器。
  • 将新创建的序列化器存入 SerializeConfig

4.写入数据: 获取到序列化写入器后,JSONSerializer 调用该写入器的 write() 方法开始将数据写入 SerializeWriter

5.返回 JSON 字符串: 最后,SerializeWriter 将存储的字符串转换为最终的 JSON 字符串,并返回给用户。

通过这些步骤,用户最终得到了序列化后的 JSON 字符串。

2.2 详细流程

下面详细介绍一下具体的代码实现流程:

1.调用JSON.toJSONString(obj)方法后,最终会走到下面的重载函数,这个函数主要干了三件事:

a.创建SerializeWriter对象 out,用于储存在序列化过程中产生的数据。

b.创建JSONSerializer 对象serializer,该对象持有所有负责具体对象序列化工作类的引用。

c.将对象 object 解析成 string,并将结果写入到out的buffer中。
public static String toJSONString(Object object, // 序列化对象                                  SerializeConfig config, // 全局序列化配置,维护一个IdentityHashMap存储不同的Java类及其对应的Serializer之间的关系                                  SerializeFilter[] filters, // 序列化拦截器,定制序列化需求,如某个属性是否序列化                                  String dateFormat, // 序列化日期格式                                  int defaultFeatures, // 默认序列化特性                                  SerializerFeature... features) //自定义序列化特性{    // out 对象保存解析对象的结果,储存在序列化过程中产生的数据,最终会转换成 string    SerializeWriter out = new SerializeWriter(null, defaultFeatures, features);    try {        // 相当于序列化的组合器,持有所有负责具体对象序列化工作类的引用        JSONSerializer serializer = new JSONSerializer(out, config);        if (dateFormat != null && dateFormat.length() != 0) {            serializer.setDateFormat(dateFormat);            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);        }        if (filters != null) {            for (SerializeFilter filter : filters) {                //添加拦截器                serializer.addFilter(filter);            }        }        // 将对象 object 解析成 string,并将结果写入到out的buffer中        serializer.write(object);        return out.toString();    } finally {        out.close();    }}
这个序列化方法实际并不是真正执行序列化操作,首先做序列化特性配置,然后追加序列化拦截器,开始执行序列化对象操作委托给了config对象查找。

2.其中serializer对象的write方法如下,这个方法主要做了两件事:

a.通过对象的类获得对应的解析类ObjectSerializer。
b.调用解析类的write方法序列化对象。
public final void write(Object object) {    if (object == null) {        out.writeNull();        return;    }    Class> clazz = object.getClass();    /**     * 获取到对应的解析类,所有的类都实现了接口 ObjectSerializer     *      * 对于自定义的JavaBean类,从IdentityHashMap找不到会重新创建 JavaBeanSerializer 类并放入Map中     */    ObjectSerializer writer = getObjectWriter(clazz);    try {        // 使用具体serializer实例处理对象        writer.write(this, object, null, null, 0);    } catch (IOException e) {        throw new JSONException(e.getMessage(), e);    }}
那么接下来就是搞懂怎么获得解析类,解析类又是怎么序列化对象的就可以了。

3.获取对象的解析类

getObjectWriter方法会内部调用重载方法,且create参数为true。

a. 首先,先调用get(clazz)方法,查看IdentityHashMap中有没有已经注册的解析类,有则直接返回。
b. 若没有,则会通过一系列判断以及create为true的条件走到createJavaBeanSerializer(clazz)方法中,创建对应的解析类并放入到IdentityHashMap中。
public ObjectSerializer getObjectWriter(Class> clazz) {   return getObjectWriter(clazz, true);}public ObjectSerializer getObjectWriter(Class> clazz, boolean create) {    //首先从内部已经注册查找特定class的序列化实例    ObjectSerializer writer = get(clazz);
if (writer != null) { return writer; } /*省略*/ if (writer == null) { String className = clazz.getName(); if(){ /*省略*/ } else{ if (create) { writer = createJavaBeanSerializer(clazz); put(clazz, writer); } } }
if (writer == null) { writer = get(clazz); } } return writer;}
4.createJavaBeanSerializer(clazz)方法创建解析类。

这个方法会创建出JavaBeanSerializer对象和SerializeBeanInfo对象,SerializeBeanInfo主要是包含了java对象的字段和方法信息以及注释等,它决定了一个java对象序列化过程的输出。JavaBeanSerializer会根据SerializeBeanInfo对象中的fields字段,创建对应的成员变量的FieldSerializer。

    public final ObjectSerializer createJavaBeanSerializer(Class> clazz) {        String className = clazz.getName();        /*省略*/
SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy, fieldBased); /*省略*/
return createJavaBeanSerializer(beanInfo); }
a.首先,通过TypeUtils获取变量beanInfo
i.最终会走到computeGetters方法通过getter拿到所有对象的属性信息。
  public static SerializeBeanInfo buildBeanInfo(Class> beanType //          , Map<String, String> aliasMap //          , PropertyNamingStrategy propertyNamingStrategy //          , boolean fieldBased //  ) {    /*省略*/    // fieldName,field ,先生成fieldName的快照,减少之后的findField的轮询    Map<String, Field> fieldCacheMap = new HashMap<String, Field>();    ParserConfig.parserAllFieldToCache(beanType, fieldCacheMap);    List fieldInfoList = fieldBased            ? computeGettersWithFieldBase(beanType, aliasMap, false, propertyNamingStrategy) //            : computeGetters(beanType, jsonType, aliasMap, fieldCacheMap, false, propertyNamingStrategy);     /*省略*/      return new SerializeBeanInfo(beanType, jsonType, typeName, typeKey, features, fields, sortedFields);  }

ii.computeGetters方法的具体内容如下,通过判断以get开头和is开头的方法返回元素属性的集合。

    public static List computeGetters(Class> clazz, //                                                 JSONType jsonType, //                                                 Map<String, String> aliasMap, //                                                 Map<String, Field> fieldCacheMap, //                                                 boolean sorted, //                                                 PropertyNamingStrategy propertyNamingStrategy //    ) {      /*省略*/      Method[] methods = clazz.getMethods();      /*省略*/      for (Method method : methods) {        String methodName = method.getName();        /*省略*/        if (methodName.startsWith("get")) {          /*省略*/        };        if (methodName.startsWith("is")) {          /*省略*/        };
} }
b.然后创建java类的解析器,为了提升性能,默认通过asm动态生成类来避免重复执行时的反射开销。
public ObjectSerializer createJavaBeanSerializer(SerializeBeanInfo beanInfo) {  /*省略*/  boolean asm = this.asm && !fieldBased;  /*省略*/  if (asm) {        try {            ObjectSerializer asmSerializer = createASMSerializer(beanInfo);            if (asmSerializer != null) {                return asmSerializer;            }        }    /*省略*/  }}private final JavaBeanSerializer createASMSerializer(SerializeBeanInfo beanInfo) throws Exception {    JavaBeanSerializer serializer = asmFactory.createJavaBeanSerializer(beanInfo);    /*省略*/    return serializer;} public JavaBeanSerializer createJavaBeanSerializer(SerializeBeanInfo beanInfo) throws Exception {    /*省略*/    generateWriteMethod(clazz, mw, getters, context);    /*省略*/}
private void generateWriteMethod(Class> clazz, MethodVisitor mw, FieldInfo[] getters, Context context) throws Exception { /*省略*/ mw.visitMethodInsn(INVOKESPECIAL, JavaBeanSerializer, "write", "(L" + JSONSerializer + ";Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/reflect/Type;I)V"); /*省略*/}
最终调用的还是JavaBeanSerializer的write方法写入。

5.调用解析类的write方法序列化对象

首先会根据IgnoreNonFieldGetter属性选择是否忽略没有对应字段的get方法,然后通过调用每个属性序列化器的getPropertyValueDirect方法获得对应的值。

protected void write(JSONSerializer serializer, //                    Object object, //                    Object fieldName, //                    Type fieldType, //                    int features,                    boolean unwrapped  ) throws IOException {      /*省略*/      final boolean ignoreNonFieldGetter = out.isEnabled(SerializerFeature.IgnoreNonFieldGetter);      /*省略*/      if (notApply) {          propertyValue = null;          } else {              try {                  propertyValue = fieldSerializer.getPropertyValueDirect(object);              }               /*省略*/            }    /*省略*/  }
可以看到,最终还是通过反射获取到属性的值加入到输出流中。
public Object getPropertyValueDirect(Object object) throws InvocationTargetException, IllegalAccessException {    Object fieldValue =  fieldInfo.get(object);    if (persistenceXToMany && !TypeUtils.isHibernateInitialized(fieldValue)) {        return null;    }    return fieldValue;}//fieldInfo.get(object)方法public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {    return method != null            ? method.invoke(javaObject)            : field.get(javaObject);}



三、序列化注意事项


1. getter方法规范化

FastJSON序列化元素时是通过调用对象的所有以“get”或者“is”开头的方法实现的,根据上文源码中实现逻辑,应该注意以下几点:

  • 要序列化的元素必须有对应的getter方法。
  • getter方法中不要有过于复杂的逻辑,最好只读不修改,避免在某次序列化过程中导致数据对象变更。引言中抛出的第一个问题也是在序列化时,由于某个get方法抛出异常导致的。
  • 非对应成员变量的方法尽量不要以get开头,不然很容易造成踩坑或者造成序列化后新增了一个没有的属性的问题,如果实在要用的时候可以使用注解@JSONField(serialize = false)忽视该方法,或者在序列化的时候加上SerializerFeature.IgnoreNonFieldGetter参数,忽略掉所有没有对应成员变量(Field)的getter函数。

举个🌰:类MyTest定义了一个没有对应成员变量的getNoField()方法,并且方法里有错误代码,根据源码逻辑,序列化过程中一定会调用这个方法。

S@Dataclass MyTest {    private String name;    public int value;
public int getNoField() { return 1/0; }}public class FastJsonTest { public static void main(String[] args) { MyTest myTest = new MyTest(); myTest.setName("张三"); System.out.println(JSON.toJSONString(myTest)); }}
测试代码输出如下:序列化失败,抛出ArithmeticException异常。
Exception in thread "main" com.alibaba.fastjson.JSONException: write javaBean error, fastjson version 1.2.84, class MyTest.MyTest, method : getNoField  at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:541)  at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:154)  at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:312)  at com.alibaba.fastjson.JSON.toJSONString(JSON.java:793)  at com.alibaba.fastjson.JSON.toJSONString(JSON.java:731)  at com.alibaba.fastjson.JSON.toJSONString(JSON.java:688)  at MyTest.FastJsonTest.main(FastJsonTest.java:18)Caused by: java.lang.ArithmeticException: / by zero  at MyTest.MyTest.getNoField(FastJsonTest.java:29)  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)  at java.lang.reflect.Method.invoke(Method.java:498)  at com.alibaba.fastjson.util.FieldInfo.get(FieldInfo.java:571)  at com.alibaba.fastjson.serializer.FieldSerializer.getPropertyValueDirect(FieldSerializer.java:143)  at com.alibaba.fastjson.serializer.JavaBeanSerializer.write(JavaBeanSerializer.java:284)  ... 6 more
如果序列化方法加入:

SerializerFeature.IgnoreNonFieldGetter参数,则可以序列化成功。

JSON.toJSONString(myTest,SerializerFeature.IgnoreNonFieldGetter);


2.  布尔类型属性命名不要用is开头

布尔类型的属性不要使用is开头,fastJons会截取is后面的名称当作属性名,造成序列化错误的情况。

举个🌰:自定义类MyTest中包含属性isActive和value,另外还写了一个没有对应成员变量的getNoField方法。

@Dataclass MyTest {    private boolean isActive;    private boolean value;
public MyTest() { } public MyTest(boolean isActive, boolean value) { this.isActive = isActive; this.value = value; } public int getNoField() { return 1; }}
public class FastJsonTest {
public static void main(String[] args) { MyTest myTest = new MyTest(true, false); myTest.setActive(true); myTest.setValue(false); System.out.println(JSON.toJSONString(myTest)); }}
测试代码的输出结果为:属性"isActive"错误的输出了"active",还错误的输出了不存在属性noField。
{"active":true,"noField":1,"value":false}
注:在阿里巴巴开发手册中已经专门强调任何布尔类型的变量都不要加is前缀,容易造成解析异常。(所以说开发手册还是要多看几遍!!)




3. 重复引用问题

序列化过程如果存在重复/循环引用的情况,FastJSON就会用引用标识$ref代替,这样做的好处一是可以节省空间,二是可以避免循环引用的问题。举个🌰:

  public static void main(String[] args) {
User user1 = new User(); user1.setAge(18); user1.setName("张三");
List list = new ArrayList<>(); list.add(user1); list.add(user1); //重复引用 System.out.println(JSON.toJSONString(list)); }
这段代码的输出结果如下:可以看到重复引用的user1变成了{"$ref":"$[0]"}格式。
[{"age":18,"name":"张三"},{"$ref":"$[0]"}]

注意,这种字符串格式只有使用FastJSON才可以正确反序列化,当涉及到是有了不同的序列化框架时往往会导致失败,如果想要正常的序列化方式化,可以通过:

JSON.toJSONString(map,SerializerFeature.DisableCircularReferenceDetect)

禁止使用循环引用。

四、附录

这里给出序列化时常用到注解和SerializerFeature序列化属性。

最后,由于个人水平有限,如文章中有误解或疏漏之处,欢迎各位大佬批评指正~

1.注解

注解

含义

参数

@JSONField

该注解作用于方法上,字段上和参数上,可在进行序列化和反序列化时进行个性功能定制。

  • 注解属性name,指定序列化后的名字。name值应该在此类中唯一。
  • 注解属性ordinal,指定序列化后的字段顺序 属性值越小,顺序越靠前。
  • 注解属性format,指定序列化后的格式。
  • 注解属性serialize,指定是否序列化该字段,默认是true。
  • 注解属性deserialize,指定是否反序列化该字段。
  • 注解属性serialzeFeatures序列化时的特性定义(就是下面的SerializerFeature枚举类)。

@JSonType

该注解作用于类上,对该类的字段进行序列化和反序列化时的特性功能定制。

  • 属性includes,要被序列化的字段。
  • 属性orders,要被序列化的字段的顺序。
  • 属性serialzeFeatures,和前面内容一样,不再赘述。



@JSONCreator

用于标识一个类的构造函数或静态工厂方法,以便在反序列化 (将 JSON 数据转换为 Java 对象) 时使用。

@JSONPOJOBuilder

用于定义POJO的构建器类和构建方法,使得创建复杂对象变得灵活。

  • buildMethod:指定生成对象的方法名,通常这个方法是用于构建最终对象的方法,默认值为 "build"。
  • withPrefix:指定带有前缀的设置方法的名称,默认值是 "with"。例如,如果你的 builder 中有 setName 方法,使用 withPrefix = "with" 时,将会生成 withName 方法。

2.SerializerFeature枚举类

黄色标记的为常用枚举值。

         常量

                           含义

QuoteFieldNames

是否输出key值引号,默认为 true

UseSingleQuotes

输出是否使用单引号,默认为 false

WriteMapNullValue

是否输出值为 null 的字段,默认为 false

WriteEnumUsingToString

是否输出枚举值的 toString() 方法,默认为 false

UseISO8601DateFormat

是否使用 ISO8601 格式输出日期,默认为 false

WriteNullListAsEmpty

是否将 null 值的 List 输出为空数组,默认为 false

WriteNullStringAsEmpty

是否将 null 值的 String 输出为空字符串,默认为 false

WriteNullNumberAsZero

是否将 null 值的 Number 输出为 0,默认为 false

WriteNullBooleanAsFalse

是否将 null 值的 Boolean 输出为 false,默认为 false

SkipTransientField

是否跳过 transient 修饰的字段,默认为 false

SortField

是否按照字段名称排序输出,默认为 false

WriteTabAsSpecial

是否将制表符输出为 ,默认为 false

PrettyFormat

是否格式化输出,默认为 false

WriteClassName

是否输出对象的 class 名称,默认为 false

DisableCircularReferenceDetect

是否禁止循环引用检测,默认为 false

WriteSlashAsSpecial

是否将斜杠输出为 /,默认为 false

BrowserCompatible

是否输出为浏览器兼容的 JSON 格式,默认为 false

WriteDateUseDateFormat

是否按照指定的日期格式输出日期,默认为 false

DisableCheckSpecialChar

是否禁止特殊字符检测,默认为 false

NotWriteDefaultValue

是否不输出默认值字段,默认为 false

BeanToArray

是否将 JavaBean 转换为数组输出,默认为 false

WriteNonStringKeyAsString

是否将非 String 类型的 key 转换为 String 类型输出,默认为 false

NotWriteRootClassName

是否不输出根对象的 class 名称,默认为 false

DisableASM

是否禁用 ASM 库进行序列化,默认为 false