关于java:java序列化实现原理和深度分析

6次阅读

共计 6949 个字符,预计需要花费 18 分钟才能阅读完成。

java 序列化

什么是序列化?

对象和二进制的转换。

转换的目标是啥?

对象转换为二进制,而后再把二进制复原为对象。

具体利用场景是,把对象写到磁盘文件,或者更常见的就是把对象传到近程机器(比方,dubbo rpc 框架)。

什么是 java 序列化?

java 序列化非凡一点点,是对象和字节数组 (byte[] ) 的转换。然而,字节数组的实质也是二进制。

序列化的作用?

所以,无论是其余语言,还是 java 语言的序列化,实质作用都是为了从磁盘文件或者近程机器复原对象。


官网文档介绍

 Serialization is used for lightweight persistence and for communication via sockets or Java Remote Method Invocation (Java RMI).

https://docs.oracle.com/javas…

如何实现序列化?

实现序列化接口

public class Person implements Serializable {private static final long serialVersionUID = 2709425275741743919L;}

demo

pojo 类,次要是要实现序列化接口。

package test2;

import java.io.Serializable;

public class Person implements Serializable { // 实现序列化接口

  private static final long serialVersionUID = 1L;

  private String name;
  private Integer age;
  private String address;

  public Person() {}

  public Person(String name, Integer age, String address) {
    this.name = name;
    this.age = age;
    this.address = address;
  }


  @Override
  public String toString() {
    return "test2.Person{" +
        "name='" + name + '''+", age="+ age +", address='"+ address +''' +
        '}';
  }
}

测试类,外围步骤

  1. 序列化

把对象转换为二进制(即字节数组),而后写到磁盘文件

2、反序列化

从磁盘文件复原对象,其实就是把二进制再转换为对象

package test2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * @author gzh
 * @createTime 2021/12/6 8:08 PM
 */
public class Test {public static void main(String[] args) throws Exception {testversion1L();
  }

  public static void testversion1L() throws Exception {File file = new File("Person.out");
    // 序列化
    ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
    Person Person = new Person("Haozi", 22, "上海");
    oout.writeObject(Person); // 把对象转换为二进制(即字节数组),而后写到磁盘文件
    oout.close();

    // 反序列化
    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
    Object newPerson2 = oin.readObject();// 从磁盘文件复原对象,就把二进制转换为对象
    oin.close();
    System.out.println(newPerson2);
  }
}

java 序列化到底干了什么?协定格局是什么?

最次要和最实质其实也是把对象转换为二进制。

格局就是,java 独特的那一套货色,次要包含:

  1. java 根底数据类型
  2. java 非根底数据类型

所以,总之,java 序列化的二进制,只有 java 语言本人可能辨认,即只有 java 语言本人才能够把二进制再复原转换为对象。


源码剖析参考:

https://juejin.cn/post/703916…

序列化 id 的作用到底是什么?

jdk api 官网文档

Serializable

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.

However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization.

Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value.

It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class–serialVersionUID fields are not useful as inherited members.

总结

序列化 id,实质是 pojo 类的版本号。

  1. 如果没显式写序列化 id,jvm 默认生成一个随机值。
  2. 如果写了,个别状况下,值写 1L 就能够了。

用 idea 生成一个很大的随机值也能够。

  1. 官网举荐,最好显示写一个值(个别状况写 1L 就能够了)——因为 jvm 的实现可能不一样,导致消费者和提供者的 jvm 生成的版本号不一样。
  2. 如果提供者新增了字段或者批改了字段类型,就要降级序列化 id 版本,这样的话,提供者反序列化的时候就会异样:消费者和提供者的序列化 id 版本不统一。这正是咱们要的后果——因为如果提供者新增了字段或者批改了字段类型,然而提供者没有降级序列化 id 版本,而是依然和提供者的版本一样,那么提供者反序列化的时候就不会异样,然而这个时候,提供者的新增字段的值是 null(提供者新增了字段)或者类型转换异样(提供者批改了字段类型)。

demo

接着下面的 demo 代码例子,这里再演示一下。

先给提供者的 pojo 类加个字段 -email。

package test;

import java.io.Serializable;

public class Person implements Serializable {

  private static final long serialVersionUID = 2L;

  private String name;
  private Integer age;
  private String address;
  private String email; // 新增字段

  public Person() {}

  public Person(String name, Integer age, String address) {
    this.name = name;
    this.age = age;
    this.address = address;
  }

  public Person(String name, Integer age, String address,String email) {
    this.name = name;
    this.age = age;
    this.address = address;
    this.email = email;
  }

  @Override
  public String toString() {
    return "Person{" +
        "name='" + name + '''+", age="+ age +", address='"+ address +''' +
        ", email='" + email + '''+'}';
  }
}

而后,间接从下面 demo 曾经生成的文件里反序列化,失去对象。留神,要正文掉序列化代码。

package test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import test2.Person;

/**
 * @author gzh
 * @createTime 2021/12/6 8:08 PM
 */
public class Test {public static void main(String[] args) throws Exception {testversion1L();
  }

  public static void testversion1L() throws Exception {File file = new File("Person.out");
    // 序列化
//    ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
//    Person Person2 = new Person("Haozi", 22, "上海");
//    oout.writeObject(Person2);
//    oout.close();

    // 反序列化
    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
    Object newPerson2 = oin.readObject(); // 失去对象
    oin.close();
    System.out.println(newPerson2);
  }
}

打印后果:

Person{name='Haozi', age=22, address='上海', email='null'}

阐明:

  1. 提供者必须也要有对应类 pojo 类

从打印后果看,间接从磁盘文件反序列化能够失去对象,前提是提供者也要存在同一个包同一个类名字的 pojo 类,提供者才能够创建对象。

如果提供者没有对应的 pojo 类,就会报错:找不到类异样。

Exception in thread "main" java.lang.ClassNotFoundException: Person
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:686)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1868)
    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)
    at Test.testversion1L(Test.java:27)
    at Test.main(Test.java:14)

如何模仿这个异样?间接把 pojo 类删除或者换个名字即可。

  1. 提供者新增字段的值为什么是 null?

如果提供者有对应的 pojo 类,提供者就能够从磁盘文件的二进制数据,创建对象,正如下面的打印后果。然而,因为提供者新增了一个字段,而消费者写入二进制数据到磁盘文件的时候并没有这个字段,所以新增的字段的值是 null。

然而,理论工作当中,咱们应该不容许这种状况呈现。如果提供者新增了字段,pojo 类要降级一下版本,即序列化 id 的值改为 2L。

持续往下剖析,正如下面的例子,消费者写入磁盘的二进制数据的版本是 1L,这个时候,提供者反序列化会报错:版本号不匹配,具体来说是,流 (即消费者) 的版本号是 1,本地 (即提供者) 的版本是 2。

Exception in thread "main" java.io.InvalidClassException: Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
    at 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)
    at Test.testversion1L(Test.java:27)
    at Test.main(Test.java:14)
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'

Process finished with exit code 1

尽管这个时候异样了,然而这正是咱们想要的后果。因为能够一眼看进去,消费者和提供者的 pojo 版本不匹配,背地的实质必定是提供者新增了字段或者批改了字段的类型。这个时候,消费者,也应该新增字段或者批改字段类型,和提供者保持一致。如果提供者没有降级版本号,尽管提供者没有报错,然而提供者新增字段的值是 null(因为消费者短少该新增字段),这个时候其实反而是有问题的,因为失常状况下提供者新增了字段,必定是要是应用该字段的值的,值从哪里来?必定是消费者来。消费者怎么来?也新增字段,并且和提供者版本保持一致即可解决问题。

参考

https://www.liaoxuefeng.com/w…

Java serialVersionUID 有什么作用?https://www.jianshu.com/p/91f…

正文完
 0