该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2011-06-20
最后修改:2011-06-20
fastjson官方地址: http://code.alibabatech.com/wiki/display/FastJSON/Home 总体分析,首先上图,即fastjson的总体处理思想,其实也是所有json序列化器需要考虑的问题。
在这里,需要考虑的主要有两个部分,一是临时保存在序列化过程中用于储存数据的容器,二是处理对象序列化的序列化器。
代码分析部分 首先,将一个对象序列化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的方法(输出和追加)作一个预览:
方法总共可以分五个部分,第一个部分是针对writer基本功能一个扩展,即支持输出int,字符,以及字符数组,追加字符数组(包括字符串)等;第二个部分提供了写整形和长整形的基本方法;第三个部分是提供写基本数据类型数组的支持;第四个部分是提供写一个数字+一个字符的形式,比如数据+[,\],}]这种格式;第五个部分是提供写数据串,主是是针对字符串追加双引号或单引号(以支持标准json)。
五个部分的方法,每个部分都有其特殊的作用和意义,针对最常用的数字和字符串作了特别的对待。 在实现上面,SerializeWriter使用了一个内部的字符数组作为数据的储存器,同时使用了一个计数器计算当前存储的字符量。既然使用了字符数组,那么肯定有相关的操作,如字符扩容等。整个写数据的过程,其实就是往这个字符数组追加数据的过程,需要考虑只是如何追加数据的问题,即上面所列出的这么多些方法。在最终写完数据之后,即可将这个字符数组转为我们所需要的字符串了。 对象序列化入口 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之所以产生的原因。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间: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中;二是如何保证处理过程中的速度。 首先,类的声明,继承了Writer类,实现了输出字符的基本功能,并且提供了拼接数据的基本功能。内部使用了一个buf数组和count来进行计数。这个类的实现结果和StringBuilder的工作模式差不多。但我们说为什么不使用StringBuilder,主要是因为StringBuilder没有针对json序列化提出更加有效率的处理方式,而且单就StringBuilder而言,内部是为了实现字符串拼接而生,因为很自然地使用了更加能够读懂的方式进行处理。相比,serializeWriter单处理json序列化数据传输,功能单一,因此在某些方面更加优化一些。
在初始构造时,会从当前线程变量中取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的处理模式的,在以下的分析中会说到。 总体分类 实现思想 数据输出最后都变成了拼接字符的功能,即将各种类型的数据转化为字符数组的形式,然后将字符数组拼接到buf数组当中。这中间主要逻辑如下: 实现解析 基本功能
其中第一个函数,可以忽略,可以理解为实现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方法。另外几个方法处理逻辑与此方法相同。 整形和长整形 方法如下:
这两个方法,按照我们的逻辑,首先需要将整性和长整性转化为字符串(无特殊字符),然后以字符数组的形式输出即可。在进行处理时,主要参考了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),这里是安全的,因为没有特殊字符。 基本类型数组
数组的形式,主要是将数组的每一部分输出出来,即可。在输出时,需要输出前缀“[”和后缀“]”以及每个数据之间的“,“。按照我们的逻辑,首先还是计算长度,其次是准备空间,再者是写数据,最后是定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方法进行处理。
//计算长度,直接取值,不需要进行计算 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数据扩容的情况出现。尽管这种情况很少发生,但还是尽量避免。特殊是在输出集合数据的情况下,在集合数据输出下,各个数据的长度未定,因此不能计算出总输出长度,只能一个对象一个对象输出,在这种情况下,先要输出一个对象,然后再输出对象的间隔符或结尾符。如果先调用输出数据,再调用输出间隔符或结尾符,远不如将两者结合起来,一起进行计算和输出。
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,也要求必须追加双(单)引号以示区分了。字符串处理方法有以下几种:
其中第1,2方法表示分别用双引号和单引号将字符串包装起来,第3,4方法表示在字符串输出完毕之后,再输出一个冒号,第5方法表示输出一个字符串数组,使用双引号包装字符串。第7,8方法未知(不明真相的方法?)
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),而不再是分开处理。 下一篇,从数据处理过程对源码进行分析,同时解析其中针对性能优化的处理部分。
|
|
返回顶楼 | |
发表时间: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所需要的序列信息。并考虑其中的性能优化。
取得解析器 对于第1,2,3,4类型,在fastjson中使用了一个全局的单态实例来保存相对应的解析器;第5类型,处理集合类型,对于集合类型及其,由于其处理逻辑均是一样,所以只需要针对子类作一些的处理,让其返回相对应的集合类型解析器即可;第6类型,时间处理器,将时间转化为类似yyyy-MM-ddTHH:mm:ss.SSS的格式;第7类型,处理fastjson专有jsonAwre类型;第8类型,处理对象的数组形式,即处理数组时,需要考虑数组中的统一对象类型;第9,即处理我们最常使用的对象,javaBean类型,这也是在项目中解析得最多的类型。 我们要看一下相对应的取解析器的方法,即类JsonSerializer.getObjectWriter(Class<?> clazz)方法,参考其中的实现:
首先,取对象解析器是由一个类型为JSONSerializerMap的对象mapping中取。之所以使用此类型,这是性能优化的一部分,此类型并不是一个完整的map类型,只是实现了一个类似map的操作的类型。其内部并没有使用基于equals的比较方式,而是使用了System.identityHashCode的实现。对于一个对象,其identityHashCode的值是定值,而对于一个类型,在整个jvm中只有一个,因此这里使用基于class的identityHashCode是可以了,也避免了使用class的euqlas来进行比较。 解析过程
即首先,取得输出时所需要的outWriter,然后再根据配置决定是输出单引号+字符串还是双引号+字符串。 而对于其它并没有由outWriter所直接支持的类型而言,解析过程就相对于复杂一些。但是总的逻辑还是基于以下逻辑: 只需此三步,即可完美的解析一个对象。因此,我们可以参考其中的一个具体实现,并讨论其中的具体优化措施。
以下实现与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的实现还是按照我们常用的思路在进行。在性能优化处,采取了两个优化点,一是输出字段时,并不直接输出字段名,而是采取字段+冒号的方式一起输出,减少outWriter扩容计算。二是在查找value解析器时,尽量使用前一个value的解析器,避免重复查找。
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中对于数据的处理上。此处更多的是设计模式的使用,以实现繁多的对象的解析。
|
|
返回顶楼 | |
发表时间:2011-06-20
最后修改:2011-06-20
最后,感谢温少,能够在百忙之中,对内容进行审核,待还有未发现之处,还请提出,随时修改:) |
|
返回顶楼 | |
发表时间: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代码我一直连不上去,代码下不下来阿 |
|
返回顶楼 | |
发表时间:2011-06-20
Fly_m 写道
最后,感谢温少,能够在百忙之中,对内容进行审核,待还有未发现之处,还请提出,随时修改:)
写的很好,很深入,而且分析的过程中,还帮我发现了一些小问题。 |
|
返回顶楼 | |
发表时间: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代码我一直连不上去,代码下不下来阿 发现网络有一些问题,如果连不上,多尝试几次就好了,如果一直不好,请联系我。 |
|
返回顶楼 | |
发表时间:2011-06-20
y>z,z<x
<script>alert('a');</script> |
|
返回顶楼 | |
发表时间:2011-06-20
刚听说fastjson,请问性能方面和jackson相比如何?
另外,是否支持流输出?用作服务器时,支持流输出,在性能以及减少内存碎片方面还是很有实用价值的. |
|
返回顶楼 | |
发表时间: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(); } } |
|
返回顶楼 | |