Skip to content

自定义序列化的最佳实践

ray.zh edited this page Jun 6, 2024 · 15 revisions

1. 快速使用

使用@MagicConverter注解对需要自定义序列化的类或成员属性进行标记,即可以在类或类属性上使用该注解。 该注解需要配置一个转换器类,转换器类需要实现MConverter接口。 接下来,框架在进行序列化/反序列化时将会使用配置的转换器进行装包和拆包。 该注解有三个成员属性:

  • converter,该属性用于传入转换器类
  • attachParams, 该属性为自定义参数,框架在调用MConverter中的方法时会携带该参数
  • fixSize, 自定义序列化数据的字节长度,可不设置此处,然后在调用序列化方法时返回对应长度
  • handleCollection,是否自定义序列化整个集合。true时需要控制整个集合的序列化过程,false则只需要控制单个元素的序列化过程
1.1 自定义序列化属性
/**
 * 自定义一个枚举转换器
 */
public class CustomSexEnumSerialize implements MConverter<SexEnum> {
    // 定义为数组, 固定枚举顺序
    private SexEnum[] sexEnums = new SexEnum[] {SexEnum.MAN, SexEnum.WOMAN, SexEnum.UNKNOWN};

    /**
     * 封包,将字节数据转为对象
     * @param nextReadIndex 起始位置,即在此之前的都已读取
     * @param fullBytes 完整字节数据
     * @param attachParams 附加参数
     * @param clz 正在pack操作的类
     * @param fieldObj正在pack操作的对象
     * @param rootObj正在pack操作的根对象, rootObj 和 fieldObj 在进行嵌套结构pack操作时将会不同
     * @return 返回总共读取的字节数和封装好的对象
     */
    @Override
    public MResult<SexEnum> pack(int nextReadIndex, byte[] fullBytes, String attachParams, Class clz, Object fieldObj, Object rootObj) {
        byte index = fullBytes[4];

        // 注意,如果没有在注解中申明`fixSize`, 这里必须要返回读取的字节数;
        // 框架序列化接下来的属性时将会跳过这些字节
        return MResult.build(
                1,
                sexEnums[index]
        );
    }

    /**
     * 拆包,将对象拆包为字节数组
     * @param sexEnum
     * @param attachParams 附加参数
     * @param rootObj正在unpack操作的根对象
     * @return
     */
    @Override
    public byte[] unpack(SexEnum sexEnum, String attachParams, Object rootObj) {
        for (int i = 0; i < sexEnums.length; i++) {
            if(sexEnum == sexEnums[i]) {
                return new byte[]{(byte)i};
            }
        }
        return null;
    }
}

/**
 * 实体类中即可使用上面定义的转换器
 */
@MagicClass()
public class CustomConverterInField {
    @MagicField(order = 1, size = 5)
    private String name;
    @MagicField(order = 2)
    @MagicConverter(converter = CustomSexEnumSerialize.class)
    private SexEnum sex;
    @MagicField(order = 3)
    private int age;
}
1.2 自定义序列化类

这个例子自定义序列化一个自定义类

/**
 * 自定义一个转换器
 */
public class CustomBookConverter implements MConverter<Book> {
    @Override
    public MResult<Book> pack(int nextReadIndex, byte[] fullBytes, String attachParams, Class clz, Object fieldObj, Object rootObj) {
        if(attachParams.equals("1")) {
            // ignore

        }  else if(attachParams.equals("2")) {
           // ignore
        }else if(attachParams.equals("3")) {
           // 注意 attachParams参数
            Book2 book2 = new Book2();
            book2.setCreateDate(new Date());
            book2.setId(24);
          // 注意返回值中需要返回读取的字节数
            return MResult.build(0,book2);
        }
        return MResult.build(0, null);
    }

    @Override
    public byte[] unpack(Book object, String attachParams, Object rootObj) {
        // 序列化过程, 省略了
        return new byte[0];
    }
}

/**
 * 定义实体类1
 */
@MagicClass
public class Book2 {
    private int id;
    private Date createDate;

    // ignore getter/stter....
}

