为什么远程传输对象要序列化?

对象的序列化,在实际的开发过程中,使用的非常频繁,尤其是微服务开发,如果你用的是SpringBoot + Dubbo组合的框架,那么在通过rpc调用的时候,如果传输的对象没有实现序列化,会直接报错!
首页 新闻资讯 行业资讯 为什么远程传输对象要序列化?

01、背景介绍

序列化和反序列化几乎是工程师们每天都需要面对的事情,尤其是当前流行的微服务开发。

光看定义上,对于初学者来说,可能很难一下子理解序列化的意义,尤其是面对这种特别学术词语的时候,内心会不由自主的发问:它到底是啥,用来干嘛的?

如果用通俗的方式来理解,你可以用变魔术的方式来理解它,就好比你想把一件铁器从一个地方运往到另一个地方,在出发的时候,通过魔术方式将这个东西融化成一桶铁水,当到达目的地之后,又通过变魔术的方式,将这桶铁水还原成一件铁器。当铁器变成铁水的过程,可以理解为序列化;从铁水变成铁器,可以理解为反序列化。

站在程序世界的角度看,我们都知道计算机之间传递信息的最小单元是字节流,序列化其实就是将一个对象变成所有的计算机都能识别的字节流;反序列化就是将接受到的字节流还原成一个程序能识别的对象。

简单的说,序列化最终的目的是为了对象可以更方面的进行跨平台存储和进行网络传输。

基本上只要是涉及到跨平台存储或者进行网络传输的数据,都需要进行序列化。

互联网早期的序列化方式主要有COM和CORBA。

COM主要用于Windows平台,并没有真正实现跨平台,另外COM的序列化的原理利用了编译器中虚表,使得其学习成本巨大(想一下这个场景, 工程师需要是简单的序列化协议,但却要先掌握语言编译器)。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。

CORBA是早期比较好的实现了跨平台,跨语言的序列化协议。COBRA的主要问题是参与方过多带来的版本过多,版本之间兼容性较差,以及使用复杂晦涩。这些政治经济,技术实现以及早期设计不成熟的问题,最终导致COBRA的渐渐消亡。J2SE 1.3之后的版本提供了基于CORBA协议的RMI-IIOP技术,这使得Java开发者可以采用纯粹的Java语言进行CORBA的开发。

随着软件技术的快速发展,之后逐渐出现了比较流行的序列化方式,例如:XML、JSON、Protobuf、Thrift 和 Avro等等。

这些序列化方式各有千秋,不能简单的说哪一种序列化方式是最好的,只能从你的当时环境下去选择最适合你的序列化方式,如果你要为你的公司项目进行序列化技术的选型,主要可以从以下几个方面进行考虑:

  • 是否支持跨平台:尤其是多种语言混合开发的项目,是否支持跨平台直接决定了系统开发难度

  • 序列化的速度:速度快的方式会为你的系统性能提升不少

  • 序列化出来的大小:数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能

BB了这么多,作为一名 java 程序员,我们应该如何使用序列化呢,以及序列化的过程中应该需要注意的问题。

下面,我们一起来了解一下!

02、代码实践

java 实现序列化方式非常简单,只需要实现Serializable接口即可,例如下面这个类。

publicclass Student implementsSerializable{/**
     * 用户名
     */private String name;/**
     * 年龄
     */privateIntegerage;publicStudent(String name,Integerage){
        this.name=name;this.age=age;}@OverridepublicString toString(){return"Student1{"+"name='"+name+'\''+", age="+age+'}';}
}

我们来测试一下,将Student对象进行二进制的数据存储后,并从文件中读取数据出来转成Student对象,这个过程其实就是一个序列化和反序列化的过程。

