Fastjson2你开始使用了吗?来看看源码解析

概述

FastJson2FastJson项目的重要升级,目标是为下一个十年提供一个高性能的JSON库。根据官方给出的性能来看,相比v1版本,确实有了很大的提升,本篇文章我们来看下究竟做了哪些事情,使得性能有了大幅度的提升。

本篇将采用代码测试 + 源码阅读的方式对FastJson2的性能提升做一个较为全面的探索。

一、环境准备

首先,我们搭建一套用于测试的环境,这里采用springboot项目,分别创建两个module:fastjsonfastjson2。使用两个版本进行对比试验。

代码结构如下所示:

1.1 引入对应依赖

在父pom当中引入一些我们需要使用的公共依赖,这里为了简便,使用了


    org.projectlombok
    lombok
    1.18.24

复制代码

在fastjson当中引入fastjson的依赖:


    com.alibaba
    fastjson
    1.2.79

复制代码

在fastjson2当中引入fastjson2的依赖:


    com.alibaba.fastjson2
    fastjson2
    2.0.8

复制代码

1.2 创建测试类

这里为了方便,直接使用main方法进行测试。

  • 创建类:Student.java

    import lombok.Builder;
    import lombok.Data;
    
    @Data
    @Builder
    public class Student {
        private String name;
        private Integer age;
        private String address;
    
        public Student(String name, Integer age, String address) {
            this.name = name;
            this.age = age;
            this.address = address;
        }
    }
    复制代码
  • 创建测试main方法:

    /**
    * 定义循环次数
    */
    private final static Integer NUM = 100;
    
    public static void main(String[] args) {
       // 总时间
       long totalTime = 0L;
       //初始化学生数据
       List studentList = new ArrayList<>();
       // 10w学生
       for (int i = 0; i < 100000; i++) {
           studentList.add(Student.builder().name("我犟不过你").age(10).address("黑龙江省哈尔滨市南方区哈尔滨大街267号").build());
       }
       // 按指定次数循环
       for (int i = 0; i < NUM; i++) {
           // 单次循环开始时间
           long startTime = System.currentTimeMillis();
           // 遍历学生数据
           studentList.forEach(student -> {
               // 序列化
               String s = JSONObject.toJSONString(student);
               //字符串转回java对象
               JSONObject.parseObject(s, Student.class);
           });
           // 将学生list序列化,之后转为jsonArray
           JSONArray jsonArray = JSONArray.parseArray(JSONObject.toJSONString(studentList));
           // 将jsonArray转java对象list
           jsonArray.toJavaList(Student.class);
           //单次处理时间
           long endTime = System.currentTimeMillis();
           // 单次耗时
           totalTime += (endTime - startTime);
           System.out.println("单次耗费时间:" + (endTime - startTime) + "ms");
       }
       System.out.println("平均耗费时间:" + totalTime / NUM + "ms");
    }
    复制代码

    上述代码在fastjson和fastjson2的测试中基本相同,唯一不同在于在fastjson2当中,jsonArray.toJavaList方法转变成了jsonArray.toList

二、性能测试

本节将使用上面的代码进行测试。在此之前,我们首先需要针对两个子工程设置相同的堆空间大小128M,以免造成偏差:

2.1 第一次测试

下面正是开始测试:

  • fastjson结果

    单次耗费时间:863ms
    单次耗费时间:444ms
    单次耗费时间:424ms
    单次耗费时间:399ms
    单次耗费时间:384ms
    单次耗费时间:355ms
    单次耗费时间:353ms
    单次耗费时间:363ms
    ... ...
    单次耗费时间:361ms
    单次耗费时间:356ms
    单次耗费时间:355ms
    单次耗费时间:357ms
    单次耗费时间:351ms
    单次耗费时间:354ms
    平均耗费时间:366ms
    复制代码

    如上所示,除了第一次很慢,第二次变快,到最后基本稳定在360毫秒左右,最终的平均耗时是366ms

  • fastjson2结果

    单次耗费时间:957ms
    单次耗费时间:803ms
    单次耗费时间:468ms
    单次耗费时间:435ms
    单次耗费时间:622ms
    单次耗费时间:409ms
    单次耗费时间:430ms
    ··· ···
    单次耗费时间:400ms
    单次耗费时间:641ms
    单次耗费时间:403ms
    单次耗费时间:398ms
    单次耗费时间:431ms
    单次耗费时间:356ms
    单次耗费时间:362ms
    单次耗费时间:626ms
    单次耗费时间:404ms
    单次耗费时间:395ms
    平均耗费时间:478ms
    复制代码

    如上所示,首次执行慢,逐步变快,但是后面就出现问题了,怎么执行的时间这么不稳定?跨度从390多到640多?这是怎么回事?平均时间也达到了478ms,反而比fastjson还要慢。

