论坛首页 Java企业应用论坛

alibaba fastjson(json序列化器)序列化部分源码解析

浏览 20095 次
该帖已经被评为精华帖
作者 正文
   发表时间:2011-06-20   最后修改:2011-06-20

本文copy自http://www.flydmeng.com/index.php/code/alibaba-fastjson-json-serializer-chapter-source-analyse-one-global-analyse.html

 

 

fastjson官方地址: http://code.alibabatech.com/wiki/display/FastJSON/Home
    从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信息。

 

 SerializeWriter out = new SerializeWriter();
        JSONSerializer serializer = new JSONSerializer(out);
        serializer.write(object);
        return out.toString();
 

 

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

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

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

 

public void write(int c)
public void write(char c)
public void write(char c[], int off, int len)
public void write(String str, int off, int len)
public SerializeWriter append(CharSequence csq)
public SerializeWriter append(CharSequence csq, int start, int end)
public SerializeWriter append(char c)
 
public void writeInt(int i)
public void writeLong(long i)
 
public void writeBooleanArray(boolean[] array)
public void writeShortArray(short[] array)
public void writeByteArray(byte[] array)
public void writeIntArray(int[] array)
public void writeIntArray(Integer[] array)
public void writeLongArray(long[] array)
 
public void writeIntAndChar(int i, char c)
public void writeLongAndChar(long i, char c)
 
public void writeStringWithDoubleQuote(String text)
public void writeKeyWithDoubleQuote(String text)
public void writeStringWithSingleQuote(String text)
public void writeStringArray(String[] array)
public void writeKeyWithSingleQuote(String text)
public void writeKeyWithDoubleQuoteIfHashSpecial(String text)
public void writeKeyWithSingleQuoteIfHashSpecial(String text)
 

 

    五个部分的方法,每个部分都有其特殊的作用和意义,针对最常用的数字和字符串作了特别的对待。

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

    对象序列化入口

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

 

public JSONSerializer(SerializeWriter out){
        this(out, JSONSerializerMap.getGlobalInstance());
    }
 

 

这时使用了全局的一个对象序列化器映射,加上后面在getObjectWriter中追加的对象序列化器映射。在整个jsonSerializer中,可以使用的对象序列化器有以下这些:

 

 

 

put(Boolean.class, BooleanSerializer.instance);
        put(Byte.class, ByteSerializer.instance);
        put(Short.class, ShortSerializer.instance);
        put(Integer.class, IntegerSerializer.instance);
        put(Long.class, LongSerializer.instance);
        put(Float.class, FloatSerializer.instance);
        put(Double.class, DoubleSerializer.instance);
        put(BigDecimal.class, BigDecimalSerializer.instance);
        put(BigInteger.class, BigIntegerSerializer.instance);
        put(String.class, StringSerializer.instance);
        put(byte[].class, ByteArraySerializer.instance);
        put(short[].class, ShortArraySerializer.instance);
        put(int[].class, IntArraySerializer.instance);
        put(long[].class, LongArraySerializer.instance);
        put(float[].class, FloatArraySerializer.instance);
        put(double[].class, DoubleArraySerializer.instance);
        put(boolean[].class, BooleanArraySerializer.instance);
        put(Integer[].class, IntegerArraySerializer.instance);
        put(String[].class, StringArraySerializer.instance);
        put(Object[].class, ObjectArraySerializer.instance);
        put(Class.class, ClassSerializer.instance);
 
        // atomic
        put(AtomicBoolean.class, AtomicBooleanSerializer.instance);
        put(AtomicInteger.class, AtomicIntegerSerializer.instance);
        put(AtomicLong.class, AtomicLongSerializer.instance);
        put(AtomicReference.class, AtomicReferenceSerializer.instance);
        put(AtomicIntegerArray.class, AtomicIntegerArraySerializer.instance);
        put(AtomicLongArray.class, AtomicLongArraySerializer.instance);
 
        // jmx
        put(CompositeData.class, CompositeDataSerializer.instance);
        put(CompositeDataSupport.class, CompositeDataSerializer.instance);
        put(TabularData.class, TabularDataSerializer.instance);
        put(TabularDataSupport.class, TabularDataSerializer.instance);
        put(ObjectName.class, ObjectNameSerializer.instance);
        put(SimpleType.class, SimpleTypeSerializer.instance);
 
        //在执行过程中追加部分
                mapping.put(clazz, MapSerializer.instance);
                mapping.put(clazz, ListSerializer.instance);
                mapping.put(clazz, CollectionSerializer.instance);
                mapping.put(clazz, DateSerializer.instance);
                mapping.put(clazz, JSONAwareSerializer.instance);
                mapping.put(clazz, JSONStreamAwareSerializer.instance);
                mapping.put(clazz, EnumSerializer.instance);
                mapping.put(clazz, new ArraySerializer(compObjectSerializer));
                mapping.put(clazz, new ExceptionSerializer(clazz));
                mapping.put(clazz, new JavaBeanSerializer(clazz));

 

     这些序列化器,覆盖了基本数据,字符串类型,日期,以及集合,map,以及javaBean的所有序列化器。因为不存在没有匹配不了的序列化器。既然有个序列化器,就可以执行序列化工作了。即到了序列化入口应该做的工作了。

 

