[alibaba/fastjson]MixInAnnotations特性及测试用例

2024-08-28 913 views
6

实现了类似jackson中的MixInAnnotations功能 参考链接:https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations

提供四个新API:

JSON.addMixInAnnotations(Target.class, MixIn.class); //新增Target类与MixIn类的关联关系,连续add多次只取最后一次生效
JSON.removeMixInAnnotations(Target.class); //移除Target类与MixIn类的关联
JSON.getMixInAnnotations(Target.class); //获取与Target类关联的MixIn类
JSON.clearMixInAnnotations(); //清除所有Target类及其MixIn类的对应关系

使用示例: 现在有一个Java类Rectangle(Target Class),定义如下

public final class Rectangle {
    final private int w, h;
    public Rectangle(int w, int h) {
      this.w = w;
      this.h = h;
    }
    public int getW() { return w; }
    public int getH() { return h; }
    public int getSize() { return w * h; }
}

其对应的MixIn类(Mix-In Class),定义如下:

abstract class MixIn {
  MixIn(@JSONField(name="width") int w, @JSONField(name="height") int h) { }

  @JSONField(name="width") abstract int getW(); // rename property
  @JSONField(name="height") abstract int getH(); // rename property
  @JSONField(serialize=false) abstract int getSize(); // we don't need it!

}

API使用:

//配置MixInAnnotations
JSON.addMixInAnnotations(Rectangle.class, MixIn.class); 

// 序列化
Rectangle rectangle = new Rectangle(5, 10);
String str = JSON.toJSONString(rectangle);

System.out.println(str); // {"width":5, "height":10}

//反序列化
Rectangle rectangle2 = JSON.parseObject(str, Rectangle.class);

System.out.println(rectangle2.getW()); // 5
System.out.println(rectangle2.getH()); // 10

//移除MixInAnnotations
JSON.removeMixInAnnotations(Rectangle.class);

大致思路如下:

  1. 在JSON类中维护一个全局Map对象,用于存储Target类 -> MixIn类的映射关系,并提供相应的增删改查方法。
  2. 重载TypeUtils.getAnnotations()方法,使其除了有获取Class对象的注解功能之外,额外新增了对属性和方法的注解获取功能。
  3. 修改TypeUtils.getAnnotations()中的逻辑,使其每次在获取类、对象、方法的注解时都会检查是否存在对应的Mixin注解,如果有的话,优先使用MixIn类的注解。(这个过程中会递归对Target类和MixIn的超类中对应的信息做匹配,以支持类继承结构)
  4. 将与序列化/反序列化相关函数中获取注解的行为统一到TypeUtils.getAnnotations()上(不再是直接通过field.getAnnotations()这种形式)
  5. 每次添加Target类与MixIn类的关联时,检查IdentityHashMap中是否缓存了Target类对应的Serializer/Deserializer,如果有的话,删除(避免直接从缓存中取值而导致MixIn类对beaninfo的修改不生效)。 由于每个类对应的Serializer和Deserializer会缓存在SerializeConfig和DeserializeConfig中,所以如果这个类被序列化过后再添加mixIn类的话会不起作用(因为下次序列化会直接用上一次缓存起来的序列化器)。为了解决这个问题,在SerializeConfig和DeserializeConfig两个类中分别新建了一个新的IdentityHashMap,用于存储带mixIn的序列化器和反序列化器。

一句话总结就是:“在Target类的类、方法、属性获取注解时先从MixIn类中获取,将对应信息写入Target类的序列化/反序列化器中,并与无MixIn的类对象的序列化/反序列化器分开缓存,分开获取。”

性能测试(测试代码在最后):

为了公平起见,测试分为三个对照组:

  1. 当前的Master分支(无MixIn特性相关代码,直接使用注解)
  2. MixIn开发分支(不使用MixIn功能,直接使用注解)
  3. MixIn开发分支(使用MixIn功能,不直接使用注解)

