在《使用Objenesis实例化java对象》中我们提到了创建一个对象的示例的场景中,有这么一条是将序列化之后的对象重新反序列化为Java对象,那么本篇将就在这一点上,介绍说明下使用Protostuff对Java对象进行序列化和反序列化操作首先。

引子

关于为何要序列化、如何序列化可以参看下https://www.cnblogs.com/wugongzi/p/14345859.html,这里不做额外说明,需要了解到的是,我们一般提到的java对象序列化的方案大致可分为如下几种

  • Java自身提供的序列化功能,需要实现Serializable接口或者Externalizable接口,并使用java的IOStream进行序列化和反序列化的操作
  • 使用第三方库进行JSON或者XML格式的序列化,XML序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的 字节码文件比较大,而且效率不高,适应于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景。相对于XML来说,JSON的字节流较小,而且可读性也非常好,应用也非常普遍的。常用的有Jackson、阿里的Fastjson、谷歌的Gson等
  • 除此之外,Protobuf也是一个非常广泛应用的选择,Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。Protobu解析性能比较高,序列化以后数据量相对较少,适合应用在对象的持久化场景中。但是不同于前面提到的几个方法,要使用 Protobuf 会相对略麻烦些,他有自己的语法,自己的编译器

和Protobuf类似、Protostuff也是谷歌的产品,它是基于Protobuf发展而来的,相对于Protobuf提供了更多的功能和更简易的用法。其中,protostuff-runtime实现了无需预编译对Java Bean进行protobuf序列化/反序列化的能力。protostuff-runtime的局限是序列化前需预先传入schema,反序列化不负责对象的创建只负责复制,因此必须提供默认构造函数。在性能上,Protostuff不输原生的Protobuf,甚至有反超之势

演示代码

我们直接贴一段已经写好了的,自己封装的SerializeUtil工具类来看下,首先是添加相关依赖

<dependency>
    <groupId>io.protostuff</groupId>
    <artifactId>protostuff-core</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>io.protostuff</groupId>
    <artifactId>protostuff-runtime</artifactId>
    <version>1.7.1</version>
</dependency>

以及SerializeUtil类的具体代码,注意这里用到了前一篇《使用Objenesis实例化java对象》中提到的Objenesis类来创建一个空的java对象,在deserialize方法中我们用他来构造一个新的空对象,ObjenesisStd的构造函数默认使用缓存来构造。


/**
 * @author CheungQ
 */
public class SerializeUtil {
    private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap();
    private static Objenesis objenesis = new ObjenesisStd(true);

    public SerializeUtil() {
    }

    public static <T> Schema<T> getSchema(Class<T> cls) {
        if (cachedSchema.get(cls) == null) {
            Schema<T> schema = RuntimeSchema.createFrom(cls);
            cachedSchema.putIfAbsent(cls, schema);
        }
        return (Schema)cachedSchema.get(cls);
    }

    public static <T> byte[] serialize(T obj) {
        Class<T> cls = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate();

        byte[] bytes;
        try {
            Schema<T> schema = getSchema(cls);
            bytes = ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
        return bytes;
    }

    public static <T> T deserialize(byte[] data, Class<T> cls) {
        try {
            T message = objenesis.newInstance(cls);
            Schema<T> schema = getSchema(cls);
            ProtostuffIOUtil.mergeFrom(data, message, schema);
            return message;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

当然。如果你不想用Objenesis,也可以使用

ProtostuffIOUtil.mergeFrom(data, schema.newMessage(), schema);

的方式来构造新对象,这里的newMessage方法底层还是使用的newInstance的方式来构造对象的。这里就是需要注意的,上面提到过的反序列化不负责对象的创建只负责复制

序列化测试

还是拿之前在《常用Java查看对象所占内存方法》一问中创建的对象类来做测试,顺便我们可以用这篇文章中的方法来测试下最终序列化之后对象所占内存的对比情况。

StudentInfo studentInfo = new StudentInfo();
studentInfo.setName("CheungQ");
studentInfo.setAge(16);

RegionInfo regionInfoProvince = new RegionInfo();
regionInfoProvince.setName("江苏");
regionInfoProvince.setRegionCode(321);
regionInfoProvince.setAbbreviation("苏");
RegionInfo regionInfoCity = new RegionInfo();
regionInfoCity.setName("南京");
regionInfoCity.setRegionCode(21);
regionInfoCity.setAbbreviation("宁");
RegionInfo regionInfoDistrict = new RegionInfo();
regionInfoDistrict.setName("玄武区");
regionInfoDistrict.setRegionCode(3821);
Address address = new Address();
address.setProvince(regionInfoProvince);
address.setCity(regionInfoCity);
address.setDistrict(regionInfoDistrict);
studentInfo.setAddress(address);

EducationInformation education1 = new EducationInformation();
education1.setSchoolName("NEET家里蹲大学");
education1.setStartTime(new Date());
education1.setEndTime(new Date());
education1.setLearningStatus(LearningStatusEnum.GRADUATED);
EducationInformation[] information = new EducationInformation[]{education1};
EducationalExperience educationalExperience = new EducationalExperience();
educationalExperience.setEducationInformationData(information);
studentInfo.setEducationalExperience(educationalExperience);

byte[] arr = SerializeUtil.serialize(studentInfo);
System.out.println("GraphLayout:"+GraphLayout.parseInstance(arr).toPrintable());
System.out.println("Lucence get size:"+RamUsageEstimator.sizeOf(arr));
StudentInfo studentInfo2 = SerializeUtil.deserialize(arr,StudentInfo.class);

最终我们可以看到序列化之后得到的是一个长度为113的byte[]数组

占用内存136字节

以及我们把原对象、Protostuff序列化、以及FastJson序列化之后的结果做下对比看下

String str = JSON.toJSONString(studentInfo);
System.out.println("Lucence Object studentInfo:"+RamUsageEstimator.sizeOf(studentInfo));
System.out.println("Lucence byte[] arr:"+RamUsageEstimator.sizeOf(arr));
System.out.println("Lucence JSON string:"+RamUsageEstimator.sizeOf(str));

可以看到这明显的大小差异

IOStream的方式序列化操作有点麻烦,我们可以另外写一个序列化的方法,同时需要把现有的所有对象都加下implements Serializable

@SneakyThrows
public static void ioStreamSerializeToFile(Serializable obj){
    FileOutputStream fileOutputStream = new FileOutputStream("obj");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(obj);
    objectOutputStream.flush();
    objectOutputStream.close();
}

最终可以看到序列化后的文件如图

可以在IDEA的Plugins中添加BinEd – Binary / Hexadecimal Editor插件查看

直接内存中序列化也可以如下

@SneakyThrows
public static byte[] ioStreamSerialize(Serializable obj) {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    objectOutputStream.writeObject(obj);
    return byteArrayOutputStream.toByteArray();
}

@SneakyThrows
public static Object ioStreamDeserialize(byte[] bytes) {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    return objectInputStream.readObject();
}

对结果占用内存情况查看下,可以得到,IOStream序列化之后的结果