Class<?> clazz = object.getClass();
            ObjectSerializer writer = getObjectWriter(clazz);
            writer.write(this, object);

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

 

 

    序列化过程

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

void write(JSONSerializer serializer, Object object) throws IOException

 

 

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

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

 

 


 

 

SerializeWriter out = serializer.getWrier();
        Integer value = (Integer) object;
        out.writeInt(value.intValue());

 

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

 

总结

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

 

 


 

 

 

 

 

  • 大小: 64.6 KB
   发表时间:2011-06-20   最后修改:2011-06-20

本文copy自 http://www.flydmeng.com/index.php/code/alibaba-fastjson-serializer-source-analyse-2-performence-optimize-a.html (转请贴博客地址)

 

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

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

public SerializeWriter(){
        buf = bufLocal.get(); // new char[1024];
        if (buf == null) {
            buf = new char[1024];
        } else {
            bufLocal.set(null);
        }
    }

 

 在初始构造时,会从当前线程变量中取buf数组并设置在对象属性buf中。而在每次序列化完成之后,会通过close方法,将此buf数组再次绑定在线程变量当中,如下所示:

/**
     * Close the stream. This method does not release the buffer, since its contents might still be required. Note:
     * Invoking this method in this class will have no effect.
     */
    public void close() {
        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函数。

 

    实现解析

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

public void write(int c)
public void write(char c)
public void write(char c[], int off, int len)
public void write(String str, int off, int len)
public SerializeWriter append(CharSequence csq)
public SerializeWriter append(CharSequence csq, int start, int end)
public SerializeWriter append(char c)

 

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

public void write(char c[], int off, int len) {
        int newcount = count + len;//计算新计数量
        //扩容计算
        System.arraycopy(c, off, buf, count, len);//拼接字符数组
        count = newcount;//最终计数
    }

 

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

    整形和长整形

    方法如下:

public void writeInt(int i)
public void writeLong(long i)

 

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

public void writeInt(int i) throws IOException {
        if (i == Integer.MIN_VALUE) {//特殊数字处理
            write("-2147483648");
            return;
        }
 
        int size = (i < 0) ? IOUtils.stringSize(-i) + 1 : IOUtils.stringSize(i);//计算长度 A
        int newcount = count + size;
  //扩容计算
        IOUtils.getChars(i, newcount, buf);//写入buf数组 B
        count = newcount;//最终定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, int index, char[] buf)方法,这里也不再叙述。

    基本类型数组

public void writeBooleanArray(boolean[] array)
public void writeShortArray(short[] array)
public void writeByteArray(byte[] array)
public void writeIntArray(int[] array)
public void writeIntArray(Integer[] array)
public void writeLongArray(long[] array)

 

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

public void writeIntArray(int[] array) throws IOException {
        int[] sizeArray = new int[array.length];//性能优化,用于保存每一位数字长度
        int totalSize = 2;//初始长度,即[]
        for (int i = 0; i < array.length; ++i) {
            if (i != 0) {totalSize++;}//追加,长度
            int val = array[i];
//针对每一个数字取长度,此处有部分删除。分别针对minValue和普通value运算
            int size = (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils.stringSize(val);
            sizeArray[i] = size;
            totalSize += size;
        }
//扩容计算
        buf[count] = '[';//追加起即数组字符
 
        int currentSize = count + 1;//记录当前位置,以在处理数字时,调用Int的getChars方法
        for (int i = 0; i < array.length; ++i) {
            if (i != 0) {buf[currentSize++] = ',';} //追加数字分隔符
 
//追加当前数字的字符形式,分别针对minValue和普通数字作处理
            int val = array[i];
                currentSize += sizeArray[i];
                IOUtils.getChars(val, currentSize, buf);
        }
        buf[currentSize] = ']';//追加结尾数组字符
        count = newcount;//最终count定值
    }

 

    此处有关于性能优化的地方,主要有几个地方。首先将minValue和普通数字分开计算,以避免可能出现的问题;在计算长度时,尽量调用前面使用stringToSize方法,此方法最快;在进行字符追加时,利用getChars方法进行处理。
    对于仍有优化的地方,比如对于boolArray,在处理时,又有了特殊优化,主要还是在上面的两点,计算长度时,尽量地快,以及在字符追加时也尽量的快。以下为对于boolean数据的两个优化点:

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

 

数字+字符输出

public void writeIntAndChar(int i, char c)
public void writeLongAndChar(long i, char c)

 

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

public void writeIntAndChar(int i, char c) throws IOException {
        //minValue处理
//长度计算,长度为数字长度+字符长度
        int size = (i < 0) ? IOUtils.stringSize(-i) + 1 : IOUtils.stringSize(i);
        int newcount0 = count + size;
        int newcount1 = newcount0 + 1;
//扩容计算
        IOUtils.getChars(i, newcount0, buf);//输出数字
        buf[newcount0] = c;//输出字符
        count = newcount1;//最终count定值
    }

 

字符串处理

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

public void writeStringWithDoubleQuote(String text)
public void writeStringWithSingleQuote(String text)
public void writeKeyWithDoubleQuote(String text)
public void writeKeyWithSingleQuote(String text)
public void writeStringArray(String[] array)
public void writeKeyWithDoubleQuoteIfHashSpecial(String text)
public void writeKeyWithSingleQuoteIfHashSpecial(String text)

 

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

public void writeStringWithDoubleQuote(String text) {
//null处理,直接追加null字符即可,不需要双引号
        int len = text.length();
        int newcount = count + len + 2;//初始计算长度为字符串长度+2(即双引号)
//初步扩容计算
 
        int start = count + 1;
        int end = start + len;
        buf[count] = '\"';//追加起始双引号
        text.getChars(0, len, buf, start);
        count = newcount;//初步定count值
/** 以下代码为处理特殊字符 */
        for (int i = start; i < end; ++i) {
            char ch = buf[i];
            if (ch == '\b' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\\' || ch == '/' || ch == '"') {//判断是否为特殊字符
//这里需要修改count值,以及扩容判断,省略之
                System.arraycopy(buf, i + 1, buf, i + 2, end - i - 1);//数据移位,从当前处理点往后移
                buf[i] = '\\';//追加特殊字符标记
                buf[++i] = replaceChars[(int) ch];//追加原始的特殊字符为\b写为b,最终即为\\b的形式,而不是\\\b
                end++;
            }
        }
 
        buf[newcount - 1] = '\"';//转出结尾双引号
    }

 

    在处理字符串上,特殊的即在特殊字符上。因为在输出时,要输出时要保存字符串的原始模式,如\"的格式,要输出时,要输出为\ + "的形式,而不能直接输出为\",后者在输出时就直接输出为",而省略了\,这在js端是会报错的。

    总结:

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

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

 

0 请登录后投票
   发表时间:2011-06-20   最后修改:2011-06-20

本文自 http://www.flydmeng.com/index.php/code/alibaba-fastjson-serializer-source-analyse-2-performence-optimize-b-html.html (转请贴博客地址)

 

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

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

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

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

 

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

public ObjectSerializer getObjectWriter(Class<?> clazz) {
        ObjectSerializer writer = mapping.get(clazz);
        if (writer == null) {
            if (Map.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, MapSerializer.instance);
            } else if (List.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, ListSerializer.instance);
            } else if (Collection.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, CollectionSerializer.instance);
            } else if (Date.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, DateSerializer.instance);
            } else if (JSONAware.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, JSONAwareSerializer.instance);
            } else if (JSONStreamAware.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, JSONStreamAwareSerializer.instance);
            } else if (clazz.isEnum()) {
                mapping.put(clazz, EnumSerializer.instance);
            } else if (clazz.isArray()) {
                Class<?> componentType = clazz.getComponentType();
                ObjectSerializer compObjectSerializer = getObjectWriter(componentType);
                mapping.put(clazz, new ArraySerializer(compObjectSerializer));
            } else if (Throwable.class.isAssignableFrom(clazz)) {
                mapping.put(clazz, new ExceptionSerializer(clazz));
            } else {
                mapping.put(clazz, new JavaBeanSerializer(clazz));
            }
            writer = mapping.get(clazz);
        }
        return writer;
    }

 

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

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