2.4 第二次试验

我们似乎得到了一个结论,但是如何确定是fastjson2的那个方法消耗更多的内存空间呢?毕竟我们在测试方法中,调用了很多的方法。

所以我们进一步调小内存,看看是否会有内存溢出呢?

我们将内存调整为64M:

-Xms64m -Xmx64m
复制代码

运行后发现果然出现了内存溢出,并且明确的指出是堆空间内存溢出:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at com.alibaba.fastjson2.JSONReader.read(JSONReader.java:1274)
	at com.alibaba.fastjson2.JSON.parseArray(JSON.java:1494)
	at com.alibaba.fastjson2.JSONArray.parseArray(JSONArray.java:1391)
	at com.wjbgn.fastjson2.test.TestFastJson2.main(TestFastJson2.java:43)
复制代码

2.5 第三次实验

下面我们将内存增大,看看是否能够提升fastjson2的性能。将堆空间大小调整为256M

  • fastjson

    单次耗费时间:805ms
    单次耗费时间:224ms
    单次耗费时间:235ms
    单次耗费时间:228ms
    单次耗费时间:222ms
    ... ...
    单次耗费时间:191ms
    单次耗费时间:196ms
    单次耗费时间:193ms
    单次耗费时间:194ms
    单次耗费时间:192ms
    平均耗费时间:198ms
    复制代码

    如上所示,发现随着堆空间增加,fastjson1有较大的性能提升,平均时长在198ms

  • fastjson2

    单次耗费时间:671ms
    单次耗费时间:496ms
    单次耗费时间:412ms
    单次耗费时间:405ms
    单次耗费时间:315ms
    单次耗费时间:321ms
    ... ...
    单次耗费时间:337ms
    单次耗费时间:326ms
    平均耗费时间:335ms
    复制代码

    如上所示,结果在335毫秒,随着内存增加,性能有提升,但是仍然没有fastjson1快。

通过如上的实验,我们似乎可以得到如下的结论:在数据量较大时,fastjson的性能还要好于fastjson2!

2.6 第四次试验

本次测试我们要给足够大堆空间,看看这两者的性能表现,此处将堆空间设置成1g

-Xms1g -Xmx1g
复制代码
  • fastjson

    单次耗费时间:943ms
    单次耗费时间:252ms
    单次耗费时间:156ms
    单次耗费时间:155ms
    ... ...
    单次耗费时间:119ms
    单次耗费时间:114ms
    单次耗费时间:108ms
    单次耗费时间:133ms
    单次耗费时间:115ms
    平均耗费时间:133ms
    复制代码

    如上所示,在足够大的内存条件下,fastjson的平均时间达到了133ms

  • fastjson2

    单次耗费时间:705ms
    单次耗费时间:199ms
    单次耗费时间:172ms
    ... ...
    单次耗费时间:101ms
    单次耗费时间:124ms
    单次耗费时间:96ms
    平均耗费时间:119ms
    复制代码

    如上所示,fastjson2处理速度首次高于fastjson。

2.7 小结

通过前面的测试,我们能够得到如下的结论:

  • fastjson2相比fastjson确实是有性能提升,但是取决于堆内存的大小。

  • 堆空间小的情况下,fastjson的性能表现优于fastjson2。

  • 在适当的情况先,对jvm进行调优,是对应用程序的性能有影响的

  • 我们需要知道,堆空间并非越大越好,空间越大代表着GC处理时间会越长,其表现为应用响应时间的增加。

三、源码分析

本节将通过阅读源码的方式简单了解fastjson2的原理,主要分为两个方面进行阅读:

  • writer
  • reader

为什么通过这两个方面?

fastjson的核心就是将java对象序列化成json(对应writer),以及将json反序列化成java对象(对应reader)。而且其内部正是通过这样的命名方式去实现的。

3.1 序列化 writer

toJSONString方法

其实所谓的序列化,就是JSONObject.toJSONString的体现,所以我们通过跟踪其源码去发现其原理,注意我写注释的位置。

/**
 * Serialize Java Object to JSON {@link String} with specified {@link JSONReader.Feature}s enabled
 *
 * @param object   Java Object to be serialized into JSON {@link String}
 * @param features features to be enabled in serialization
 */
