编程语言
首页 > 编程语言> > 解锁大厂思维:剖析《阿里巴巴 Java 开发手册》/03 Java序列化引发的血案

解锁大厂思维:剖析《阿里巴巴 Java 开发手册》/03 Java序列化引发的血案

作者:互联网


先相信你自己,然后别人才会相信你。
——屠格涅夫

1、前言
《手册》第 9 页 “OOP 规约” 部分有一段关于序列化的约定 1:

【强制】当序列化类新增属性时,请不要修改 serialVersionUID 字段,以避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 值不一致会抛出序列化运行时异常。

我们应该思考下面几个问题:

序列化和反序列化到底是什么?
它的主要使用场景有哪些?
Java 序列化常见的方案有哪些?
各种常见序列化方案的区别有哪些?
实际的业务开发中有哪些坑点?

接下来将从这几个角度去研究这个问题。
2. 序列化和反序列化是什么?为什么需要它?
序列化是将内存中的对象信息转化成可以存储或者传输的数据到临时或永久存储的过程。而反序列化正好相反,是从临时或永久存储中读取序列化的数据并转化成内存对象的过程。

那么为什么需要序列化和反序列化呢?
希望大家能够养成从本源上思考这个问题的思维方式,即思考它为什么会出现,而不是单纯记忆。
大家可以回忆一下,平时都是如果将文字文件、图片文件、视频文件、软件安装包等传给小伙伴时,这些资源在计算机中存储的方式是怎样的。
进而再思考,Java 中的对象如果需要存储或者传输应该通过什么形式呢?

我们都知道,一个文件通常是一个 m 个字节的序列:B0, B1, …, Bk, …, Bm-1。所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对应文件的读和写来执行。2

大家试想一个典型的场景:如果要实现 Java 远程方法调用,就需要将调用结果通过网路传输给调用方,如果调用方和服务提供方不在一台机器上就很难共享内存,就需要将 Java 对象进行传输。而想要将 Java 中的对象进行网络传输或存储到文件中,就需要将对象转化为二进制字节流,这就是所谓的序列化。存储或传输之后必然就需要将二进制流读取并解析成 Java 对象,这就是所谓的反序列化。

序列化的主要目的是:方便存储到文件系统数据库系统或网络传输等

实际开发中常用到序列化和反序列化的场景有:

远程方法调用(RPC)的框架里会用到序列化。
将对象存储到文件中时,需要用到序列化。
将对象存储到缓存数据库(如 Redis)时需要用到序列化。
通过序列化和反序列化的方式实现对象的深拷贝。
  1. 常见的序列化方式

常见的序列化方式包括 Java 原生序列化、Hessian 序列化、Kryo 序列化、JSON 序列化等。

3.1 Java 原生序列化
正如前面章节讲到的,对于 JDK 中有的类,最好的学习方式之一就是直接看其源码。

Serializable 的源码非常简单,只有声明,没有属性和方法:

// 注释太长,省略
public interface Serializable {
}

在学习源码注释之前,希望大家可以站在设计者的角度,先思考一个问题:如果一个类序列化到文件之后,类的结构发生变化还能否保证正确地反序列化呢?

答案显然是不确定的。

那么如何判断文件被修改过了呢? 通常可以通过加密算法对其进行签名,文件作出任何修改签名就会不一致。但是 Java 序列化的场景并不适合使用上述的方案,因为类文件的某些位置加个空格,换行等符号类的结构没有发生变化,这个签名就不应该发生变化。还有一个类新增一个属性,之前的属性都是有值的,之前都被序列化到对象文件中,有些场景下还希望反序列化时可以正常解析,怎么办呢?

那么是否可以通过约定一个唯一的 ID,通过 ID 对比,不一致就认为不可反序列化呢?

实现序列化接口后,如果开发者不手动指定该版本号 ID 怎么办?

既然 Java 序列化场景下的 “签名” 应该根据类的特点生成,我们是否可以不指定序列化版本号就默认根据类名、属性和函数等计算呢?

如果针对某个自己定义的类,想自定义序列化和反序列化机制该如何实现呢?支持吗?

带着这些问题我们继续看序列化接口的注释。

Serializable 的源码注释特别长,其核心大致作了下面的说明:

Java 原生序列化需要实现 Serializable 接口。序列化接口不包含任何方法和属性等,它只起到序列化标识作用。

一个类实现序列化接口则其子类型也会继承序列化能力,但是实现序列化接口的类中有其他对象的引用,则其他对象也要实现序列化接口。序列化时如果抛出 NotSerializableException 异常,说明该对象没有实现 Serializable 接口。

每个序列化类都有一个叫 serialVersionUID 的版本号,反序列化时会校验待反射的类的序列化版本号和加载的序列化字节流中的版本号是否一致,如果序列化号不一致则会抛出 InvalidClassException 异常。

强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号,因为这个默认的序列化号和类的特征以及编译器的实现都有关系,很容易在反序列化时抛出 InvalidClassException 异常。建议将这个序列化版本号声明为私有,以避免运行时被修改。

实现序列化接口的类可以提供自定义的函数修改默认的序列化和反序列化行为。

自定义序列化方法:

private void writeObject(ObjectOutputStream out) throws IOException;

自定义反序列化方法:

private void readObject(ObjectInputStream in) 
  throws IOException, ClassNotFoundException;

通过自定义这两个函数,可以实现序列化和反序列化不可序列化的属性,也可以对序列化的数据进行数据的加密和解密处理。

3.2 Hessian 序列化
Hessian 是一个动态类型,二进制序列化,也是一个基于对象传输的网络协议。Hessian 是一种跨语言的序列化方案,序列化后的字节数更少,效率更高。Hessian 序列化会把复杂对象的属性映射到 Map 中再进行序列化。

3.3 Kryo 序列化
Kryo 是一个快速高效的 Java 序列化和克隆工具。Kryo 的目标是快速、字节少和易用。Kryo 还可以自动进行深拷贝或者浅拷贝。Kryo 的拷贝是对象到对象的拷贝而不是对象到字节,再从字节到对象的恢复。Kryo 为了保证序列化的高效率,会提前加载需要的类,这会带一些消耗,但是这是序列化后文件较小且反序列化非常快的重要原因。

3.4 JSON 序列化
JSON (JavaScript Object Notation) 是一种轻量级的数据交换方式。JSON 序列化是基于 JSON 这种结构来实现的。JSON 序列化将对象转化成 JSON 字符串,JSON 反序列化则是将 JSON 字符串转回对象的过程。常用的 JSON 序列化和反序列化的库有 Jackson、GSON、Fastjson 等。

http://www.imooc.com/read/55/article/1140

标签:03,存储,Java,对象,Kryo,JSON,序列化
来源: https://www.cnblogs.com/ukzq/p/13506338.html