SerializeWriter out = serializer.getWrier();
        String value = (String) object;
        if (serializer.isEnabled(SerializerFeature.UseSingleQuotes)) {
            out.writeStringWithSingleQuote(value);
        } else {
            out.writeStringWithDoubleQuote(value);
        }

 

    即首先,取得输出时所需要的outWriter,然后再根据配置决定是输出单引号+字符串还是双引号+字符串。

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

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

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

public final void write(JSONSerializer serializer, Object object) throws IOException {
        SerializeWriter out = serializer.getWrier();//取得输出器
        List<?> list = (List<?>) object;//强制转换为所需类型
 
        final int size = list.size();
        int end = size - 1;//此处定义为size-1,是因为对最后一位有特殊处理
//空集合判断,省略之
        out.append('[');//集合前缀包装
/** 以下代码使用get(X)方法访问下标,实现代码对于ArrayList实现有好处,对于LinkedList是否有好处,还待考虑 */
        for (int i = 0; i < end; ++i) {
            Object item = list.get(i);
            //空值判断
                Class<?> clazz = item.getClass();
                if (clazz == Integer.class) {//针对Integer.class特殊优化,使用outWriter自带方法
                    out.writeIntAndChar(((Integer) item).intValue(), ',');
                } else if (clazz == Long.class) {//针对Long.class特殊优化,使用outWriter自带方法
                    long val = ((Long) item).longValue();
                    out.writeLongAndChar(val, ',');
                } else {
                    serializer.write(item);//递归调用,写集合内元素
                    out.append(',');//间隔符
                }
        }
 
/** 以下代码为最后一位优化,当为最后一位时,不再需要输出间隔符,而是输出后缀包装符
这里即在处理时,直接输出后缀,与前面输出间隔符相对应 */
        Object item = list.get(end);
            Class<?> clazz = item.getClass();
 
            if (clazz == Integer.class) {
                out.writeIntAndChar(((Integer) item).intValue(), ']');
            } else if (clazz == Long.class) {
                out.writeLongAndChar(((Long) item).longValue(), ']');
            } else {
                serializer.write(item);
                out.append(']');
            }
    }

 

    以下实现与collection相比不同的即在于处理中间元素与末尾元素的区别。相对于Collection,就不能使用以上的方法了,在实现上,就只能先输出前缀,再一个一个地处理里面的元素,最后输出后缀。此实现如下所示:

public void write(JSONSerializer serializer, Object object) throws IOException {
        SerializeWriter out = serializer.getWrier();
        Collection<?> collection = (Collection<?>) object;
        out.append('[');
        boolean first = true;
        for (Object item : collection) {
            if (!first) {out.append(',');}
            first = false;
 
            Class<?> clazz = item.getClass();
            //Integer.class和Long.class特殊处理
            serializer.write(item);
        }
        out.append(']');
    }

 

    以上代码就是通常最常见的实现了。

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

public void write(JSONSerializer serializer, Object object) throws IOException {
        SerializeWriter out = serializer.getWrier();
        Map<?, ?> map = (Map<?, ?>) object;
        out.write('{');//前缀
 
        Class<?> preClazz = null;//缓存前一个value类型和相对应的解析器,减少类型判断解析
        ObjectSerializer preWriter = null;
 
        boolean first = true;
        for (Map.Entry<?, ?> entry : map.entrySet()) {
//此处有删除,即根据nameFilter和valueFilter针对key-value作转换处理
            if (!first) {out.write(',');}//输出间隔符
 
            serializer.writeFieldName(key);//输出字段名+冒号
            first = false;
 
            Class<?> clazz = value.getClass();
            if (clazz == preClazz) {//此处即细节优化内容,直接使用前一个解析器,避免再次从jsonSerializer中查找
                preWriter.write(serializer, value);
            } else {
/** 此处则就需要从jsonSerializer中查找解析器,并输出了 */
                preClazz = clazz;
                preWriter = serializer.getObjectWriter(clazz);
                preWriter.write(serializer, value);
            }
        }
        out.write('}');//后缀
    }

 

    由上可以看出,map的实现还是按照我们常用的思路在进行。在性能优化处,采取了两个优化点,一是输出字段时,并不直接输出字段名,而是采取字段+冒号的方式一起输出,减少outWriter扩容计算。二是在查找value解析器时,尽量使用前一个value的解析器,避免重复查找。
    相比map,javaBean的实现就相对更复杂。javaBean输出并不是采取key-value的方式,而是采取类似fieldSerializer的输出方式,即将属性名和值,组合成一个字段,一起进行输出。当然输出时还是先输出字段名,再输出值的实现。那么对于javaBean实现,首先要取得当前对象类型的所有可以输出的类型。
    在fastjson实现中,并没有采取javaBean属性的读取方式,而是采取了使用getXXX和isXXX方法的读取模式,来取得一个类型的可读取属性。作为性能优化的一部分,读取属性的操作结果被缓存到了getter缓存器中。其实,并不是缓存到了getter缓存器中,只是该类型的javaBean序列化器对象被缓存到了jsonSerializer的对象类型-序列化器映射中。对于同一个类型,就不需要再次解析该类型的属性了。
    有了相对应的字段,那么在实现时,就按照相应的字段进行输出即可。以下为实现代码:

public void write(JSONSerializer serializer, Object object) throws IOException {
        SerializeWriter out = serializer.getWrier();
            out.append('{');//前缀
 
            for (int i = 0; i < getters.length; ++i) {
                FieldSerializer getter = getters[i];//取属性解析器
                Object propertyValue = getter.getPropertyValue(object);//取值
//省略中间nameFilter和valueFilter过滤处理
                if (commaFlag) {out.append(',');}//间隔符
//省略nameFilter和valueFilter过滤之后的输出处理
               getter.writeProperty(serializer, propertyValue);//使用字段解析器输出内容
            }
            out.append('}');//后缀
    }

 

    由上可见,javaBean的输出实际上和map输出差不多。只不过这里又把属性的解析和输出封装了一层。在使用字段解析器(由FieldSerializer标识)输出字段值时,实际上也是先输出字段名+冒号,再输出字段值。这里就不再详细叙述。

    总结

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

 

0 请登录后投票
   发表时间:2011-06-20   最后修改:2011-06-20

最后,感谢温少,能够在百忙之中,对内容进行审核,待还有未发现之处,还请提出,随时修改:)

0 请登录后投票
   发表时间:2011-06-20   最后修改:2011-06-20
写得比较辛苦,支持一个。

一点建议:
1. 把性能优化和普通代码结构优化能区分对待
2. 性能优化章节中能突出下性能优化的一些特殊技巧 (比较关心int,char的一些特殊处理)

从你文中得到的一点信息:
1. 全程使用buffer替换string字符串拼接操作,使用ThreadLocal进行回收重用
2. 使用Integer,Long里的stringSize()计算数字类型的字符串长度
3. 使用String,Long,Integer等的getChars()方法,直接输出数据到buf上,避免出现字符拼装
4. 扩容一次性计算
5. javaBean的反序列化通过jsonSerializer缓存解析完成的method (使用多例,可以减少从map的查找过程)
6. identityHashMap使用

不知咋回事,fastjson代码我一直连不上去,代码下不下来阿
0 请登录后投票
   发表时间:2011-06-20  
Fly_m 写道

最后,感谢温少,能够在百忙之中,对内容进行审核,待还有未发现之处,还请提出,随时修改:)

 

写的很好,很深入,而且分析的过程中,还帮我发现了一些小问题。 

0 请登录后投票
   发表时间:2011-06-20  
agapple 写道
写得比较辛苦,支持一个。

一点建议:
1. 把性能优化和普通代码结构优化能区分对待
2. 性能优化章节中能突出下性能优化的一些特殊技巧 (比较关心int,char的一些特殊处理)

从你文中得到的一点信息:
1. 全程使用buffer替换string字符串拼接操作,使用ThreadLocal进行回收重用
2. 使用Integer,Long里的stringSize()计算数字类型的字符串长度
3. 使用String,Long,Integer等的getChars()方法,直接输出数据到buf上,避免出现字符拼装
4. 扩容一次性计算
5. javaBean的反序列化通过jsonSerializer缓存解析完成的method (使用多例,可以减少从map的查找过程)
6. identityHashMap使用

不知咋回事,fastjson代码我一直连不上去,代码下不下来阿


发现网络有一些问题,如果连不上,多尝试几次就好了,如果一直不好,请联系我。
0 请登录后投票
   发表时间:2011-06-20  
y>z,z<x
<script>alert('a');</script>
0 请登录后投票
   发表时间:2011-06-20  
刚听说fastjson,请问性能方面和jackson相比如何?
另外,是否支持流输出?用作服务器时,支持流输出,在性能以及减少内存碎片方面还是很有实用价值的.
0 请登录后投票
   发表时间:2011-06-20  
JE帐号 写道
刚听说fastjson,请问性能方面和jackson相比如何?
另外,是否支持流输出?用作服务器时,支持流输出,在性能以及减少内存碎片方面还是很有实用价值的.


支持的,比如:

public static final void write(Writer out, Object object) {
    SerializeWriter writer = new SerializeWriter();
    try {
        JSONSerializer serializer = new JSONSerializer(writer);
        serializer.write(object);
        writer.writeTo(out);
    } catch (IOException ex) {
        throw new JSONException(ex.getMessage(), ex);
    } finally {
        writer.close();
    }
}
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics