关于fastjson序列化部分源码解析

关于fastjson 的使用:

转载地址:http://www.iteye.com/topic/1098058

首先可以了解下fastjson序列化的实现过程:

从javaeye上看到了阿里一位人士写的fastjson,特别是其中如何将java对象序列化成json字符串这段。笔者比较关注,因为在笔者的项目中就用了一个json序列化器(造的轮子)。就下载下来看了一看,先不说和笔者所用的轮子有何区别,单就用了一个简单的测试器,来测试一下两者的处理速度。测试代码就不贴了,简单地说下测试结果。在jvm充分优化的情况下(for循环执行了很多次之后),笔者所使用的java序列化器处理速度不是很均匀,在结尾有短暂的变化(可能与虚拟机回收有关系);而fastjson在后面的处理过程当中,一般很均匀(后来发现与使用的buf分配方式有关)。最主要的区别莫在于,fastjson的速度那是不能对比了。
经过分析源码之后,发现fastjson在处理json优化上面还是下了很大的工夫的。笔者准备从以下几个方面对fastjson作一个简单的解析,也让使用fastjson的同学对fastjson有一个简单的认识。
1 总体分析 分析json序列化的总体思路和解析过程
2 性能分析A 针对字符生产部分(即outWriter)对不同类型数据的处理和与性能相关处理部分
3 性别分析B 针对序列化过程部分(即objectSerializer)对不同类型的序列化过程处理和与性能相关处理部分
4 对象解析分析 对javaBean解析部分和针对字段输出部分的处理和解析
源码分析基于1.0.5版本。

总体分析,首先上图,即fastjson的总体处理思想,其实也是所有json序列化器需要考虑的问题。

在这里,需要考虑的主要有两个部分,一是临时保存在序列化过程中用于储存数据的容器,二是处理对象序列化的序列化器。
在fastjson中,保存数据的容器使用了wirter,字符输出流,而且是自实现的一个字符输出流。相对原来的writer,追加了很多需要输出的信息的实现,比如输出一个字符串,输出一个字符,输出一个long类型数据等。而处理对象序列化的序列化器,而使用了责任链模式和工厂模式,将不同类型的java对象分散到不同的序列化器当中。而每个序列化器只处理与自身类型相对应的数据信息,这样就避免了在处理时,各种情况交织在一块,逻辑混乱的问题。
下面就源码本身作一个分析,其中结合两个部分进行分析。

代码分析部分

首先,将一个对象序列化json字符串调用的是JSON对象的toJSONString方法,这里调用的是无参数方法。(注:本文不分析采用vistor实现json序列化代码的部分)。具体代码如下所示:

第一行,新产生的一个数据保存器,储存在序列化过程中产生的数据;第二行,产生统一的json序列化器,其中使用了outWriter,此类即json序列化的统一处理器;第三行,调用序列化方法开始序列化对象,以产生json字符串信息;第四行,返回已经储存的json信息。

Java代码
  1. SerializeWriterout=newSerializeWriter();
  2. JSONSerializerserializer=newJSONSerializer(out);
  3. serializer.write(object);
  4. returnout.toString();

数据保存器(序列化输出容器)

SerializeWriter是一个用于储存在序列化过程中产生的数据信息,它与jdk中的StringBuiler有着类似的功能,即将不同的数据填充到此容器中。之所以不使用StringBuilder的原因之一在于StringBuilder没有提供一些特别为性能优化的方法,并且StringBuilder在处理过程中增加了多余的操作(如新分配对象)。该容器的主要功能就是接收不同的数据,并将这些数据存储到该内部的一个字符数组当中,同时记录字符总数。
既然充当了一个数据输出的角色,那么就可以往其中输入任何的数据,包括int,byte,short等基本类型和对应的包装类型,也包括日期数据,以及经常使用的字符串数据等。对于在这些数据类型之外的其它类型,由于json的特殊结构,所有的高级类型均可以转化于这些基础类型的组织体,所以不需要再针对高级类型作处理了(这些是序列化器应该考虑的问题)。

首先对SerializeWriter的方法(输出和追加)作一个预览:

方法总共可以分五个部分,第一个部分是针对writer基本功能一个扩展,即支持输出int,字符,以及字符数组,追加字符数组(包括字符串)等;第二个部分提供了写整形和长整形的基本方法;第三个部分是提供写基本数据类型数组的支持;第四个部分是提供写一个数字+一个字符的形式,比如数据+[,\],}]这种格式;第五个部分是提供写数据串,主是是针对字符串追加双引号或单引号(以支持标准json)。publicvoidwrite(intc)

  • charc)
  • charc[],intoff,85); font-weight:bold">intlen)
  • voidwrite(Stringstr,85); font-weight:bold">publicSerializeWriterappend(CharSequencecsq)
  • publicSerializeWriterappend(CharSequencecsq,85); font-weight:bold">intstart,85); font-weight:bold">intend)
  • publicSerializeWriterappend(
  • voidwriteInt(inti)
  • voidwriteLong(longi)
  • voidwriteBooleanArray(boolean[]array)
  • voidwriteShortArray(short[]array)
  • voidwriteByteArray(byte[]array)
  • voidwriteIntArray(int[]array)
  • voidwriteIntArray(Integer[]array)
  • voidwriteLongArray(long[]array)
  • voidwriteIntAndChar(inti,85); font-weight:bold">voidwriteLongAndChar(longi,85); font-weight:bold">voidwriteStringWithDoubleQuote(Stringtext)
  • voidwriteKeyWithDoubleQuote(Stringtext)
  • voidwriteStringWithSingleQuote(Stringtext)
  • voidwriteStringArray(String[]array)
  • voidwriteKeyWithSingleQuote(Stringtext)
  • voidwriteKeyWithDoubleQuoteIfHashSpecial(Stringtext)
  • voidwriteKeyWithSingleQuoteIfHashSpecial(Stringtext)
  • 五个部分的方法,每个部分都有其特殊的作用和意义,针对最常用的数字和字符串作了特别的对待。

    在实现上面,SerializeWriter使用了一个内部的字符数组作为数据的储存器,同时使用了一个计数器计算当前存储的字符量。既然使用了字符数组,那么肯定有相关的操作,如字符扩容等。整个写数据的过程,其实就是往这个字符数组追加数据的过程,需要考虑只是如何追加数据的问题,即上面所列出的这么多些方法。在最终写完数据之后,即可将这个字符数组转为我们所需要的字符串了。

    对象序列化入口

    JsonSerializer,准备地讲,这个类不应该叫这个名字,因为它与其它的对象序列化器相混淆了。这只是一个提供对象序列化的一个入口;同时,它持有所有具体负责对象序列化工作类的引用。将这些序列化器集中起来,需要用到哪个对象序列化器时,就取出这个序列化器,并调用相应的序列化方法。
    既然是对象序列化入口,它就需要关注两个事情。一是我们究竟有哪些序列化器可以使用,二是对于一个对象,应该使用哪一个序列化器来进行工作。对于这两个问题,JsonSerializer内部持有一个JSONSerializerMap的属性,即表示应该序列化的对象类型和对应的序列化器的一个映射。我们来看默认的构造方法,它使用了默认的全局对象类型和对象序列化器映射:

    首先获得当前序列化对象所在的类型,再根据类型取得相对应的序列化器,最后使用序列化器进行正式的序列化工作。

    序列化过程

    正如上面所说,进入序列化工作之后,即是针对每一种类型进行序列化处理了。该序列化工作使用了统一的方法,即实现了统一的序列化方法:

    voidwrite(JSONSerializerserializer,Objectobject)throwsIOException

    该方法在抽象类(可以说是接口)ObjectSerializer中定义,即所有的序列化器都继承了此类,并实现了此方法用于处理不同的情形。对于上层调用(如JsonSerializer),不需要考虑每一个类型的序列化工作是如何实现的,只需要针对不同的类型找到正确的序列化器,进行序列化工作即可。

    对于一个序列化器,通常的工作,是首先取得当前的数据储存容器,然后根据不同的对象类型,将对象输出到outWriter中即可。比如一个序列化实现IntergerSerializer,它的实现如下:


    SerializeWriterout=serializer.getWrier();

  • Integervalue=(Integer)object;
  • out.writeInt(value.intValue());
  • 这 样即完成了一个完整的序列化工作。当然,对于复杂的数据类型,在实现过程中,可能需要递归地调用JsonSerializer的序列化工作,这得归结于如何处理不同的对象类型了。比如处理一个对象集合时,除需要处理集合本身之外,还需要处理集合中的每一个对象,这时又是一个解析过程。由于使用了同一个jsonSerializer,所以在进行数据处理时,输出的数据会按照在解析过程中的顺序,顺序地写入到outWriter中,这样即保证了数据的正确性。

    总结

    整个解析过程,相对来说,比较地简单。因为,这个解析工作从原理上来讲,也并不复杂。困难地在于,如何处理不同的数据类型,以及在处理过程中如何保证处理的效率。这即是fastjson之所以产生的原因。
    本篇从整个结构出发,对fastjson中的json序列化过程有了一个初步的理解,让大家都能够很好地正解fastjson,包括fastjson本身在实现上可能存在的不合理情况。在下一篇中,就效率实现上的两个重要方面(输出效率和解析过程)分别进行解析。


    接上篇,在论述完基本概念和总体思路之后,我们来到整个程序最重要的部分-性能优化。之所以会有fastjson这个项目,主要问题是为了解决性能这一块的问题,将序列化工作提高到一个新的高度。我们提到,性能优化主要有两个方面,一个如何将处理后的数据追加到数据储存器,即outWriter中;二是如何保证处理过程中的速度。
    本篇从第一个性能优化方面来进行解析,主要的工作集中在类SerializeWriter上。

    首先,类的声明,继承了Writer类,实现了输出字符的基本功能,并且提供了拼接数据的基本功能。内部使用了一个buf数组和count来进行计数。这个类的实现结果和StringBuilder的工作模式差不多。但我们说为什么不使用StringBuilder,主要是因为StringBuilder没有针对json序列化提出更加有效率的处理方式,而且单就StringBuilder而言,内部是为了实现字符串拼接而生,因为很自然地使用了更加能够读懂的方式进行处理。相比,serializeWriter单处理json序列化数据传输,功能单一,因此在某些方面更加优化一些。
    在类声明中,这里有一个优化措施(笔者最开始未注意到,经作者指出之后才明白)。即是对buf数组的缓存使用,即在一次处理完毕之后,储存的数据容器并不销毁,而是留在当前线程变量中。以便于在当前线程中再次序列化json时使用。源码如下:

    publicSerializeWriter(){

  • buf=bufLocal.get();//newchar[1024];
  • if(buf==null){
  • buf=newchar[1024];
  • }else{
  • bufLocal.set(null);
  • }
  • }
  • 在初始构造时,会从当前线程变量中取buf数组并设置在对象属性buf中。而在每次序列化完成之后,会通过close方法,将此buf数组再次绑定在线程变量当中,如下所示:

    /**

  • *Closethestream.Thismethoddoesnotreleasethebuffer,sinceitscontentsmightstillberequired.Note:
  • *Invokingthismethodinthisclasswillhavenoeffect.
  • */
  • voidclose(){
  • bufLocal.set(buf);
  • }
  • 当然,buf重新绑定了,肯定计数器count应该置0。这是自然,count是对象属性,每次在新建时,自然会置0。

    在实现过程当中,很多具体的实现是借鉴了StringBuilder的处理模式的,在以下的分析中会说到。

    总体分类

    接上篇而言,我们说outWriter主要实现了五个方面的输出内容。
    1,提供writer的基本功能,输出字符,输出字符串
    2,提供对整形和长整形输出的特殊处理
    3,提供对基本类型数组输出的支持
    4,提供对整形+字符的输出支持
    5,提供对字符串+双(单)引号的输出方式
    五个方面主要体现在不同的作用域。第一个提供了最基本的writer功能,以及在输出字符上最基本的功能,即拼接字符数组(不是字符串);第二个针对最常用的数字进行处理;第三个,针对基本类型数组类处理;第四个针对在处理集合/数组时,最后一位的特殊处理,联合了输出数字和字符的双重功能,效率上比两个功能的实现原理上更快一些;第四个,针对字符串的特殊处理(主要是特殊字符处理)以及在json中,字符串的引号处理(即在json中,字符串必须以引号引起来)。

    实现思想

    数据输出最后都变成了拼接字符的功能,即将各种类型的数据转化为字符数组的形式,然后将字符数组拼接到buf数组当中。这中间主要逻辑如下:
    1 对象转化为字符数组
    2 准备装载空间,以容纳数据
    2.1 计数器增加
    2.2 扩容,字符数组扩容
    3 装载数据
    4 计数器计数最新的容量,完成处理
    这里面主要涉及到一个buf数组扩容的概念,其使用的扩容函数expandCapacity其内部实现和StringBuilder中一样。即(当前容量 + 1)* 2,具体可以见相应函数或StringBuilder.ensureCapacityImpl函数。

    实现解析

    基本功能
    基本功能有以下几个函数:

    charc)

    其中第一个函数,可以忽略,可以理解为实现writer中的writ(int)方法,在具体应用时未用到此方法。第2个方法和第7个方法为写单个字符,即往buf数组中写字符。第3,4,5,6,均是写一个字符数组(字符串也可以理解为字符数组)。因此,我们单就字符数组进行分析,源码如下:

    intlen){

  • intnewcount=count+len;//计算新计数量
  • //扩容计算
  • System.arraycopy(c,off,buf,count,len);//拼接字符数组
  • count=newcount;//最终计数
  • 从上注释可以看出,其处理流程和我们所说的标准处理逻辑一致。在处理字符拼接时,尽量使用最快的方法,如使用System.arrayCopy和字符串中的getChars方法。另外几个方法处理逻辑与此方法相同。
    警告:不要在正式应用中对有存在特殊字符的字符串(无特殊字符的字符串除外)使用以上的输出方式,请使用第5组方式进行json输出。对于字符数组的处理在以上处理方式中不会对特殊字符进行处理。如字符串 3\"'4,在使用以上方式输出时,只会输出 3"'4,其中的转义字符在转化为toChar时被删除掉。
    因此,在实际处理中,只有字符数组会使用以上方式进行输出。不要将字符串与字符数组相混合。字符数组不考虑转义问题,而字符串需要考虑转义。

    整形和长整形

    方法如下:

    longi)

  • 这两个方法,按照我们的逻辑,首先需要将整性和长整性转化为字符串(无特殊字符),然后以字符数组的形式输出即可。在进行处理时,主要参考了Integer和Long的toString实现方式和长度计算。首先看一个实现:

    inti)throwsIOException{

  • if(i==Integer.MIN_VALUE){//特殊数字处理
  • write("-2147483648");
  • return;
  • intsize=(i<0)?IOUtils.stringSize(-i)+1:IOUtils.stringSize(i);//计算长度A
  • intnewcount=count+size;
  • IOUtils.getChars(i,newcount,buf);//写入buf数组B
  • //最终定count值
  • 以上首先看特殊数字的处理,因为int的范围从-2147483648到2147483647,因此对于-2147483648这个特殊数字(不能转化为-号+正数的形式),进行特殊处理。这里调用了write(str)方法,实际上就是调用了在第一部分的public void write(String str,int off,int len),这里是安全的,因为没有特殊字符。
    其次是计算长度,两者都借鉴了jdk中的实现,分别为Integer.stringSize和Long.stringSize,这里就不再叙述。
    再写入buf数组,我们说都是将数字转化为字符数组,再定入buf数组中。这里的实现,即按照这个步骤在进行。这里在IOUtils中,借鉴了Integer.getChars(int i,int index,char[] buf)方法和Long.getChars(long i,char[] buf)方法,这里也不再叙述。

    基本类型数组

    long[]array)

  • 数组的形式,主要是将数组的每一部分输出出来,即可。在输出时,需要输出前缀“[”和后缀“]”以及每个数据之间的“,“。按照我们的逻辑,首先还是计算长度,其次是准备空间,再者是写数据,最后是定count值。因此,我们参考一个实现:

    int[]array)int[]sizeArray=int[array.length];//性能优化,用于保存每一位数字长度

  • inttotalSize=2;//初始长度,即[]
  • for(inti=0;i<array.length;++i){
  • if(i!=0){totalSize++;}//追加,长度
  • intval=array[i];
  • //针对每一个数字取长度,此处有部分删除。分别针对minValue和普通value运算
  • intsize=(val<0)?IOUtils.stringSize(-val)+1:IOUtils.stringSize(val);
  • sizeArray[i]=size;
  • totalSize+=size;
  • buf[count]='[';//追加起即数组字符
  • intcurrentSize=count+1;//记录当前位置,以在处理数字时,调用Int的getChars方法
  • 0){buf[currentSize++]=',';}//追加数字分隔符
  • //追加当前数字的字符形式,分别针对minValue和普通数字作处理
  • currentSize+=sizeArray[i];
  • IOUtils.getChars(val,currentSize,buf);
  • buf[currentSize]=']';//追加结尾数组字符
  • //最终count定值
  • 此处有关于性能优化的地方,主要有几个地方。首先将minValue和普通数字分开计算,以避免可能出现的问题;在计算长度时,尽量调用前面使用stringToSize方法,此方法最快;在进行字符追加时,利用getChars方法进行处理。
    对于仍有优化的地方,比如对于boolArray,在处理时,又有了特殊优化,主要还是在上面的两点,计算长度时,尽量地快,以及在字符追加时也尽量的快。以下为对于boolean数据的两个优化点:

    //计算长度,直接取值,不需要进行计算

  • if(val){
  • size=4;//"true".length();
  • else{}
  • //追加字符时,不需要调用默认的字符拼接,直接手动拼接,减少中间计算量
  • booleanval=array[i];
  • //System.arraycopy("true".toCharArray(),4);
  • buf[currentSize++]='t';
  • buf[currentSize++]='r';
  • buf[currentSize++]='u';
  • buf[currentSize++]='e';
  • else{/**省略**/}
  • 数字+字符输出

    charc)

    以上两个方法主要在处理以下情况下使用,在不知道要进行序列化的对象的长度的情况下,要尽量避免进行buf数据扩容的情况出现。尽管这种情况很少发生,但还是尽量避免。特殊是在输出集合数据的情况下,在集合数据输出下,各个数据的长度未定,因此不能计算出总输出长度,只能一个对象一个对象输出,在这种情况下,先要输出一个对象,然后再输出对象的间隔符或结尾符。如果先调用输出数据,再调用输出间隔符或结尾符,远不如将两者结合起来,一起进行计算和输出。
    此方法基于以下一个事实:尽量在已知数据长度的情况下进行字符拼接,这样有利于快速的为数据准备数据空间。
    在具体实现时,此方法只是减少了数据扩容的计算,其它方法与基本实现和组合是一致的,以writeIntAndChar为例:

    charc)//minValue处理

  • //长度计算,长度为数字长度+字符长度
  • 1:IOUtils.stringSize(i);
  • intnewcount0=count+size;
  • intnewcount1=newcount0+1;
  • //输出数字
  • buf[newcount0]=c;//输出字符
  • count=newcount1; 字符串处理

    作为在业务系统中最常用的类型,字符串是一个必不可少的元素之一。在json中,字符串是以双(单)引号,引起来使用的。因此在输出时,即要在最终的数据上追加双(单)引号。否则,js会将其作为变量使用而报错。而且在最新的json标准中,对于json中的key,也要求必须追加双(单)引号以示区分了。字符串处理方法有以下几种:

    voidwriteKeyWithSingleQuoteIfHashSpecial(Stringtext)

  • 其中第1,2方法表示分别用双引号和单引号将字符串包装起来,第3,4方法表示在字符串输出完毕之后,再输出一个冒号,第5方法表示输出一个字符串数组,使用双引号包装字符串。第7,8方法未知(不明真相的方法?)
    字符串是可以知道长度的,所以第一步确定长度即OK了。 在第一步扩容计算之后,需要处理一个在字符串中特殊的问题,即转义字符处理。如何处理转义字符,以及避免不必要的扩容计算,是必须要考虑的。在fastjson中,采取了首先将其认定为全非特殊字符,然后再一个个字符判断,对特殊字符再作处理的方法。在一定程序上避免了在一个个判断时,扩容计算的问题。我们就其中一个示例进行分析:

    voidwriteStringWithDoubleQuote(Stringtext){

  • //null处理,直接追加null字符即可,不需要双引号
  • intlen=text.length();
  • intnewcount=count+len+//初始计算长度为字符串长度+2(即双引号)
  • //初步扩容计算
  • intstart=count+intend=start+len;
  • buf[count]='\"';//追加起始双引号
  • text.getChars(0,len,start);
  • //初步定count值
  • /**以下代码为处理特殊字符*/
  • inti=start;i<end;++i){
  • charch=buf[i];
  • if(ch=='\b'||ch=='\n'||ch=='\r'||ch=='\f'||ch=='\\'||ch=='/'||ch=='"'){//判断是否为特殊字符
  • //这里需要修改count值,以及扩容判断,省略之
  • System.arraycopy(buf,i+1,0)">2,end-i-1);//数据移位,从当前处理点往后移
  • buf[i]='\\';//追加特殊字符标记
  • buf[++i]=replaceChars[(int)ch];//追加原始的特殊字符为\b写为b,最终即为\\b的形式,而不是\\\b
  • end++;
  • buf[newcount-1]='\"';//转出结尾双引号
  • 在处理字符串上,特殊的即在特殊字符上。因为在输出时,要输出时要保存字符串的原始模式,如\"的格式,要输出时,要输出为\ + "的形式,而不能直接输出为\",后者在输出时就直接输出为",而省略了\,这在js端是会报错的。

    总结:

    在针对输出优化时,主要利用了最有效率的手段进行处理。如针对数字和boolean时的处理方式。同时,在处理字符串时,也采取了先处理最常用字符,再处理特殊字符的形式。在针对某些经常碰到的场景时,使用了联合处理的手段(如writeIntAndChar),而不再是分开处理。
    整个处理的思想,即是在处理单个数据时,采取最优方式;在处理复合数据时,避免扩容计算;尽量使用jdk中的方法,以避免重复轮子(可能轮子更慢)。

    下一篇,从数据处理过程对源码进行分析,同时解析其中针对性能优化的处理部分。


    前面讲了进行对象解析的两个方面,并讲了针对outWriter将不同类型的数据信息写到buf字符数组。本篇讲解对象解析的过程,即如何将不同类型的对象解析成outWriter所需要的序列信息。并考虑其中的性能优化。

    取得解析器
    首先我们需要取得指定对象的json序列化器,以便使用特定的序列化器来序列化对象。因此,需要有一个方法来取得相对应的序列化器。在fastjson中,使用了一个类似map的结构来保存对象类型和及对应的解析器。对于对象类型,在整个fastjson中,分为以下几类:

    1基本类型以及其包装类型,字符串
    2基本类型数组以及包装类型数组
    3Atomic类型
    4JMX类型
    5集合类型以及子类
    6时间类型
    7json类型
    8对象数组类型
    9javaBean类型

    对于第1,2,3,4类型,在fastjson中使用了一个全局的单态实例来保存相对应的解析器;第5类型,处理集合类型,对于集合类型及其,由于其处理逻辑均是一样,所以只需要针对子类作一些的处理,让其返回相对应的集合类型解析器即可;第6类型,时间处理器,将时间转化为类似yyyy-MM-ddTHH:mm:ss.SSS的格式;第7类型,处理fastjson专有jsonAwre类型;第8类型,处理对象的数组形式,即处理数组时,需要考虑数组中的统一对象类型;第9,即处理我们最常使用的对象,javaBean类型,这也是在项目中解析得最多的类型。

    我们要看一下相对应的取解析器的方法,即类JsonSerializer.getObjectWriter(Class<?> clazz)方法,参考其中的实现:

    publicObjectSerializergetObjectWriter(Class<?>clazz){

  • ObjectSerializerwriter=mapping.get(clazz);
  • if(writer==if(Map.class.isAssignableFrom(clazz)){
  • elseif(List.if(Collection.if(Date.if(JSONAware.if(JSONStreamAware.if(clazz.isEnum()){
  • if(clazz.isArray()){
  • Class<?>componentType=clazz.getComponentType();
  • ObjectSerializercompObjectSerializer=getObjectWriter(componentType);
  • if(Throwable.newJavaBeanSerializer(clazz));
  • writer=mapping.get(clazz);
  • returnwriter;
  • 首先,取对象解析器是由一个类型为JSONSerializerMap的对象mapping中取。之所以使用此类型,这是性能优化的一部分,此类型并不是一个完整的map类型,只是实现了一个类似map的操作的类型。其内部并没有使用基于equals的比较方式,而是使用了System.identityHashCode的实现。对于一个对象,其identityHashCode的值是定值,而对于一个类型,在整个jvm中只有一个,因此这里使用基于class的identityHashCode是可以了,也避免了使用class的euqlas来进行比较。
    接着再根据每一个类型从mapping中取出相对应的解析器。首先从继承而来的全局解析器取得解析器,如果对象不属于第1,4类型,而开始进入以下的if else阶段。
    我们从上面的源码中,可以看出解析器主要分为两个部分,一个是与解析类型相关的,一个是无关的。比如对于第5,7类型,其中最5类型是集合类型,由于不知道集合类型中的具体类型(因为存在继承关系),所以类型无关。对于第8,9类型,其中第8类型为对象数组类型,对于对象数组,数组中的每一个对象的类型都是确定的,且整个数组只有一种类型,因此可以确定其类型,这时候就要使用类型相关解析器了,对于第9类型,需要使用解析对象的类型来确定相对应的javaBean属性,因此是类型相关。
    另外一个确定解析器的过程当中,使用了映射机制,即将当前解析器与对应的类型映射起来,以便下一次时使用。对于集合类型及子类型,由于当前类型并不是确定的List或Collection类型,因此将当前类型与集合解析器映射起来。对于对象类型,需要将当前类型传递给相对应的解析器,以确定具体的属性。

    解析过程

    解析方法由统一的接口所定义,每个不同的解析器只需要实际这个方法并提供各自的实现即可。此方法为write(JSONSerializer serializer,Object object),由ObjectSerializer提供。带两个参数,第一个参数,即为解析的起点类jsonSerializer,此类封装了我们所需要的outWriter类,需要时只需要从此类取出即可。第二个参数为我们所要解析的对象,在各个子类进行实现时,可将此对象通过强制类型转换,转换为所需要的类型即可。
    具体的解析过程根据不同的数据类型不所不同,对于第1,2类型,由于在outWriter中均有相对应的方法,所以在具体实现时,只需要调用相应的outWriter方法即可,而不需要作额外的工作。比如对于字符串解析器,它的解析过程如下所示:

    Stringvalue=(String)object;

  • if(serializer.isEnabled(SerializerFeature.UseSingleQuotes)){
  • out.writeStringWithSingleQuote(value);
  • out.writeStringWithDoubleQuote(value);
  • 即首先,取得输出时所需要的outWriter,然后再根据配置决定是输出单引号+字符串还是双引号+字符串。

    而对于其它并没有由outWriter所直接支持的类型而言,解析过程就相对于复杂一些。但是总的逻辑还是基于以下逻辑:

    1. 基于数据类型特点输出所特有的字符包装内容
    2. 基于数据类型特点转换为outWriter所能识别的内容
    3. 逐步解析,将对象解析产生的字符数组输出到outWriter中

    只需此三步,即可完美的解析一个对象。因此,我们可以参考其中的一个具体实现,并讨论其中的具体优化措施。
    在取得解析器方法getObjectWriter(Class<?> clazz)中,我们可以看到,对于集合类型中的Collection和List,fastjson是分开进行解析的。即两者在解析时在细微处有着不一样的实现。两者的区别在于List是有序的,可以根据下标对元素进行访问,对于常用List实现,ArrayList,使用下标访问子元素的价格为O1。这就是在fastJson中采取的一点优化措施,详细看以下实现代码:

    final SerializeWriterout=serializer.getWrier();//取得输出器

  • List<?>list=(List<?>)object;//强制转换为所需类型
  • intsize=list.size();
  • intend=size-//此处定义为size-1,是因为对最后一位有特殊处理
  • //空集合判断,省略之
  • out.append('[');//集合前缀包装
  • /**以下代码使用get(X)方法访问下标,实现代码对于ArrayList实现有好处,对于LinkedList是否有好处,还待考虑*/
  • 0;i<end;++i){
  • Objectitem=list.get(i);
  • //空值判断
  • Class<?>clazz=item.getClass();
  • if(clazz==Integer.class){//针对Integer.class特殊优化,使用outWriter自带方法
  • out.writeIntAndChar(((Integer)item).intValue(),',');
  • if(clazz==Long.//针对Long.class特殊优化,使用outWriter自带方法
  • longval=((Long)item).longValue();
  • out.writeLongAndChar(val,250); line-height:18px"> serializer.write(item);//递归调用,写集合内元素
  • out.append(',');//间隔符
  • /**以下代码为最后一位优化,当为最后一位时,不再需要输出间隔符,而是输出后缀包装符
  • 这里即在处理时,直接输出后缀,与前面输出间隔符相对应*/
  • Objectitem=list.get(end);
  • class){
  • ']');
  • out.writeLongAndChar(((Long)item).longValue(),250); line-height:18px"> serializer.write(item);
  • out.append(']');
  • 以下实现与collection相比不同的即在于处理中间元素与末尾元素的区别。相对于Collection,就不能使用以上的方法了,在实现上,就只能先输出前缀,再一个一个地处理里面的元素,最后输出后缀。此实现如下所示:

    Collection<?>collection=(Collection<?>)object;

  • out.append('[');
  • booleanfirst=true;
  • for(Objectitem:collection){
  • if(!first){out.append(',');}
  • first=false;
  • //Integer.class和Long.class特殊处理
  • 以上代码就是通常最常见的实现了。

    相对于集合类型实现,map实现和javaBean实现相对来说,稍微复杂了一点。主要是输出key和value的问题。在fastjson中,key输出表现为使用outWriter的writeKey来进行输出,value输出则同样使用常规的输出。按照我们先前所说的逻辑,首先还是输出包装字符内容,即{,在末尾输出}。然后,再根据每个key-value映射特点,采取相对应的输出方式。
    当然,对于map类型输出和javaBean输出还是不一样的。两者可以互相转换,但fastjson在输出时还是采取了不一样的输出方式。那么,我们以源代码来查看相应的实现:

    Map<?,?>map=(Map<?,?>)object;

  • out.write('{');//前缀
  • Class<?>preClazz=null;//缓存前一个value类型和相对应的解析器,减少类型判断解析
  • ObjectSerializerpreWriter=null;
  • for(Map.Entry<?,?>entry:map.entrySet()){
  • //此处有删除,即根据nameFilter和valueFilter针对key-value作转换处理
  • if(!first){out.write(',');}//输出间隔符
  • serializer.writeFieldName(key);//输出字段名+冒号
  • Class<?>clazz=value.getClass();
  • if(clazz==preClazz){//此处即细节优化内容,直接使用前一个解析器,避免再次从jsonSerializer中查找
  • preWriter.write(serializer,value);
  • /**此处则就需要从jsonSerializer中查找解析器,并输出了*/
  • preClazz=clazz;
  • preWriter=serializer.getObjectWriter(clazz);
  • out.write('}');//后缀
  • 由上可以看出,map的实现还是按照我们常用的思路在进行。在性能优化处,采取了两个优化点,一是输出字段时,并不直接输出字段名,而是采取字段+冒号的方式一起输出,减少outWriter扩容计算。二是在查找value解析器时,尽量使用前一个value的解析器,避免重复查找。
    相比map,javaBean的实现就相对更复杂。javaBean输出并不是采取key-value的方式,而是采取类似fieldSerializer的输出方式,即将属性名和值,组合成一个字段,一起进行输出。当然输出时还是先输出字段名,再输出值的实现。那么对于javaBean实现,首先要取得当前对象类型的所有可以输出的类型。
    在fastjson实现中,并没有采取javaBean属性的读取方式,而是采取了使用getXXX和isXXX方法的读取模式,来取得一个类型的可读取属性。作为性能优化的一部分,读取属性的操作结果被缓存到了getter缓存器中。其实,并不是缓存到了getter缓存器中,只是该类型的javaBean序列化器对象被缓存到了jsonSerializer的对象类型-序列化器映射中。对于同一个类型,就不需要再次解析该类型的属性了。
    有了相对应的字段,那么在实现时,就按照相应的字段进行输出即可。以下为实现代码:

    out.append('{');0;i<getters.length;++i){

  • FieldSerializergetter=getters[i];//取属性解析器
  • ObjectpropertyValue=getter.getPropertyValue(object);//取值
  • //省略中间nameFilter和valueFilter过滤处理
  • if(commaFlag){out.append(',0); padding:0px; margin:0px; width:auto; border:0px">//省略nameFilter和valueFilter过滤之后的输出处理
  • getter.writeProperty(serializer,propertyValue);//使用字段解析器输出内容
  • out.append('}'); 由上可见,javaBean的输出实际上和map输出差不多。只不过这里又把属性的解析和输出封装了一层。在使用字段解析器(由FieldSerializer标识)输出字段值时,实际上也是先输出字段名+冒号,再输出字段值。这里就不再详细叙述。

    总结

    在整个解析过程中,更多的是根据对象类型查找到对象解析器,再使用对象解析器序列化对象的过程。在这中间,根据不同的对象采取不同的解析,并在实现中采取部分优化措施,以尽量地提高解析效率,减少中间运算。减少中间运算,是在解析过程中采取的最主要的优化办法。实际上,最主要的优化措施还是体现在outWriter中对于数据的处理上。此处更多的是设计模式的使用,以实现繁多的对象的解析。 整个fastjson的序列化部分,就到此为止。单就笔者而言,在查看源代码的时候,也发现了一些问题,可能是作者未考虑的问题,或者是实际中未遇到。但在版本升级过程中,也渐渐地对功能进行了增强,比如对于@JsonField注解的使用,NameFilter和ValueFilter的使用,使fastjson越来越符合业务系统的需要。如果可以,笔者会将其用到笔者所在的项目中,而不再重复发明轮子:)

    版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

    相关推荐


    文章浏览阅读2.4k次。最近要优化cesium里的热力图效果,浏览了网络上的各种方法,发现大多是贴在影像上的。这么做好是好,但是会被自生添加的模型或者其他数据给遮盖。其次是网上的方法大多数是截取成一个矩形。不能自定义的截取自己所需要的。经过尝试,决定修改下cesium heatmap,让他达到我们需要的要求。首先先下载 cesium heatmap包。其中我们可以看到也是通过叠加entity达到添加canvas的方法绘制到地图上。我们先把这一段代码注释} else {} };
    文章浏览阅读1.2w次,点赞3次,收藏19次。在 Python中读取 json文件也可以使用 sort ()函数,在这里我介绍一个简单的示例程序: (4)如果我们想将字符串转换为列表形式,只需要添加一个变量来存储需要转换的字符串即可。在上面的代码中,我们创建了一个名为` read`的对象,然后在文件的开头使用`./`关键字来命名该对象,并在文件中定义了一个名为` json`的变量,并在其中定义了一个名为` json`的字段。比如,我们可以使用 read方法读取 json文件中的内容,然后使用 send方法将其发送到 json文件中。_python怎么读取json文件
    文章浏览阅读1.4k次。首字母缩略词 API 代表应用程序编程接口,它是一种设备,例如用于使用编程代码发送和检索数据的服务器。最常见的是,该技术用于从源检索数据并将其显示给软件应用程序及其用户。当您访问网页时,API 的工作方式与浏览器相同,信息请求会发送到服务器,如何在 Windows PC 中手动创建系统还原点服务器会做出响应。唯一的区别是服务器响应的数据类型,对于 API,数据是 JSON 类型。JSON 代表 JavaScript Object Notation,它是大多数软件语言中 API 的标准数据表示法。_api是什么 python
    文章浏览阅读802次,点赞10次,收藏10次。解决一个JSON反序列化问题-空字符串变为空集合_cannot coerce empty string ("") to element of `java.util.arraylist
    文章浏览阅读882次。Unity Json和Xml的序列化和反序列化_unity json反序列化存储换行
    文章浏览阅读796次。reader.readAsText(data.file)中data.file的数据格式为。使用FileReader对象读取文件内容,最后将文件内容进行处理使用。_a-upload 同时支持文件和文件夹
    文章浏览阅读775次,点赞19次,收藏10次。fastjson是由国内的阿里推出的一种json处理器,由java语言编写,无依赖,不需要引用额外的jar包,能直接运行在jdk环境中,它的解析速度是非常之快的,目前超过了所有json库。提示:以下是引用fastjson的方法,数据未涉及到私密信息。_解析器用fastjson还是jackson
    文章浏览阅读940次。【Qt之JSON文件】QJsonDocument、QJsonObject、QJsonArray等类介绍及使用_使用什么方法检查qjsondocument是否为空
    文章浏览阅读957次,点赞34次,收藏22次。主要内容原生 ajax重点重点JSON熟悉章节目标掌握原生 ajax掌握jQuery ajax掌握JSON第一节 ajax1. 什么是ajaxAJAX 全称为,表示异步的Java脚本和Xml文件,是一种异步刷新技术。2. 为什么要使用ajaxServlet进行网页的变更往往是通过请求转发或者是重定向来完成,这样的操作更新的是整个网页,如果我们只需要更新网页的局部内容,就需要使用到AJAX来处理了。因为只是更新局部内容,因此,Servlet。
    文章浏览阅读1.4k次,点赞45次,收藏13次。主要介绍了JsonFormat与@DateTimeFormat注解实例解析,文中通过示例代码介绍的非常详细,对大家的学习 或者工作具有一定的参考学习价值,需要的朋友可以参考下 这篇文章主要介绍了从数据库获取时间传到前端进行展示的时候,我们有时候可能无法得到一个满意的时间格式的时间日期,在数据库中显 示的是正确的时间格式,获取出来却变成了时间戳,@JsonFormat注解很好的解决了这个问题,我们通过使用 @JsonFormat可以很好的解决:后台到前台时间格式保持一致的问题,
    文章浏览阅读1k次。JsonDeserialize:json反序列化注解,作用于setter()方法,将json数据反序列化为java对象。可以理解为用在处理接收的数据上。_jsondeserialize
    文章浏览阅读2.7k次。labelme标注的json文件是在数据标注时产生,不能直接应用于模型训练。各大目标检测训练平台或项目框架均有自己的数据格式要求,通常为voc、coco或yolo格式。由于yolov8项目比较火热,故此本博文详细介绍将json格式标注转化为yolo格式的过程及其代码。_labelme json 转 yolo
    文章浏览阅读790次,点赞26次,收藏6次。GROUP_CONCAT_UNORDERED(): 与GROUP_CONCAT类似,但不保证结果的顺序。COUNT_DISTINCT_AND_ORDERED(): 计算指定列的不同值的数量,并保持结果的顺序。COUNT_ALL_DISTINCT(): 计算指定列的所有不同值的数量(包括NULL)。AVG_RANGE(): 计算指定列的最大值和最小值之间的差异的平均值。JSON_OBJECT(): 将结果集中的行转换为JSON对象。COUNT_DISTINCT(): 计算指定列的不同值的数量。_mysql json 聚合
    文章浏览阅读1.2k次。ajax同步与异步,json-serve的安装与使用,node.js的下载_json-serve 与node版本
    文章浏览阅读1.7k次。`.net core`提供了Json处理模块,在命名空间`System.Text.Json`中,下面通过顶级语句,对C#的Json功能进行讲解。_c# json
    文章浏览阅读2.8k次。主要介绍了python对于json文件的读写操作内容_python读取json文件
    文章浏览阅读770次。然而,有时候在处理包含中文字符的Json数据时会出现乱码的情况。本文将介绍一种解决Json中文乱码问题的常见方法,并提供相应的源代码和描述。而某些情况下,中文字符可能会被错误地编码或解码,导致乱码的出现。通过适当地控制编码和解码过程,我们可以有效地处理包含中文字符的Json数据,避免乱码的发生。通过控制编码和解码过程,我们可以确保Json数据中的中文字符能够正确地传输和解析。为了解决这个问题,我们可以使用C#的System.Text.Encoding类提供的方法进行编码和解码的控制。_c# json 中文编码
    文章浏览阅读997次。【代码】【工具】XML和JSON互相转换。_xml 转json
    文章浏览阅读1.1k次。json path 提取数据_jsonpath数组取值
    文章浏览阅读3w次,点赞35次,收藏36次。本文主要介绍了pandas read_json时ValueError: Expected object or value的解决方案,希望能对学习python的同学们有所帮助。文章目录1. 问题描述2. 解决方案_valueerror: expected object or value