publicclass ObjectMainTest {publicstatic void main(String[]args)throws Exception {//序列化serializeAnimal();//反序列化deserializeAnimal();}

    private static void serializeAnimal()throws Exception {
        Student black=new Student("张三",20);System.out.println(black.toString());System.out.println("=================开始序列化================");ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.log"));oos.writeObject(black);oos.flush();oos.close();}

    private static void deserializeAnimal()throws Exception {
        System.out.println("=================开始反序列化================");ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.log"));Student black=(Student)ois.readObject();ois.close();System.out.println(black.toString());}
}

输出结果:

Student{name='张三',age=20}=================开始序列化=================================开始反序列化================Student{name='张三',age=20}

看起来是不是超级简单,但是请你别大意,这里面的坑还真不少,请看下面的问题汇总!

03、序列化问题汇总

3.1、static 属性不能被序列化

实际在序列化的时候,被static修饰的属性字段是不能被序列化进去的,因为静态变量属于类的状态,序列化并不保存静态变量!

3.2、Transient 属性不会被序列化

被Transient修饰的属性无法被序列化,眼见为实,我们给Student类的name字段加一个transient修饰符。

publicclass Student implementsSerializable{/**
     * 用户名
     */private transient String name;//...省略}

运行测试方法,输出结果如下:

Student{name='张三',age=20}=================开始序列化=================================开始反序列化================Student{name='null',age=20}

很明显,被transient修饰的name属性,反序列化后的结果为null。

3.3、序列化版本号 serialVersionUID 问题

只要是实现了Serializable接口的类都会有一个版本号,如果我们没有定义,JDK 工具会按照我们对象的属性生成一个对应的版本号,当然我们还可以自定义,例如给Student类自定义一个序列化版本号,操作如下。

publicclass Student implementsSerializable{//自定义序列化版本号private static final long serialVersionUID=1l;//...省略}

如何验证这一点呢?

首先,我们先序列化一个Student对象,里面没有自定义版本号,然后在反序列化的时候,我们给这个对象自定义一个版本号,运行测试程序,看能不能反序列化成功?

Exceptioninthread"main"java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student;localclass incompatible: stream classdesc serialVersionUID=821478144412499207,localclass serialVersionUID=1at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)

答案很明显,反序列化失败!

分析原因:Student对象序列化时的版本号是821478144412499207,反序列化时的版本号是1,两者不一致,导致无法反序列化成功!

当我们没有显式的自定义序列化版本号时,JDK 会根据当前对象的属性自动生成一个对象的版本号,只要对象的属性不会发生变化,这个版本号也基本上不会发生变化,但是当对象的属性发生了变化,对应的反序列化对象没有跟着一起变化,大概率会出现反序列化失败!

为了眼见为实,我们继续以实际案例给大家演示一下。

还是以上面那个为主,我们先序列化一个Student对象,里面没有自定义版本号,然后在反序列化操作的时候,我们给Student对象新增一个属性email,同时也不自定义版本号。

publicclass Student implementsSerializable{/**
     * 用户名
     */private String name;/**
     * 年龄
     */privateIntegerage;/**
     * 邮箱
     */private String email;//省略set、get...}

看看运行效果:

Exceptioninthread"main"java.io.InvalidClassException: com.example.java.serializable.test1.entity.Student;localclass incompatible: stream classdesc serialVersionUID=821478144412499207,localclass serialVersionUID=-5996907635197467174at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)

答案很显然,反序列化报错了!两者的版本号不一致!

在平时开发的过程中,实体类的属性难免会发生改动,我们有些同学啊,在写代码的时候只是把序列化的接口实现了,但是没有自定义版本号,在这点上,我强烈建议大家一定要给每个实现了Serializable接口的类,自定义一个版本号,即使对象的属性发生了变化,也不会影响到数据的序列化和反序列化操作!

操作很简单,直接在实体类里面加上这个静态变量即可!

//自定义序列化版本号private static final long serialVersionUID=1l;

3.4、父类、子类序列化问题

在实际的开发过程中,尤其是实体类,为了对象属性的复用,我们往往会采用继承的方式来处理。

使用了继承之后,父类属性是否可以正常被序列化呢?下面我们一起来看看!

  • 父类没有实现序列化,子类实现序列化

首先我们创建两个类Parent和Child,Child继承自Parent。

publicclass Parent {

    private String name;publicString getName(){returnname;}publicParent setName(String name){
        this.name=name;returnthis;}

}
publicclass Child extends Parent implementsSerializable{


    private static final long serialVersionUID=1l;private String id;publicString getId(){returnid;}publicChild setId(String id){
        this.id=id;returnthis;}
    
}

编写测试类,先序列化,然后再反序列化!

publicclass ObjectMainTest {publicstatic void main(String[]args)throws Exception {
        serializeAnimal();deserializeAnimal();}

    private static void serializeAnimal()throws Exception {
        Child black=new Child();black.setId("123");black.setName("张三");System.out.println("id:"+black.getId()+",name:"+black.getName());System.out.println("=================开始序列化================");ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.log"));oos.writeObject(black);oos.flush();oos.close();}

    private static void deserializeAnimal()throws Exception {
        System.out.println("=================开始反序列化================");ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.log"));Child black=(Child)ois.readObject();ois.close();System.out.println("id:"+black.getId()+",name:"+black.getName());}
}

运行结果如下:

id:123,name:张三=================开始序列化=================================开始反序列化================id:123,name:null

结果很明显,父类的属性没有被序列化进去!

我们在来试试,另一种常见

  • 父类实现序列化,子类不实现序列化

publicclass Parent implementsSerializable{

    private static final long serialVersionUID=1L;private String name;publicString getName(){returnname;}publicParent setName(String name){
        this.name=name;returnthis;}

}
publicclass Child extends Parent {

    private String id;publicString getId(){returnid;}publicChild setId(String id){
        this.id=id;returnthis;}

}

接着运行一次程序,结果如下!

id:123,name:张三
=================开始序列化================
=================开始反序列化================
id:123,name:张三

结果很明显,父类的属性被序列化进去!

假如,子类和父类,都实现了序列化,并且序列化版本号都不一样,会不会出现问题呢?

  • 父类实现序列化,子类实现序列化

publicclass Parent implementsSerializable{

    private static final long serialVersionUID=1L;private String name;publicString getName(){returnname;}publicParent setName(String name){
        this.name=name;returnthis;}

}
publicclass Child extends Parent implementsSerializable{

    private static final long serialVersionUID=2l;private String id;publicString getId(){returnid;}publicChild setId(String id){
        this.id=id;returnthis;}

}

运行一次程序,结果如下!

id:123,name:张三=================开始序列化=================================开始反序列化================id:123,name:张三

父类的属性序列化依然成功,当父、子类都实现了序列化,并且定义了不同的版本号,这种情况下,版本号是跟着子类的版本号走的!

总结起来,当父类实现序列化时,子类所有的属性也会全部被序列化;但是当父类没有实现序列化,子类在序列化时,父类属性并不会被序列化!

3.5、自定义序列化过程

Serializable接口内部序列化是 JVM 自动实现的,但是在某些少数的场景下,你可能想自定义序列化和反序列化的内容,但是又不想改实体类属性,这个时候你可以采用自定义序列化的实现方式。

自定义序列化方式,其实也很简单,只需要实现 JDK 自身提供的Externalizable接口就行,里面有两个核心方法,一个是数据写入,另一个是数据的读取。

publicinterface Externalizable extends java.io.Serializable{

    void writeExternal(ObjectOutputout)throws IOException;void readExternal(ObjectInputin)throws IOException,ClassNotFoundException;}

Externalizable接口的实现过程也很简单,我们创建一个Person,实现自Externalizable的两个方法。

publicclass Person implements Externalizable {

    private static final long serialVersionUID=1l;private String name;privateintage;/**
     * 实现了Externalizable这个接口时需要提供无参构造,在反序列化时会检测
     */publicPerson(){
        System.out.println("Person: empty");}publicPerson(String name,intage){
        this.name=name;this.age=age;}@Overridepublicvoid writeExternal(ObjectOutputout)throws IOException {
        System.out.println("person writeExternal...");out.writeObject(name);out.writeInt(age);}@Overridepublicvoid readExternal(ObjectInputin)throws ClassNotFoundException,IOException {
        System.out.println("person readExternal...");name=(String)in.readObject();age=in.readInt();}@OverridepublicString toString(){return"Person{"+"name='"+name+'\''+", age="+age+'}';}
}

测试Person对象的序列化和反序列化。

publicclass ExternalizableMain {publicstatic void main(String[]args)throws IOException,ClassNotFoundException {serializable();deserializable();}

    private static voidserializable()throws IOException {
        Person person=new Person("张三",15);System.out.println(person.toString());System.out.println("=================开始序列化================");FileOutputStream boas=new FileOutputStream("person.log");ObjectOutputStream oos=new ObjectOutputStream(boas);oos.writeObject(person);oos.close();boas.close();}

    private static void deserializable()throws IOException,ClassNotFoundException {
        System.out.println("============反序列化=============");ObjectInputStream bis=new ObjectInputStream(new FileInputStream("person.log"));Person person=(Person)bis.readObject();System.out.println(person.toString());}
}

运行结果如下:

Person{name='张三',age=15}=================开始序列化================person writeExternal...============反序列化=============Person: empty
person readExternal...Person{name='张三',age=15}

04、小结

对象的序列化,在实际的开发过程中,使用的非常频繁,尤其是微服务开发,如果你用的是SpringBoot + Dubbo组合的框架,那么在通过rpc调用的时候,如果传输的对象没有实现序列化,会直接报错!

在使用序列化的时候,坑点还不少,尤其是版本号的问题,这个很容易被忽略,大家在实际开发的时候,强烈推荐自定义版本号,这样可以避免传输的对象属性发生变化的时候,接口反序列化出错的概率!

05、参考

1、https://www.huaweicloud.com/articles/6b6d1d97c0a9155899f0f7354c86610d.html

2、https://zhuanlan.zhihu.com/p/40462507

3、https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html

38    2024-09-03 08:17:59    序列化 Spring Dubbo