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

本文深入探讨了Java序列化的概念,包括对象与二进制转换的目的,以及在持久化和远程调用中的应用。通过示例展示了如何实现序列化和反序列化,强调了序列化ID的重要性,解释了其在类版本控制中的角色。当类版本升级时,正确的序列化ID能避免因字段变化导致的异常,确保数据的一致性。
摘要由CSDN通过智能技术生成

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/javase/7/docs/technotes/guides/serialization/index.html

如何实现序列化?

实现序列化接口

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/7039166805448851463#heading-2

序列化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™ 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/wiki/1252599548343744/1298366845681698

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值