static String toJSONString(Object object, JSONWriter.Feature... features) {
    // 初始化 【ObjectWriterProvider】 ,关注【JSONFactory.defaultObjectWriterProvider】
    JSONWriter.Context writeContext = new JSONWriter.Context(JSONFactory.defaultObjectWriterProvider, features);

    boolean pretty = (writeContext.features & JSONWriter.Feature.PrettyFormat.mask) != 0;
    // 初始化jsonwriter,ObjectWriter会将json数据写入jsonwriter
    JSONWriterUTF16 jsonWriter = JDKUtils.JVM_VERSION == 8 ? new JSONWriterUTF16JDK8(writeContext) : new JSONWriterUTF16(writeContext);

    try (JSONWriter writer = pretty ?
            new JSONWriterPretty(jsonWriter) : jsonWriter) {
        if (object == null) {
            writer.writeNull();
        } else {
            writer.setRootObject(object);
            Class valueClass = object.getClass();

            boolean fieldBased = (writeContext.features & JSONWriter.Feature.FieldBased.mask) != 0;
            // 获取ObjectWriter
            ObjectWriter objectWriter = writeContext.provider.getObjectWriter(valueClass, valueClass, fieldBased);
            // ObjectWriter将数据写入JSONWriter
            objectWriter.write(writer, object, null, null, 0);
        }
        return writer.toString();
    }
}
复制代码

defaultObjectWriterProvider对象

查看JSONFactory.defaultObjectWriterProvider的内容:

public ObjectWriterProvider() {
    init();
    // 初始化【ObjectWriterCreator】,用来创建【ObjectWriterProvider】
    ObjectWriterCreator creator = null;
    switch (JSONFactory.CREATOR) {
        case "reflect": //反射
            creator = ObjectWriterCreator.INSTANCE;
            break;
        case "lambda": // lambda
            creator = ObjectWriterCreatorLambda.INSTANCE;
            break;
        case "asm":
        default:
            try {//asm
                creator = ObjectWriterCreatorASM.INSTANCE;
            } catch (Throwable ignored) {
                // ignored
            }
            if (creator == null) {
                creator = ObjectWriterCreatorLambda.INSTANCE;
            }
            break;
    }
    this.creator = creator;
}
复制代码

如上所示,我们看到此处初始化了ObjectWriterCreator,其实现方式默认是基于ASM的动态字节码实现。

另外还提供了 反射lambda 的方式。

到此为止已经获取到了ObjectWriterProvider,它的作用是用来获取ObjectWriter的。

getObjectWriter方法

ObjectWriter的作用就是将java对象写入到json当中,所以我们下面开始关注这一行代码的实现:

writeContext.provider.getObjectWriter(valueClass, valueClass, fieldBased);
复制代码

继续查看getObjectWriter方法,查看关键位置代码:

if (objectWriter == null) {
    // 获取creator,此处获取的是方法开始时默认的【ObjectWriterCreatorASM】
    ObjectWriterCreator creator = getCreator();
    if (objectClass == null) {
        objectClass = TypeUtils.getMapping(objectType);
    }
    // 此处创建ObjectWriter,内部创建【FieldWriter】
    objectWriter = creator.createObjectWriter(
            objectClass,
            fieldBased ? JSONWriter.Feature.FieldBased.mask : 0,
            modules
    );
    ObjectWriter previous = fieldBased
            ? cacheFieldBased.putIfAbsent(objectType, objectWriter)
            : cache.putIfAbsent(objectType, objectWriter);

    if (previous != null) {
        objectWriter = previous;
    }
}
复制代码

createObjectWriter方法

查看creator.createObjectWriter伪代码:

// 遍历java对象当中的getter方法,获取属性名
BeanUtils.getters(objectClass, method -> {
    ... ...
String fieldName;
if (fieldInfo.fieldName == null || fieldInfo.fieldName.isEmpty()) {
    if (record) {
        fieldName = method.getName();
    } else {
        // 根据getter获取到属性名称
        fieldName = BeanUtils.getterName(method.getName(), beanInfo.namingStrategy);
    }
} else {
    fieldName = fieldInfo.fieldName;
}
    ... ...
复制代码

在上面的getterName方法获取到对象的属性名,找到属性后,创建对应的【FieldWriter】:

//创建该属性的fieldWriter
FieldWriter fieldWriter = createFieldWriter(
        objectClass,
        fieldName,
        fieldInfo.ordinal,
        fieldInfo.features,
        fieldInfo.format,
        fieldInfo.label,
        method,
        writeUsingWriter
);

// 将属性名作为key,fieldWriter作为value放入缓存【fieldWriterMap】
FieldWriter origin = fieldWriterMap.putIfAbsent(fieldName, fieldWriter);
复制代码

循环过所有的getter方法后,会得到一个全部属性的List fieldWriters集合:

fieldWriters = new ArrayList<>(fieldWriterMap.values());
复制代码

再往后,fastjson2会组装一个动态类:【ObjectWriter_1】,在里面组装能够写入JSONWriter的各种属性和方法,以及get属性获取:

定义和初始化此对象的方法如下所示:

//定义【ObjectWriter_1】的属性
genFields(fieldWriters, cw);

// 定义【ObjectWriter_1】的方法
genMethodInit(fieldWriters, cw, classNameType);
//定义【ObjectWriter_1】获取对象属性的读取方法
genGetFieldReader(
        fieldWriters,
        cw,
        classNameType,
        new ObjectWriterAdapter(objectClass, null, null, features, fieldWriters)
);
复制代码

此动态对象的末尾【1】是随数量增长的。

继续向下跟踪到如下方法:

genMethodWrite(objectClass, fieldWriters, cw, classNameType, writerFeatures);
复制代码

此方法主要的作用是创建【ObjectWrite_1】的write方法,并匹配当前java对象的属性属于哪种类型,使用哪种FieldWriter进行写入。

其内部会轮询所有的属性进行匹配,我们的属性主要是StringInteger,如下:

... ...
 else if (fieldClass == Integer.class) {
     // 处理Integer属性
    gwInt32(mwc, fieldWriter, OBJECT, i);
} else if (fieldClass == String.class) {
    // 处理String属性
    gwFieldValueString(mwc, fieldWriter, OBJECT, i);
}
... ...

复制代码
  • Integer 在内部处理时,会在动态对象生成名称是writeInt32的方法。

  • String 内部处理时在动态对象生成方法writeString

再向下会通过以下方法修改写入不同类型属性的方法名称和描述信息等

genMethodWriteArrayMapping("writeArrayMapping", objectClass, writerFeatures, fieldWriters, cw, classNameType);
复制代码

能够看到,Integer和String的后续处理方法不同:

  • String
        else if (fieldClass == String.class) {
            methodName = "writeString";
            methodDesc = "(Ljava/lang/String;)V";
        } 
    复制代码
  • Integer 则是对象"(Ljava/lang/Object;)V"

到此整个ObjectWriter_1对象就设置完成了,使用反射进行创建:

try {
    Constructor constructor = deserClass.getConstructor(Class.class, String.class, String.class, long.class, List.class);
    return (ObjectWriter) constructor.newInstance(objectClass, beanInfo.typeKey, beanInfo.typeName, writerFeatures, fieldWriters);
} catch (Throwable e) {
    throw new JSONException("create objectWriter error, objectType " + objectClass, e);
}
复制代码

回到toJSONString方法

至此我们已经拿到java对象的属性,并成功创建了【ObjectWriter】:

再返回toJSonString方法当中,看看Object的后续操作 拿到的ObjectWriter调用其【write】方法进行数据写入:

objectWriter.write(writer, object, null, null, 0);
复制代码

我们已经知道不同类型属性使用不同的FieldWriter进行写入:

  • String:我们虽然提到过使用的writeString方法,但是你会发现没有对应的FieldWriter,因为它使用的是JSONWriterUTF16JDK8writeString(String str)方法,不同版本的jdk有不同的Class。

  • Integr:使用FieldWriterInt32writeInt32(JSONWriter jsonWriter, int value)进行写入。

关于具体的写入过程就不在介绍了。

本节主要针对主要流程进行梳理,与上图对比存在部分未讲解流程,感兴趣同学参照源码自行阅读。

整个过程较为复杂,简单描述为:使用ASM动态字节码方式作为基础,通过java对象的getter方法获取对象的属性值,构建动态ObjectWriter对象,针对不同的对象属性,生成不同的写入方法,最终通过反射进行对象创建,最后进行java对象数据的写入。

值得一提的是,ObejctWriter对象是会进行缓存的,有助于性能的提升。

3.2 反序列化 reader

下面来看看反序列化reader的流程。因为大体流程与writer差不多,所以以下内容不做详细讲解了。

parseObject 方法

/**
 * json转换java对象
 *
 * @param text  json字符串
 * @param 需要转换的类
 * @return Class
 */
@SuppressWarnings("unchecked")
static  T parseObject(String text, Class clazz) {
    if (text == null || text.isEmpty()) {
        return null;
    }
        //创建reader,内部与writer相同,使用ASM动态字节码形式创建creater
    try (JSONReader reader = JSONReader.of(text)) {
        // 获取上下文
        JSONReader.Context context = reader.context;

        boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
        // 获取ObjectReader
        ObjectReader objectReader = context.provider.getObjectReader(clazz, fieldBased);

        T object = objectReader.readObject(reader, 0);
        if (reader.resolveTasks != null) {
            reader.handleResolveTasks(object);
        }
        return object;
    }
}
复制代码

JSONReader.of方法

创建reader对象,

public static JSONReader of(String str) {
    if (str == null) {
        throw new NullPointerException();
    }
    //创建reader的上下文,内部与writer相同,使用ASM动态字节码形式创建creater,包装成context
    Context context = JSONFactory.createReadContext();
    // jdk8以上版本使用下面的字符串处理方式
    if (JDKUtils.JVM_VERSION > 8 && JDKUtils.UNSAFE_SUPPORT && str.length() > 1024 * 1024) {
        try {
            byte coder = UnsafeUtils.getStringCoder(str);
            if (coder == 0) {
                byte[] bytes = UnsafeUtils.getStringValue(str);
                return new JSONReaderASCII(context, str, bytes, 0, bytes.length);
            }
        } catch (Exception e) {
            throw new JSONException("unsafe get String.coder error");
        }

        return new JSONReaderStr(context, str, 0, str.length());
    }
    // jdk 8 及以下字符串处理
    final int length = str.length();
    char[] chars;
    if (JDKUtils.JVM_VERSION == 8) {
        // jdk8字符串转char
        chars = JDKUtils.getCharArray(str);
    } else {
        chars = str.toCharArray();
    }
    // 创建JSONReaderUTF16对象
    return new JSONReaderUTF16(context, str, chars, 0, length);
}
复制代码

getObjectReader方法

与getObjectWriter类似,获取动态的json数据读取对象。关注重点代码:

if (objectReader == null) {
    // 获取前面创建的creater
    ObjectReaderCreator creator = getCreator();
    // 创建ObjectReader对象,根据java类的类型
    objectReader = creator.createObjectReader(objectClass, objectType, fieldBased, modules);
}
复制代码

createObjectReader方法

关注下面这行代码:

// 创建属性读取对象数组
FieldReader[] fieldReaderArray = createFieldReaders(objectClass, objectType, beanInfo, fieldBased, modules);
复制代码

继续跟进,发现遍历java对象的setter方法,此时我们应该能够想到,向对象设置值的时候,一定是使用的setter方法:

BeanUtils.setters(objectClass, method -> {
    fieldInfo.init();
    // 创建Fieldreader
    createFieldReader(objectClass, objectType, namingStrategy, orders, fieldInfo, method, fieldReaders, modules);
});
复制代码

createFieldReader方法会获取java对象当中的属性,以及set开头的方法。

此对象包含setterFieldReaders,用于向java对象写入数据。

回到parseObject

下面看如何读取json数据到java对象:

object = objectReader.readObject(reader, 0);
复制代码

object内部主要是循环遍历fieldReaders,它内部包含json当中的属性和对象的set方法:

正是通过这些属性和set方法将json的数据放到java对象当中。

首先将对象的属性和值放到map当中:

valueMap.put(fieldReader.getFieldNameHash(), fieldValue);
复制代码

通过下面的方法将map转换成java对象:

T object = createInstanceNoneDefaultConstructor(
        valueMap == null
                ? Collections.emptyMap()
                : valueMap);
复制代码

内部通过构造器和值去创建一个新的java对象:

return (T) constructor.newInstance(args);
复制代码

注意:因为这个原因,在java对象当中必须要有一个相应的带有参数的构造器,否则会报错。

到此为止就成功拿到转换后的java对象了。

小结

感兴趣的同学可以参考上图的内容,结合本文提供的流程,自己跟踪一遍源码。

整个过成简单描述:底层使用ASM动态字节码为基础,通过java对象的setter方法去构建动态的ObjectReader对象,最终通过构造器去创建一个新的java对象

四、总结

关于fastjson2的简单测试,以及源码阅读到此就告一段落了。

针对fastjson2有以下几点总结:

  • fastjson2对于fastjson的兼容,可以使用下面的依赖:

    
        com.alibaba
        fastjson
        2.0.8
    
    复制代码

    但是官方也不保证100%兼容。

  • 内存占用,通过前面的测试,发现fastjson2有明显占用更大内存的现象,甚至在相同内存条件下,fastjson1可以完美执行,而fastjson2有产生内存溢出的风险。


到此为止关于fastjson2的介绍就结束了,感谢大家的观看。

我个人也是摸索着去学习和阅读,对于有些解释可能还存在一些误区和误读,希望爱好阅读源码的朋友们帮忙指点出来。本文仅作为大家阅读源码的参考,希望有更多的fastjson2的源码阅读类文章出现,便于大家一起学习。

你可能感兴趣的:(java,算法,spring,boot)