序列化和反序列化,看这一篇就够了!

Java 领域的对象如何传输

基于 socket 进行对象传输

先举个简单的例子,写一个socket通信的代码
User

public class User {
    
    

    private String name;
    private int age;

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public int getAge() {
    
    
        return age;
    }

    public void setAge(int age) {
    
    
        this.age = age;
    }

    @Override
    public String toString() {
    
    
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

SocketServerProvider

public class Server {
    
    



    public static void main(String[] args) throws IOException, ClassNotFoundException {
    
    
        ServerSocket serverSocket=null;

        serverSocket=new ServerSocket(8080);

        Socket socket=serverSocket.accept(); //建立好连接

        ObjectInputStream objectInputStream=
                new ObjectInputStream(socket.getInputStream());

        User user=(User)objectInputStream.readObject();

        System.out.println(user);

    }
}

SocketClientConsumer

public class Client {
    
    

    public static void main(String[] args) throws IOException {
    
    
        Socket socket=null;

        socket=new Socket("localhost",8080);

        User user=new User();
        user.setAge(18);
        user.setName("leon");

        ObjectOutputStream out=new ObjectOutputStream
                (socket.getOutputStream());
        out.writeObject(user);

        socket.close();


    }
}

运行结果
➢ 这段代码运行以后,能够实现Java对象的正常传输吗? 很显然,会报错
1
如何解决报错的问题呢?
对User这个对象实现一个Serializable接口,再次运行就可以看到对象能够正常传输了

public class User implements Serializable {
    
    

    private String name;
    private int age;


    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public int getAge() {
    
    
        return age;
    }

    public void setAge(int age) {
    
    
        this.age = age;
    }

    @Override
    public String toString() {
    
    
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

了解序列化的意义
我们发现对User这个类增加一个Serializable,就可以解决Java对象的网络传输问题。这就 是今天想给大家讲解的序列化这块的意义

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行 时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实 应用中,就可能要求在 JVM 停止运行之后能够保存(持久化)指定的对象,并在将来重新读取 被保存的对象。Java对象序列化就能够帮助我们实现该功能

简单来说
序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列 的过程称为对象的序列化 反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程 成为对象的反序列化

序列化的高阶认识
简单认识一下 Java 原生序列化

前面的代码中演示了,如何通过JDK提供了Java对象的序列化方式实现对象序列化传输,主 要通过输出流java.io.ObjectOutputStream和对象输入流java.io.ObjectInputStream来实现。

java.io.ObjectOutputStream:表示对象输出流 , 它的writeObject(Object obj)方法可以对参 数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 java.io.ObjectInputStream:表示对象输入流 ,它的readObject()方法源输入流中读取字节序 列,再把它们反序列化成为一个对象,并将其返回

需要注意的是,被序列化的对象需要实现java.io.Serializable接口

序列化的高阶认识

serialVersionUID 的作用
在IDEA中通过如下设置可以生成serializeid
2
字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识 符的静态变量

我们先写一个例子来说明版本号的作用
首先,定义一个序列化接口,包含序列化,反序列化方法

public interface ISerializer {
    
    

    <T> byte[] serialize(T obj);


    <T> T deserialize(byte[] data,Class<T> clazz);
}

再编写一个将对象序列化到文件和反序列化的实现类

public class JavaSerializerWithFile implements ISerializer{
    
    


    @Override
    public <T> byte[] serialize(T obj) {
    
    
        try {
    
    
            ObjectOutputStream outputStream=
                    new ObjectOutputStream(new FileOutputStream
                            (new File("user")));

            outputStream.writeObject(obj);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return new byte[0];
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
    
    
        try {
    
    
            ObjectInputStream objectInputStream=
                    new ObjectInputStream(new FileInputStream
                            (new File("user")));
            return (T) objectInputStream.readObject();

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }
}

运行Main方法
对象写入文件

public class JavaSerializerMain {
    
    

    public static void main(String[] args) {
    
    
        ISerializer iSerializer = new JavaSerializerWithFile();
        User user = new User();
        user.setAge(18);
        user.setName("leon");
        iSerializer.serialize(user);
    }
}

2
1.上图看到,我们已把User对象写入到文件中
2.然后修改user对象,增加serialVersionUID字段

public class User implements Serializable {
    
    


    private static final long serialVersionUID = 1899348156368054587L;
    private String name;
    private int age;   
}

3.然后通过反序列化来把对象提取出来

public class JavaSerializerMain {
    
    

    public static void main(String[] args) {
    
    
        ISerializer iSerializer = new JavaSerializerWithFile();
//        User user = new User();
//        user.setAge(18);
//        user.setName("leon");
//        iSerializer.serialize(user);
        User deserialize = iSerializer.deserialize(null, User.class);
        System.out.println(deserialize.toString());

    }
}

3
4. 运行结果:提示无法反序列化

结论
Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化 时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进 行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的 异常,即是InvalidCastException。

从结果可以看出,文件流中的 class 和 classpath 中的 class,也就是修改过后的 class,不兼 容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。从错误结果来看,如果没有为 指定的class配置serialVersionUID,那么java编译器会自动给这个class进行一个摘要算法, 类似于指纹算法,只要这个文件有任何改动,得到的UID就会截然不同的,可以保证在这么 多类中,这个编号是唯一的。所以,由于没有显指定 serialVersionUID,编译器又为我们生成了一个 UID,当然和前面保存在文件中的那个不会一样了,于是就出现了 2 个序列化版本号 不一致的错误。因此,只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一 个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法或者属性可以用。

tips: serialVersionUID 有两种显示的生成方式

一是默认的 1L ,比如: private static final long serialVersionUID = 1L; 二是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段 当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序 列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作 序 列 化 版 本 比 较 用 ,这 种 情 况 下,如果 Class 文件 ( 类名,方法明等 ) 没有发生变化 ( 增加空格,换行,增加注释等等 ) ,就算 再编译多次, serialVersionUID 也不会变化的

Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变 量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

绕开 transient 机制的办法

虽然name被transient修饰,但是通过我们写的这两个方法依然能够使得name字段正确 被序列化和反序列化

public class User implements Serializable {
    
    


    private static final long serialVersionUID = 1899348156368054587L;
    private String name;
    private int age;


    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
    
    
        s.defaultWriteObject();

        s.writeObject(name);
    }

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
    
    
        s.defaultReadObject();
        name=(String)s.readObject();
    }


    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public int getAge() {
    
    
        return age;
    }

    public void setAge(int age) {
    
    
        this.age = age;
    }

    @Override
    public String toString() {
    
    
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

writeObject 和 readObject 原理
writeObject 和 readObject 是两个私有的方法,他们是什么时候被调用的呢?从运行结果来 看,它确实被调用。而且他们并不存在于Java.lang.Object,也没有在Serializable中去声明。 我们唯一的猜想应该还是和 ObjectInputStream 和 ObjectOutputStream 有关系,所以基于 这个入口去看看在哪个地方有调用

4
从源码层面来分析可以看到,readObject是通过反射来调用的。 其实我们可以在很多地方看到readObject和writeObject的使用,比如HashMap。

Java 序列化的一些简单总结
  1. Java序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
  2. 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
  3. 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进 行序列化(实现深度克隆)
  4. 当某个字段被申明为transient后,默认的序列化机制会忽略这个字段
  5. 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和 readObject

分布式架构下常见序列化技术

初步了解了Java序列化的知识以后,我们又得回到分布式架构中,了解序列化的发展过程

了解序列化的发展

随着分布式架构、微服务架构的普及。服务与服务之间的通信成了最基本的需求。这个时候, 我们不仅需要考虑通信的性能,也需要考虑到语言多元化问题
所以,对于序列化来说,如何去提升序列化性能以及解决跨语言问题,就成了一个重点考虑 的问题。
由于Java本身提供的序列化机制存在两个问题

  1. 序列化的数据比较大,传输效率低
  2. 其他语言无法识别和对接

以至于在后来的很长一段时间,基于XML格式编码的对象序列化机制成为了主流,一方面解 决了多语言兼容问题,另一方面比二进制的序列化方式更容易理解。以至于基于XML的SOAP 协议及对应的WebService框架在很长一段时间内成为各个主流开发语言的必备的技术。 再到后来,基于 JSON 的简单文本格式编码的 HTTP REST 接口又基本上取代了复杂的 Web Service 接口,成为分布式架构中远程通信的首要选择。但是 JSON 序列化存储占用的空间 大、性能低等问题,同时移动客户端应用需要更高效的传输数据来提升用户体验。在这种情 况下与语言无关并且高效的二进制编码协议就成为了大家追求的热点技术之一。首先诞生的 一个开源的二进制序列化框架-MessagePack。它比 google 的 Protocol Buffers 出现得还要早。

简单了解各种序列化技术
XML 序列化框架介绍

XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大, 而且效率不高,适用于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景, 同时XML又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟 知的Webservice,就是采用XML格式对数据进行序列化的。XML序列化/反序列化的实现方 式有很多,熟知的方式有XStream和Java自带的XML序列化和反序列化两种

我们写个demo说明一下

引入xsteam包

 <dependency>
      <groupId>com.thoughtworks.xstream</groupId>
      <artifactId>xstream</artifactId>
      <version>1.4.10</version>
    </dependency>
public class XStreamSerializer implements ISerializer{
    
    

    XStream xStream=new XStream(new DomDriver());

    @Override
    public <T> byte[] serialize(T obj) {
    
    

        return xStream.toXML(obj).getBytes();
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
    
    
        return (T)xStream.fromXML(new String(data));
    }
}

运行一下

public class SerialDemo {
    
    

    public static void main(String[] args) {
    
    
        ISerializer iSerializer=new XStreamSerializer();
        User user=new User();// JVM内存中.  如何把内存中的对象进行网络传输.(实体)->字节序列
        user.setAge(18);
        user.setName("leon");
        byte[] bytes=iSerializer.serialize(user);
        System.out.println("byte.length:"+bytes.length);
        /**
         * xmlstream: byte.leng 223
        */
        System.out.println(new String(bytes));// byte长度 223
        User user1=iSerializer.deserialize(bytes,User.class);
        System.out.println(user1);
    }
}

运行结果
5

JSON 序列化框架

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JSON 的字节流更小,而且可读性也非常好。现在JSON数据格式在企业运用是最普遍的

  1. Jackson (https://github.com/FasterXML/jackson)
  2. 阿里开源的FastJson (https://github.com/alibaba/fastjon)
  3. Google的GSON (https://github.com/google/gson) 这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,但是 Jackson、 GSON的稳定性要比Fastjson好。而fastjson的优势在于提供的api非常容易使用

我们再来实现一下json的序列化

引入fastjson包

  <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.41</version>
    </dependency>
public class FastJsonSeriliazer implements ISerializer{
    
    
    @Override
    public <T> byte[] serialize(T obj) {
    
    

        return JSON.toJSONString(obj).getBytes();
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
    
    
        return (T)JSON.parseObject(new String(data),clazz);
    }
}

运行一下

public class SerialDemo {
    
    

    public static void main(String[] args) {
    
    

        ISerializer iSerializer=new FastJsonSeriliazer();
        User user=new User();// JVM内存中.  如何把内存中的对象进行网络传输.(实体)->字节序列
        user.setAge(18);
        user.setName("leon");
        byte[] bytes=iSerializer.serialize(user);
        System.out.println("byte.length:"+bytes.length);
        /**
         * json: byte.leng 24
         */
        System.out.println(new String(bytes));// byte长度 24
        User user1=iSerializer.deserialize(bytes,User.class);
        System.out.println(user1);
    }
}

在这里插入图片描述
可以看到,相比xml的传输字节,json的字节流更小,这就意味着服务通信中更小的开销,和性能的提升。

Hessian 序列化框架

Hessian是一个支持跨语言传输的二进制序列化协议,相对于Java默认的序列化机制来说, Hessian具有更好的性能和易用性,而且支持多种不同的语言
实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构, 性能更高

 <dependency>
      <groupId>com.caucho</groupId>
      <artifactId>hessian</artifactId>
      <version>4.0.38</version>
    </dependency>
public class HessianSerializer implements ISerializer{
    
    
    @Override
    public <T> byte[] serialize(T obj) {
    
    
        ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
        HessianOutput hessianOutput=new HessianOutput(outputStream);
        try {
    
    
            hessianOutput.writeObject(obj);
            return outputStream.toByteArray();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

        return new byte[0];
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clazz) {
    
    
        ByteArrayInputStream inputStream=new ByteArrayInputStream(data);
        HessianInput hessianInput=new HessianInput(inputStream);
        try {
    
    
            return (T)hessianInput.readObject();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }
}
public class SerialDemo {
    
    

    public static void main(String[] args) {
    
    
        ISerializer iSerializer=new HessianSerializer();
        User user=new User();// JVM内存中.  如何把内存中的对象进行网络传输.(实体)->字节序列
        user.setAge(18);
        user.setName("leon");
        //原生实现
        byte[] bytes=iSerializer.serialize(user);
        System.out.println("byte.length:"+bytes.length);
        /**
         * hessian: byte.length: 51
         */
        System.out.println(new String(bytes));// byte长度 51
        User user1=iSerializer.deserialize(bytes,User.class);
        System.out.println(user1);
    }
}

在来运行一下看结果
6

Avro 序列化

Avro是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持 二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro提供的机制使动 态语言可以方便地处理Avro数据。

kyro 序列化框架

Kryo是一种非常成熟的序列化实现,已经在Hive、Storm)中使用得比较广泛,不过它不能 跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的 hessian2

Protobuf 序列化框架

Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。Google提供了多种 语言来实现,比如Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件, Protobuf是一个纯粹的表示层协议,可以和各种传输层协议一起使用。 Protobuf使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要 求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应 用在对象的持久化场景中 但是要使用Protobuf会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要 用到的话必须要去投入成本在这个技术的学习中

protobuf有个缺点就是要传输的每一个类的结构都要生成对应的proto文件,如果某个类发 生修改,还得重新生成该类对应的proto文件

序列化技术的选型

技术层面

  1. 序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能
  2. 序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间
  3. 序列化协议是否支持跨平台,跨语言。因为现在的架构更加灵活,如果存在异构系统通信 需求,那么这个是必须要考虑的
  4. 可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新, 这就要求我们采用的序列化协议基于良好的可扩展性/兼容性,比如在现有的序列化数据结 构中新增一个业务字段,不会影响到现有的服务
  5. 技术的流行程度,越流行的技术意味着使用的公司多,那么很多坑都已经淌过并且得到了
    解决,技术解决方案也相对成熟
  6. 学习难度和易用性

选型建议

  1. 对性能要求不高的场景,可以采用基于XML的SOAP协议
  2. 对性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro都可以。
  3. 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读 性都很不错
  4. Avro设计理念偏于动态类型语言,那么这类的场景使用Avro是可以的

各个序列化技术的性能比较这个地址有针对不同序列化技术进行性能比较:https://github.com/eishay/jvmserializers/wiki

猜你喜欢

转载自blog.csdn.net/weixin_38087538/article/details/107981050