为了减少偶然导致的误差,每项测试都进行了20次,并取去掉最大值和最小值后的平均值为最终结果。(保留两位小数)

按照fastjson的特性,序列化与反序列化耗时的部分集中在创建类的序列化器和反序列化器上,而这个行为只发生在这个类的对象首次序列化/反序列化时,因为之后可以直接调用前一次缓存下来的序列化器/反序列化器。因此,性能测试以测量首次单次序列化/反序列化的结果为主。 测试过程中通过取样打印进行正确性的验证(保证使用MixIn与直接使用注解效果相同,虽然附带的测试用例已经测过了,但保险起见,这里我也做了一些抽样测试)。

测试结果(单位为毫秒):
序列化:
分支/调用次数 Master分支(注解) MixIn分支(注解) MixIn分支(MixIn)
1次 239.63 239.88 215.5
100次 241.50 248.13 210.63
10000次 375.75 370.75 344.00
1000000次 1282.25 1343.25 1293.5
反序列化:
分支/调用次数 Master分支(注解) MixIn分支(注解) MixIn分支(MixIn)
1次 25.50 24.24 27.00
100次 42.88 44.25 44.00
10000次 263.75 267.25 268.38
1000000次 1442.63 1470.63 1515.88

顺便测试了一下带继承结构的性能(只取首次时间):

分支/操作 Master分支(注解) MixIn分支(注解) MixIn分支(MixIn)
序列化 266.56 253.90 216.89
反序列化 27.56 24.80 25.44
结论:

经过测试可以发现添加MixIn功能后,数据与原版本数据相比,在正负范围内浮动程度正常,对性能无影响

测试环境

机型:Microsoft Surface Pro 4 内存:4G 处理器:Intel Core i5-6300U 2.40GHz 2.50GHz IDE:IntelliJ IDEA Community Edition 2019.2 jdk:jdk8u221

测试代码

(通过注释相应的注解代码来切换对照组,通过修改times变量的值修改调用次数):

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PerformanceTest {

    public static class BigClass {
        //@JSONField(name="boolean")
        boolean aBoolean;
        //@JSONField(name="byte")
        byte aByte;
        //@JSONField(name="short")
        short aShort;
        //@JSONField(name="int")
        int anInt;
        //@JSONField(name="char")
        char aChar;
        //@JSONField(name="long")
        long aLong;
        //@JSONField(name="float")
        float aFloat;
        //@JSONField(name="double")
        double aDouble;

        public boolean getaBoolean() {
            return aBoolean;
        }

        public void setaBoolean(boolean aBoolean) {
            this.aBoolean = aBoolean;
        }

        public byte getaByte() {
            return aByte;
        }

        public void setaByte(byte aByte) {
            this.aByte = aByte;
        }

        public short getaShort() {
            return aShort;
        }

        public void setaShort(short aShort) {
            this.aShort = aShort;
        }

        public int getAnInt() {
            return anInt;
        }

        public void setAnInt(int anInt) {
            this.anInt = anInt;
        }

        public char getaChar() {
            return aChar;
        }

        public void setaChar(char aChar) {
            this.aChar = aChar;
        }

        public long getaLong() {
            return aLong;
        }

        public void setaLong(long aLong) {
            this.aLong = aLong;
        }

        public float getaFloat() {
            return aFloat;
        }

        public void setaFloat(float aFloat) {
            this.aFloat = aFloat;
        }

        public double getaDouble() {
            return aDouble;
        }

        public void setaDouble(double aDouble) {
            this.aDouble = aDouble;
        }
    }

    abstract class BigClassMixIn {
        @JSONField(name="boolean")
        private boolean aBoolean;
        @JSONField(name="byte")
        private byte aByte;
        @JSONField(name="short")
        private short aShort;
        @JSONField(name="int")
        private int anInt;
        @JSONField(name="char")
        private char aChar;
        @JSONField(name="long")
        private long aLong;
        @JSONField(name="float")
        private float aFloat;
        @JSONField(name="double")
        private double aDouble;
    }

    public static class BigClassSub extends BigClass {
        //@JSONField(name="string")
        String string;
        //@JSONField(name="list")
        List list;
        //@JSONField(name="map")
        Map map;
        //@JSONField(name="array")
        int[] array;

        public String getString() {
            return string;
        }

        public void setString(String string) {
            this.string = string;
        }

        public List getList() {
            return list;
        }

        public void setList(List list) {
            this.list = list;
        }

        public Map getMap() {
            return map;
        }

        public void setMap(Map map) {
            this.map = map;
        }

        public int[] getArray() {
            return array;
        }

        public void setArray(int[] array) {
            this.array = array;
        }
    }

    abstract class BigClassSubMixIn extends BigClassMixIn {
        @JSONField(name="string") abstract boolean getString();
        @JSONField(name="list") abstract boolean getList();
        @JSONField(name="map") abstract boolean getMap();
        @JSONField(name="array") abstract boolean getArray();
    }

    public static void main(String[] args) {
        long serializeTime = 0;
        long deserializeTime = 0;
        int times = 1;

        JSON.addMixInAnnotations(BigClass.class, BigClassMixIn.class);
        for (int i = 0; i < times; i++) {
            BigClass bigClass = new BigClass();
//            BigClassSub bigClass = new BigClassSub();
            bigClass.aBoolean = i%2==0? true: false;
            bigClass.aByte = (byte)i;
            bigClass.aChar = (char)i;
            bigClass.anInt = i;
            bigClass.aShort = (short)i;
            bigClass.aLong = i;
            bigClass.aFloat = i;
            bigClass.aDouble = i;
//            bigClass.string = "";
//            bigClass.list = new ArrayList();
//            bigClass.map = new HashMap();
//            bigClass.array = new int[8];

            long start = System.currentTimeMillis();
            String jsonString = JSON.toJSONString(bigClass);
            long mid = System.currentTimeMillis();
            BigClass bigClass2 = JSON.parseObject(jsonString, BigClass.class);
            long end = System.currentTimeMillis();

            serializeTime += (mid - start);
            deserializeTime += (end - mid);
        }
        JSON.removeMixInAnnotations(BigClass.class);

        System.out.println("序列化:" + (serializeTime));
        System.out.println("反序列化:" + deserializeTime);
    }
}

回答

1

看了下你的pr,可以解决默认情况下使用全局Config的场景。我觉得如果要使用new出来的Config应该也有解决方法,可以想一想然后一起提

7

用identityhashmap修复了remove功能,顺带解决了Config实例使用mixin的问题,代码已提

7

现在用的方式是用JSON类的静态方法进行全局配置,之前有考虑像jackson那样通过config来配置,如方案1:https://github.com/Omega-Ariston/fastjson/issues/1 但是后来实现的时候发现这样做改动太大了 0-0 jackson那边的MixInHandler思路我看过,不太好移植到这。 如果想实现单次配置也不难,通过add和remove就可以完成。

5

这个功能用在什么场景,解决了什么问题呢?

7

意思就是注解可以不用写在实体上面?不错的想法,项目中碰到过第三方类写不了注解,用这种应该可以吧

9

嗯,但是我看JSON类中用来存mixin类的mixInMapper用的是作者自己实现的IdentityHashMap,如果有多个线程同时对它进行put操作可能会有操作丢失的问题,导致MixIn配置失效。

4

有道理...已经把JSON.mixInMapper改成ConcurrentHashMap实现

9

主要是在做JavaBean对象和注解的解耦。注解可以不用直接写在JavaBean上,转而写在额外配置的MixIn类上。这么做的好处之一是可以灵活地通过切换Target类与MixIn类的关联关系来切换不同的注解应用于不同场景,好处之二是可以为一些只读的第三方JavaBean类进行注解的配置。