序列化、反序列化原理和Protobuf实现机制

目录

1、基本概念

1.1 什么是序列化和反序列化

1.2 为什么需要序列化与反序列化

1.3 序列化算法步骤

2、实现序列化和反序列化

2.1 JDK类库API

2.2 谷歌gson方式

2.3 谷歌ProtoBuf协议组件


1、基本概念

1.1 什么是序列化和反序列化

(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;

 (2)序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。

 (3)反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

 (4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。

1.2 为什么需要序列化与反序列化

我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。

还有,当互联网电商项目并发访问很大的时候,数百万用户产生数百万个session对象,内存可能吃不消,同时用户登录后不一定需要时时刻刻用到该session对象,那么我们的web容器可以将当前并没有使用的session对象序列化到磁盘中,等到需要使用的时候再反序列化为对象!

  那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!

  换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

  当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。

好处一:实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),

好处二:利用序列化实现远程通信,即在网络上传送对象的字节序列。

总的来说可以归结为以下几点:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中; ​

(2)通过序列化以字节流的形式使对象在网络中进行传递和接收; ​

(3)通过序列化在进程间传递对象;

1.3 序列化算法步骤

(1)将对象实例相关的类元数据输出。 ​

(2)递归地输出类的超类描述直到不再有超类。 ​

(3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。 ​

(4)从上至下递归输出实例的数据

2、实现序列化和反序列化

对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException

2.1 JDK类库API

2.1.1 实现方式

(1)java.io.ObjectOutputStream:表示对象输出流;

  它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;

(2)java.io.ObjectInputStream:表示对象输入流;

  它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回;

PS: 只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!

@Test
    public void test() throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("object.txt")));
        Person person = new Person(18,"laohu","men");
        oos.writeObject(person);
        oos.flush();
        oos.close();
        //从文件中读取字节序列,反序列化为对象
        ObjectInputStream ois= new ObjectInputStream(new FileInputStream(new File("object.txt")));
        Object obj =  ois.readObject();
        ois.close();
        System.out.println(obj);
    }

    @Test
    public void test1() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //序列化到对象输出流中,文件类型可以为任何类型
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        Person person = new Person(18,"laohu","men");
        oos.writeObject(person);
        byte[] bytes = baos.toByteArray();
        System.out.println(Arrays.toString(bytes));
        oos.flush();
        oos.close();
        //从文件中读取字节序列,反序列化为对象
        ObjectInputStream ois= new ObjectInputStream(new ByteArrayInputStream(bytes));
        System.out.println(ois.readObject());
        ois.close();
    }
​
    //这个地方先标注下打印出来的字节数组,等下跟protobuf的对比
    /*[-84, -19, 0, 5, 115, 114, 0, 17, 99, 111, 109, 46, 121, 100, 116, 46, 112, 111, 106, 111, 46, 85, 115, 101, 114, 108, 124, -59, -89, -28, 34, 123, -76, 2, 0, 3, 73, 0, 2, 105, 100, 76, 0, 8, 112, 97, 115, 115, 119, 111, 114, 100, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 0, 0, 0, 1, 116, 0, 6, 49, 50, 51, 52, 53, 54, 116, 0, 5, 108, 97, 111, 104, 117]*/

2.1.2 相关注意事项

1、序列化时,只对对象的状态进行保存,而不管对象的方法;

2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

3、在序列化对象时,不仅会序列化当前对象,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推!

@Test
public void test2() throws IOException, ClassNotFoundException {
    //序列化到对象输出流中,文件类型可以为任何类型
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("object.txt")));
    Person person = new Person(18,"laohu","men");
    Role role = new Role("admin");
    person.setRole(role);
    oos.writeObject(person);
    oos.flush();
    oos.close();
    //从文件中读取字节序列,反序列化为对象
    ObjectInputStream ois= new ObjectInputStream(new FileInputStream(new File("object.txt")));
    System.out.println(ois.readObject());
    ois.close();
}

所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。 

4、声明为transient类型的成员数据不能被序列化。因为transient代表对象的临时数据。如果想让该字段再次可以被序列化(选择性序列化),在类中添加两个方法:writeObject()与readObject(),需要手动对成员变量进行序列化!

    private transient String VALUE ="transient";
    // writeObject()会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉VALUE字段。然后再调用writeObject()方法显示地将VALUE字段写入
    // ObjectOutputStream中。
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(VALUE);
    }
​
    // readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        VALUE = (String) in.readObject();
    }
​
    /*必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?
    毫无疑问,使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。这两个方法会在序列化、反序列化的过程中被自动调用。且不能关闭流,否则会导致序列化操作失败*/

5、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;

Java提供了另一个序列化接口 Externalizable,Externalizable 继承于 Serializable,当使用该接口时,序列化的细节需要由程序员去完成,实现的writeExternal()与readExternal()方法对需要序列化的字段处理,如果都不处理,那么这个序列化相当于白干

package com.ydt.pojo;
​
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
​
public class Order implements Externalizable {
​
    private static final long serialVersionUID = 8334617098942116608L;
    private int id;
​
    private String orderName;
​
    private double price;
​
    public Order(int id, String orderName, double price) {
        this.id = id;
        this.orderName = orderName;
        this.price = price;
    }
​
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeObject(orderName);
    }
​
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        id = in.readInt();
        orderName = (String)in.readObject();
    }
​
    public Order() {
        
    }
​
    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", orderName='" + orderName + '\'' +
                ", price=" + price +
                '}';
    }
}
​

6、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

   在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

   在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

    @Test
    public void test() throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        Order order = new Order(1,"充气娃娃",9999.99);
        oos.writeObject(order);
        oos.flush();
        oos.close();
        //从文件中读取字节序列,反序列化为对象
        ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.txt"));
        Object obj =  ois.readObject();
        ois.close();
        System.out.println(obj);
    }   
    //执行一次后将该测试类序列化的代码去掉,只进行反序列化
    @Test
    public void test() throws IOException, ClassNotFoundException {
        //从文件中读取字节序列,反序列化为对象
        ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.txt"));
        Object obj =  ois.readObject();
        ois.close();
        System.out.println(obj);
    }   
    /*此时可以正常的序列化,现在将Order类的serialVersionUID改一下,再次执行反序列化,会报异常:
    java.io.InvalidClassException: com.ydt.pojo.Order; local class incompatible: stream classdesc serialVersionUID = 8334617098942116608, local class serialVersionUID = 8334617098942116609*/

8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;

深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设B复制了A,修改A的时候,看B是否发生变化:
    如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)
    如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)
@Test
public void testDeepCopy() throws IOException, ClassNotFoundException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
    Person person1 = new Person(18,"laohu","men");
    Role role = new Role("admin");
    person1.setRole(role);
    oos.writeObject(person1);
    oos.flush();
    oos.close();
    //从文件中读取字节序列,反序列化为对象
    ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.txt"));
    Person person2 = (Person) ois.readObject();
    person2.getRole().setRoleName("user");
    System.out.println(person1);
    System.out.println(person2);
    ois.close();
}

2.2 谷歌gson方式

我们使用比较有代表性的Gson插件来进行序列化转换

引入依赖坐标:

        
            com.google.code.gson
            gson
            2.8.0
        

    @Test
    public void testGson() throws IOException {
        Gson gson = new Gson();
        Person person = new Person(18,"laohu","men");
        //序列化
        String json = gson.toJson(person);
        byte[] bytes = json.getBytes();
        System.out.println(Arrays.toString(bytes));
​
        //反序列化
        Person decodePerson = gson.fromJson(new String(bytes),Person.class);
        System.out.println(decodePerson);
​
        //生成序列化文件
        OutputStream os = new FileOutputStream("object2.txt");
        os.write(bytes);
        os.flush();
        os.close();
    }
​
    //打印的字节数组
    /*[123, 34, 105, 100, 34, 58, 49, 44, 34, 117, 115, 101, 114, 110, 97, 109, 101, 34, 58, 34, 108, 97, 111, 104, 117, 34, 44, 34, 112, 97, 115, 115, 119, 111, 114, 100, 34, 58, 34, 49, 50, 51, 52, 53, 54, 34, 125]*/

2.3 谷歌ProtoBuf协议组件

2.3.1 为什么要使用protobuf

使用protobuf的原因肯定是为了解决开发中的一些问题,那使用其他的序列化机制会出现什么问题呢?

(1)java默认序列化机制:效率极低,而且还能不能跨语言之间共享数据。

(2)XML常用于与其他项目之间数据传输或者是共享数据,但是编码和解码会造成很大的性能损失。

(3)gson格式也是常见的一种,但是gson在解析的时候非常耗时,而且gson结构非常占内存。

但是我们protobuf是一种灵活的、高效的、自动化的序列化机制,可以有效的解决上面的问题。现在应该清楚了吧,正是由于目前的机制存在了很多问题,所以才有了这个序列化框架。

并且是语言跨平台的(基于Shell命令执行)

2.3.2 ProtoBuf入门使用

2.3.2.1、下载安装并配置环境变量

下载地址:https://github.com/protocolbuffers/protobuf/releases 选择你喜欢的版本

序列化、反序列化原理和Protobuf实现机制_第1张图片

配置Path环境变量,指定protoc.exe执行程序路径:

序列化、反序列化原理和Protobuf实现机制_第2张图片

打开CMD,输入protoc --version,显示版本号即可:

20210122170657470.png

2.3.2.2 手动命令实现(略)

语法:protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto PS:也可以生成C++文件,区别在于java_out改为cpp_out

protoc -I="D://workspaces//workspace//serializable-test" --java_out="D://workspaces//workspace//serializable-test//src//main//java" "D://workspaces//workspace//serializable-test//src//main//java//com//ydt//protobuf//person.proto"

2.3.2.3 Java代码实现

导入依赖坐标:

        
            com.google.protobuf
            protobuf-java
            3.7.1
        

先根据proto2或者proto3的语法创建一个.proto文件,下面是.proto数据类型和Java类型的对照表

序列化、反序列化原理和Protobuf实现机制_第3张图片

// 如果使用此注释,则使用proto3; 否则使用proto2
syntax = "proto3";
// 生成类的包名
option java_package = "com.ydt.template";
//生成的数据访问类的类名,如果没有指定此值,则生成的类名为proto文件名的驼峰命名方法
option java_outer_classname = "UserSerializable";
message User {
   int32 age = 1;
   string name = 2;
   string sex = 3;
}

定义一个命令类,用来根据.proto文件生成对应的序列化类:

package com.ydt.cmd;
​
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
​
public class Cmd {
​
    // protoc的目录
    private static final String PROTOC_FILE = System.getProperty("user.dir")+ "\\src\\main\\resources\\protoc.exe";
    // .proto文件所在项目根目录
    private static final String IMPOR_TPROTO = System.getProperty("user.dir");
    // 生成java类输出目录
    private static final String JAVA_OUT = System.getProperty("user.dir")+ "\\src\\main\\java";
    // 指定proto文件
    private static final String PROTOS = System.getProperty("user.dir")+ "\\src\\main\\java\\com\\ydt\\protobuf\\user.proto";
​
    /**
     * 使用java process执行shell命令
     */
    public void execute() {
        List lCommand = new ArrayList();
        lCommand.add(PROTOC_FILE);
        lCommand.add("-I=" + IMPOR_TPROTO );
        lCommand.add("--java_out=" + JAVA_OUT);
        lCommand.add(PROTOS);
        ProcessBuilder pb = new ProcessBuilder(lCommand);
        pb.redirectErrorStream(true);
        Process p;
        int i = 1;
        try {
            p = pb.start();
            try {
                //jdk实现process时,调用外部命令不是同步的调用,而是异步执行,需要等待执行完成
                i = p.waitFor();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        int iResult = p.exitValue();
        if (iResult == 0 && i == 0) {
            System.out.println(" result = " + p.exitValue() + ", execute command success! Command = " + lCommand);
        } else {
            System.out.println(" result = " + p.exitValue() + ", execute command failure! Command = " + lCommand);
        }
    }
}
​

生成序列化Java实体类测试:

    @Test
    public void test3(){
        Cmd cmd = new Cmd();
        cmd.execute();
    }

生成成功:

序列化、反序列化原理和Protobuf实现机制_第4张图片

序列化和反序列化测试:

    @Test
    public void test4() throws IOException {
        //序列化
        UserSerializable.User.Builder builder = UserSerializable.User.newBuilder();
        builder.setAge(18)
                .setName("laohu")
                .setSex("men");
        UserSerializable.User user = builder.build();
        byte[] bytes = user.toByteArray();
        System.out.println(Arrays.toString(bytes));
​
        //反序列化
        UserSerializable.User decodeUser = UserSerializable.User.parseFrom(bytes);
        System.out.println(decodeUser);
​
        //生成序列化文件
        OutputStream os = new FileOutputStream("object3.txt");
        os.write(bytes);
        os.flush();
        os.close();
​
    }
​
    //protobuf打印的结果
    /*[8, 1, 18, 5, 108, 97, 111, 104, 117, 26, 6, 49, 50, 51, 52, 53, 54]*/
    
    //Gson打印的结果
    /*[123, 34, 105, 100, 34, 58, 49, 44, 34, 117, 115, 101, 114, 110, 97, 109, 101, 34, 58, 34, 108, 97, 111, 104, 117, 34, 44, 34, 112, 97, 115, 115, 119, 111, 114, 100, 34, 58, 34, 49, 50, 51, 52, 53, 54, 34, 125]*/
​
    //jdk打印的结果
    /*[-84, -19, 0, 5, 115, 114, 0, 17, 99, 111, 109, 46, 121, 100, 116, 46, 112, 111, 106, 111, 46, 85, 115, 101, 114, 108, 124, -59, -89, -28, 34, 123, -76, 2, 0, 3, 73, 0, 2, 105, 100, 76, 0, 8, 112, 97, 115, 115, 119, 111, 114, 100, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 0, 0, 0, 1, 116, 0, 6, 49, 50, 51, 52, 53, 54, 116, 0, 5, 108, 97, 111, 104, 117]*/

起码我们现在可以看到,同样的一个User对象,protobuf序列化的字节码数组明显小太多,那么如果大量这样的对象在网络中传输的话,对于带宽的消耗是否要低得多,是不是速度会更快!

当然,Gson序列化的方式比较合理,所以大家注意了,在对性能没有极限要求的情况下,用Gson进行序列化即可,毕竟使用protobuf需要的学习成本比较大

2.3.3 ProtoBuf的实现机制

从上面我们看到,ProtoBuf在网络传输中的字节流数组非常小,这是为什么呢?

我们先来看看三种情况下生成的序列化文件:

JDK:序列化、反序列化原理和Protobuf实现机制_第5张图片

Gson:2021012217074022.png

ProtoBuf:20210122170746925.png

发现什么了没?

JDK的最复杂,0000000h-000000c0h表示行号;0-f表示列;行后面的文字表示对这行16进制的解释(PS:notepad打开需要安装hex插件)

Gson次之,包括属性,括号等

ProtoBuf只剩下顺序的属性值了!

	@java.lang.Override
    public void writeTo(com.google.protobuf.CodedOutputStream output)
                        throws java.io.IOException {
      if (id_ != 0) {
        output.writeInt32(1, id_);//指定位置1
      }
      if (!getUsernameBytes().isEmpty()) {
        com.google.protobuf.GeneratedMessageV3.writeString(output, 2, username_);//指定位置2
      }
      for (int i = 0; i < orders_.size(); i++) {
        com.google.protobuf.GeneratedMessageV3.writeString(output, 3, orders_.getRaw(i));//指定位置3
      }
      if (!getPasswordBytes().isEmpty()) {
        com.google.protobuf.GeneratedMessageV3.writeString(output, 4, password_);//指定位置4
      }
      unknownFields.writeTo(output);
    }

另外,ProtoBuf还有最大的特性:数据动态伸缩

打个比方,对于int age = 35这个成员变量来说,如果是JSON或者JDK的情况下,不管怎么样,都是占四个字节,而ProtoBuf采用可伸缩的机制,根据你实际的值来确定所占字节(int(1-5)最多五个字节),那么对于问题很明朗了,世界上超过100岁的人不多吧,超过1000岁的那是神仙了,所以ProtoBuf在age这个成员变量上最多消耗两个字节位!空间就这么省出来了!

		public final void writeUInt32NoTag(int value) throws IOException {
            if (CodedOutputStream.HAS_UNSAFE_ARRAY_OPERATIONS && this.spaceLeft() >= 10) {
                while((value & -128) != 0) {
                    //010000000(128)
                    //110000000(-128)   ---->010000000 != 0
                    UnsafeUtil.putByte(this.buffer, (long)(this.position++), (byte)(value & 127 | 128));
                    value >>>= 7; //010000000右移七位 --->01
                }

                UnsafeUtil.putByte(this.buffer, (long)(this.position++), (byte)value);
            } else {
                try {
                    while((value & -128) != 0) {
                        this.buffer[this.position++] = (byte)(value & 127 | 128);
                        value >>>= 7;
                    }

                    this.buffer[this.position++] = (byte)value;
                } catch (IndexOutOfBoundsException var3) {
                    throw new CodedOutputStream.OutOfSpaceException(String.format("Pos: %d, limit: %d, len: %d", this.position, this.limit, 1), var3);
                }
            }
        }

你可能感兴趣的:(分布式专题,编程语言,java)