/**
 * 定义实体类2
 * 这里对于 Book2,我们使用自定义的转换器来执行
 */
@MagicClass
public class Staff10 {
    @MagicField(order = 1)
    private int id;
    // 注意这里 attachParams  参数
    @MagicConverter(converter = CustomBookConverter.class, attachParams = "3")
    @MagicField(order = 3)
    private Book2 book;
    @MagicField(order = 4, size = 4)
    private String name;

   // ignore getter/stter....
}
1.3 自定义序列化未知类型

有些时候,数据报文设计时可能存在一个动态的类型属性,该属性决定了接来下的数据解析方式(即该属性决定了接下来序列化的java类)。

这种情况下也可以直接使用自定义序列化,将成员类型直接设置为Object即可。

例如还是上面的示例(修改了attachParam和成员类型为Object):

/**
 * 自定义一个转换器
 */
public class CustomBookConverter implements MConverter<Object > {
    @Override
    public MResult<Object > pack(int nextReadIndex, byte[] fullBytes, String attachParams, Class clz, Object fieldObj, Object rootObj) {
        if(attachParams.equals("1")) {
            // ignore
        }  else if(attachParams.equals("2")) {
           // ignore
        }  else if(attachParams.equals("3")) {
           // ignore
        }  else if(attachParams.equals("4")) {
           // 注意 attachParams参数修改为4, 而且这里可以返回任意对象
            Map res = new HashMap();
          // 注意返回值中需要返回读取的字节数
            return MResult.build(0,res );
        }
        return MResult.build(0, null);
    }

    @Override
    public byte[] unpack(Object object, String attachParams, Object rootObj) {
        // 序列化过程中, 需要判断object类型再进行序列化; 这里省略了
        return new byte[0];
    }
}


/**
 * 定义实体类1
 */
@MagicClass
public class Book2 {
    private int id;
    private Date createDate;

    // ignore getter/stter....
}

/**
 * 定义实体类2
 * 这里对于 Book2,我们使用自定义的转换器来执行
 */
@MagicClass
public class Staff10 {
    @MagicField(order = 1)
    private int id;
    // 注意这里 attachParams  参数修改为 4 了,上一个示例为 3; 另外这个属性对象为Object,这样在自定义解析中就可以返回任意对象了
    @MagicConverter(converter = CustomBookConverter.class, attachParams = "4")
    @MagicField(order = 3)
    private Object book;
    @MagicField(order = 4, size = 4)
    private String name;

   // ignore getter/stter....
}
1.4 自定义序列化集合handleCollection参数解释

框架也支持控制集合类的序列化,但有两种方式。 第一种也就是handleCollection=false(默认), 此方式下开发者只需要实现集合类中的单个元素序列化流程即可(默认),框架根据集合大小来循环调用从而完成整个序列化流程。 第二种handleCollection=true, 此种方式需要需要开发者控制整个集合的序列化流程,因为没有集合大小的配置,所以集合边界,长度都由开发者来控制。

2. 注意事项

自定义序列化虽然灵活,但需要注意以下几点:

  • 序列化或者反序列化时使用字节数一定要一致,因为框架需要这些字节数来确定下一个字段数据读取的位置 举个例子,一个对象在自定义序列化时序列化了5个字节,而在反序列化时只读了3个字节,那么读取下一个属性的数据时将会从第4个字节处开始读取。
  • 推荐使用fixSize属性标明自定义序列化对象的字节数,没有数据时框架自动置零。此处需要注意:框架不能处理null的情况,此种情况反序列化后将是一个空实例。
  • 未使用fixSize属性,则必须在反序列化时返回已处理的字节数。
  • 注解标记后,该类将会全局使用申明的转换器进行转换,即使是基础类型例如String, Integer之类的。
  • 一定要保证序列化/反序列化的字节数一致,这是最重要的。
  • array或者list序列化时,每次处理的应是单个对象的数据。框架是根据list.size调用相应次数的序列化方法来完成数据的处理
  • com.github.misterchangray.core.customconverter.CustomConverterTest中有很多测试用例可以参考