Java学习之序列化

1、引言

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

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

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

序列化和反序列化到底是什么?
它的主要使用场景有哪些?
Java 序列化常见的方案有哪些?
各种常见序列化方案的区别有哪些?
实际的业务开发中有哪些坑点?
接下来将从这几个角度去研究这个问题。

2、序列化和反序列化是什么?为什么需要它?

序列化是将内存中的对象信息转化成可以存储或者传输的数据到临时或永久存储的过程。而反序列化正好相反,是从临时或永久存储中读取序列化的数据并转化成内存对象的过程。

file

那么为什么需要序列化和反序列化呢?

我们都知道,文本文件,图片、视频和安装包等文件底层都被转化为二进制字节流来传输的,对方得文件就需要对文件进行解析,因此就需要有能够根据不同的文件类型来解码出文件的内容的程序。

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

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

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

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

3、常见的序列化方式

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

3.1 Java 原生序列化

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

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

public interface 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 等。

4、Java 常见的序列化方案对比

我们想要对比各种序列化方案的优劣无外乎两点,一点是查资料,一点是自己写代码验证。

4.1 Java 原生序列化

Java 序列化的优点是:对对象的结构描述清晰,反序列化更安全。主要缺点是:效率低,序列化后的二进制流较大。

4.2 Hessian 序列化

Hession 序列化二进制流较 Java 序列化更小,且序列化和反序列化耗时更短。但是父类和子类有相同类型属性时,由于先序列化子类再序列化父类,因此反序列化时子类的同名属性会被父类的值覆盖掉,开发时要特别注意这种情况。
Hession2.0 序列化二进制流大小是 Java 序列化的 50%,序列化耗时是 Java 序列化的 30%,反序列化的耗时是 Java 序列化的 20%。

4.3 Kryo 序列化

Kryo 优点是:速度快、序列化后二进制流体积小、反序列化超快。但是缺点是:跨语言支持复杂。注册模式序列化更快,但是编程更加复杂。

4.4 JSON 序列化

JSON 序列化的优势在于可读性更强。主要缺点是:没有携带类型信息,只有提供了准确的类型信息才能准确地进行反序列化,这点也特别容易引发线上问题。

下面给出使用 Gson 框架模拟 JSON 序列化时遇到的反序列化问题的示例代码:

/*** 验证GSON序列化类型错误*/
@Test
public void testGSON() {Map<String, Object> map = new HashMap<>();final String name = "name";final String id = "id";map.put(name, "张三");map.put(id, 20L);String jsonString = GSONSerialUtil.getJsonString(map);Map<String, Object> mapGSON = GSONSerialUtil.parseJson(jsonString, Map.class);// 正确Assert.assertEquals(map.get(name), mapGSON.get(name));// 不等  map.get(id)为Long类型 mapGSON.get(id)为Double类型Assert.assertNotEquals(map.get(id).getClass(), mapGSON.get(id).getClass());Assert.assertNotEquals(map.get(id), mapGSON.get(id));
}

下面给出使用 fastjson 模拟 JSON 反序列化问题的示例代码:

/*** 验证FatJson序列化类型错误*/
@Test
public void testFastJson() {Map<String, Object> map = new HashMap<>();final String name = "name";final String id = "id";map.put(name, "张三");map.put(id, 20L);String fastJsonString = FastJsonUtil.getJsonString(map);Map<String, Object> mapFastJson = FastJsonUtil.parseJson(fastJsonString, Map.class);// 正确Assert.assertEquals(map.get(name), mapFastJson.get(name));// 错误  map.get(id)为Long类型 mapFastJson.get(id)为Integer类型Assert.assertNotEquals(map.get(id).getClass(), mapFastJson.get(id).getClass());Assert.assertNotEquals(map.get(id), mapFastJson.get(id));
}

大家还可以通过单元测试构造大量复杂对象对比各种序列化方式或框架的效率。

如定义下列测试类为 User,包括以下多种类型的属性:

@Data
public class User implements Serializable {private Long id;private String name;private Integer age;private Boolean sex;private String nickName;private Date birthDay;private Double salary;
}

4.5 各种常见的序列化性能排序

实验的版本:kryo-shaded 使用 4.0.2 版本,gson 使用 2.8.5 版本,hessian 用 4.0.62 版本。

实验的数据:构造 50 万 User 对象运行多次。

大致得出一个结论:

从二进制流大小来讲:JSON 序列化 > Java 序列化 > Hessian2 序列化 > Kryo 序列化 > Kryo 序列化注册模式;
从序列化耗时而言来讲:GSON 序列化 > Java 序列化 > Kryo 序列化 > Hessian2 序列化 > Kryo 序列化注册模式;
从反序列化耗时而言来讲:GSON 序列化 > Java 序列化 > Hessian2 序列化 > Kryo 序列化注册模式 > Kryo 序列化;
从总耗时而言:Kryo 序列化注册模式耗时最短。
注:由于所用的序列化框架版本不同,对象的复杂程度不同,环境和计算机性能差异等原因结果可能会有出入。

5、 序列化引发的一个血案

接下来我们看下面的一个案例:

前端调用服务 A,服务 A 调用服务 B,服务 B 首次接到请求会查 DB,然后缓存到 Redis(缓存 1 个小时)。服务 A 根据服务 B 返回的数据后执行一些处理逻辑,处理后形成新的对象存到 Redis(缓存 2 个小时)。

服务 A 通过 Dubbo 来调用服务 B,A 和 B 之间数据通过 Map<String,Object> 类型传输,服务 B 使用 Fastjson 来实现 JSON 的序列化和反序列化。

服务 B 的接口返回的 Map 值中存在一个 Long 类型的 id 字段,服务 A 获取到 Map ,取出 id 字段并强转为 Long 类型使用。

执行的流程如下:

file

通过分析我们发现,服务 A 和服务 B 的 RPC 调用使用 Java 序列化,因此类型信息不会丢失。

但是由于服务 B 采用 JSON 序列化进行缓存,第一次访问没啥问题,其执行流程如下:

file

如果服务 A 开启了缓存,服务 A 在第一次请求服务 B 后,缓存了运算结果,且服务 A 缓存时间比服务 B 长,因此不会出现错误。

file

如果服务 A 不开启缓存,服务 A 会请求服务 B ,由于首次请求时,服务 B 已经缓存了数据,服务 B 从 Redis(B)中反序列化得到 Map。流程如下图所示:

file

然而问题来了: 服务 A 从 Map 取出此 Id 字段,强转为 Long 时会出现类型转换异常。

最后定位到原因是 Json 反序列化 Map 时如果原始值小于 Int 最大值,反序列化后原本为 Long 类型的字段,变为了 Integer 类型,服务 B 的同学紧急修复。

服务 A 开启缓存时, 虽然采用了 JSON 序列化存入缓存,但是采用 DTO 对象而不是 Map 来存放属性,所以 JSON 反序列化没有问题。

因此大家使用二方或者三方服务时,当对方返回的是 Map<String,Object> 类型的数据时要特别注意这个问题。

作为服务提供方,可以采用 JDK 或者 Hessian 等序列化方式;

作为服务的使用方,我们不要从 Map 中一个字段一个字段获取和转换,可以使用 JSON 库直接将 Map 映射成所需的对象,这样做不仅代码更简洁还可以避免强转失败。

代码示例:

@Test
public void testFastJsonObject() {Map<String, Object> map = new HashMap<>();final String name = "name";final String id = "id";map.put(name, "张三");map.put(id, 20L);String fastJsonString = FastJsonUtil.getJsonString(map);// 模拟拿到服务B的数据Map<String, Object> mapFastJson = FastJsonUtil.parseJson(fastJsonString,map.getClass());// 转成强类型属性的对象而不是使用map 单个取值User user = new JSONObject(mapFastJson).toJavaObject(User.class);// 正确Assert.assertEquals(map.get(name), user.getName());// 正确Assert.assertEquals(map.get(id), user.getId());
}

6、课后题

给出一个 PersonTransit 类,一个 Address 类,假设 Address 是其它 jar 包中的类,没实现序列化接口。请使用今天讲述的自定义的函数 writeObject 和 readObject 函数实现 PersonTransit 对象的序列化,要求反序列化后 address 的值正常。

@Data
public class PersonTransit implements Serializable {private Long id;private String name;private Boolean male;private List<PersonTransit> friends;private Address address;
}@Data
@AllArgsConstructor
public class Address {private String detail;}

一、序列化主要有两个困难:

1 transient 关键字,序列化时默认不序列化该字段。(新加的,增加难度)

2 假设 Address 是第三方 jar 包中的类,不允许修改实现序列化接口。

二、分析

我们通过专栏的介绍还有序列化接口java.io.Serializable的注释可知,可以自定义序列化方法和反序列化方法:

private void writeObject(java.io.ObjectOutputStream out)throws IOExceptionprivate void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException;

实现序列化和反序列化不可序列化的属性,也可以对序列化的数据进行数据的加密和解密处理。

三、参考代码

@Data
public class PersonTransit implements Serializable {private Long id;private String name;private Boolean male;private List<PersonTransit> friends;private transient Address address;/*** 自定义序列化写方法*/private void writeObject(ObjectOutputStream oos) throws IOException {oos.defaultWriteObject();oos.writeObject(address.getDetail());}/*** 自定义反序列化读方法*/private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {ois.defaultReadObject();this.setAddress(new Address( (String) ois.readObject()));}
}

单元测试

@Test
public void testJDKSerialOverwrite() throws IOException, ClassNotFoundException {PersonTransit person = new PersonTransit();person.setId(1L);person.setName("张三");person.setMale(true);person.setFriends(new ArrayList<>());Address address = new Address();address.setDetail("某某小区xxx栋yy号");person.setAddress(address);// 序列化JdkSerialUtil.writeObject(file, person);// 反序列化PersonTransit personTransit = JdkSerialUtil.readObject(file);// 判断是否相等Assert.assertEquals(personTransit.getName(), person.getName());Assert.assertEquals(personTransit.getAddress().getDetail(), person.getAddress().getDetail());
}

用到的工具类:

public class JdkSerialUtil {public static <T> void writeObject(File file, T data) throws IOException {try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));) {objectOutputStream.writeObject(data);objectOutputStream.flush();}}public static <T> void writeObject(ByteArrayOutputStream outputStream, T data) throws IOException {try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);) {objectOutputStream.writeObject(data);objectOutputStream.flush();}}public static <T> T readObject(File file) throws IOException, ClassNotFoundException {FileInputStream fin = new FileInputStream(file);ObjectInputStream objectInputStream = new ObjectInputStream(fin);return (T) objectInputStream.readObject();}public static <T> T readObject(ByteArrayInputStream inputStream) throws IOException, ClassNotFoundException {ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);return (T) objectInputStream.readObject();}
}

通过单元测试验证了我们编写代码的正确性。

7、思考题

1.为什么我们在前端向后端发送请求时,请求对象没有序列化id也可以正常使用。

因为前端向后端发送请求,实际上是一个反序列化的过程,反序列化不需要序列化id,是json格式转换为对象。如果一个对象在同一台服务器上,那么他们可以共享内存,并不需要序列化。序列一般存在网络传输和io传输的情况下

2.为什么我们在使用fegin调用接口时,为什么请求对象不需要实现序列化接口也能请求成功?

因为使用fegin调用接口时,实际上是将java对象序列化json对象,序列成java对象不需要实现序列化接口。JSON序列化是基于对象的字段和属性的反射,因此只要对象的字段可以被访问并且符合JSON的数据类型要求(例如,字符串、数字、布尔等),通常就可以成功地将对象序列化为JSON,而不需要额外的接口。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/66343.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

聊聊Http服务化改造实践

在微服务架构体系中远程RPC调用主要包括Dubbo与Http调用两个大类&#xff0c;由于Dubbo拥有服务注册中心&#xff0c;并且起服务的命名非常规范&#xff0c;使用包名.类名.方法名进行描述。 而http调用通常都是使用httpclient等相关类库&#xff0c;这些在使用上并没有问题&am…

Matlab(画图进阶)

目录 大纲 1.特殊的Plots 1.1 loglog(双对数刻度图) ​1.3 plotyy(创建具有两个y轴的图形) 1.4yyaxis(创建具有两个y轴的图) 1.5 bar 3D条形图(bar3) 1.6 pie(饼图) 3D饼图 1.7 polar 2.Stairs And Ste阶梯图 3.Boxplot 箱型图和Error Bar误差条形图 3.1 boxplot 3.2 …

微信小程序开发教学系列(12)- 实战项目案例

十二、实战项目案例 本章将通过一个简单的实战项目案例来帮助读者巩固之前学习到的知识。我们将搭建一个名为“ToDoList”的微信小程序&#xff0c;实现一个简单的任务清单功能。 项目介绍 ToDoList是一个用于记录和管理任务的小程序。用户可以添加、编辑、完成和删除任务&a…

Docker部署RustDesk Server 设置开机自启

三、Docker安装 Docker官方和国内daocloud都提供了一键安装的脚本&#xff0c;使得Docker的安装更加便捷。 官方的一键安装方式&#xff1a; curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun 国内 daocloud一键安装命令&#xff1a; curl -sSL https://…

Docker学习

文章目录 我的docker环境替换apt软件包镜像源docker安装以apt包管理器安装docker卸载 以docker官方存储库安装卸载 添加某用户到docker组配置docker镜像 docker命令学习操作守护进程相关命令操作镜像相关命令查看镜像搜索镜像拉取镜像删除镜像 操作容器相关命令查看容器创建容器…

Deepin 图形化部署 Hadoop Single Node Cluster

Deepin 图形化部署 Hadoop Single Node Cluster 升级操作系统和软件 快捷键 ctrlaltt 打开控制台窗口 更新 apt 源 sudo apt update更新 系统和软件 sudo apt -y dist-upgrade升级后建议重启 开启ssh服务 打开资源管理器 进入系统盘 找到 etc 目录 在系统盘的 etc 目录上 右键…

【校招VIP】前端校招考点之UDP

考点介绍&#xff1a; UDP是非面向连接协议&#xff0c;使用udp协议通讯并不需要建立连接&#xff0c;它只负责把数据尽可能发送出去&#xff0c;并不可靠&#xff0c;在接收端&#xff0c;UDP把每个消息断放入队列中&#xff0c;接收端程序从队列中读取数据。 『前端校招考点…

【leetcode 力扣刷题】汇总区间//合并区间//插入区间

一些关于区间的力扣题目 228. 汇总区间56. 合并区间57. 插入区间 228. 汇总区间 题目链接&#xff1a;228.汇总区间 题目内容&#xff1a; 看题目真是没懂这个题到底是要干啥……实际上题目要求的恰好覆盖数组中所有数字的最小有序区间范围列表&#xff0c;这个最小是指一个区…

使用Docker安装和部署kkFileView

&#x1f388;1 参考文档 kkFileView官方文档 &#x1f680;2 安装kkFileView 拉取Redis镜像。 docker pull keking/kkfileview启动docker容器。 docker run -it -d -p 8012:8012 keking/kkfileview --restart always解释&#xff1a; docker run redis # 从kkfileview镜像运行…

『PyQt5-Qt Designer篇』| 08 Qt Designer中容器布局和绝对布局的使用

08 Qt Designer中容器布局和绝对布局的使用 1 容器布局1.1 设计容器布局1.2 保存文件并执行2 绝对布局2.1 设计绝对布局2.2 保存文件并执行1 容器布局 1.1 设计容器布局 先拖入一个容器Frame容器,然后拖入几个控件: 把拖入的控件拖入容器中: 选中容器,右键-布局-栅格布局:…

Python大数据处理利器之Pyspark详解

摘要&#xff1a; 在现代信息时代&#xff0c;数据是最宝贵的财富之一&#xff0c;如何处理和分析这些数据成为了关键。Python在数据处理方面表现得尤为突出。而pyspark作为一个强大的分布式计算框架&#xff0c;为大数据处理提供了一种高效的解决方案。本文将详细介绍pyspark…

父组件调用子组件 ref 不生效?组件暴露 ref ?

向你的组件暴露 ref 要暴露 ref 最关键的就是 forwardRef forwardRef 是 React 中的一个高阶函数&#xff0c;用于在函数组件中将 ref 属性向下传递给子组件。 在 React 中&#xff0c;我们可以使用 ref 属性来获取对一个组件实例的引用&#xff0c;以便在父组件中操作子组件。…

Dynamic ReLU:根据输入动态确定的ReLU

这是我最近才看到的一篇论文&#xff0c;它提出了动态ReLU (Dynamic ReLU, DY-ReLU)&#xff0c;可以将全局上下文编码为超函数&#xff0c;并相应地调整分段线性激活函数。与传统的ReLU相比&#xff0c;DY-ReLU的额外计算成本可以忽略不计&#xff0c;但表示能力明显增强&…

项目(补充2):智慧教室

一。emWin环境的搭建 1.codeBlock下载 开源免费。 2.使用stm的Cubemx提供的作图软件 &#xff08;1&#xff09;在C盘下找到第三方的固件库&#xff0c;旁边有个ST文件夹 注意&#xff1a;我在下载cubemx为默认的路径 &#xff08;2&#xff09;STemWin中的Soft提供了绘图…

一文1800字从0到1使用Python Flask实战构建Web应用

Python Flask是一个轻量级的Web框架&#xff0c;它简单易用、灵活性高&#xff0c;适用于构建各种规模的Web应用。本文将介绍如何使用Python Flask框架来实战构建一个简单的Web应用&#xff0c;并展示其基本功能和特性。 第一部分&#xff1a;搭建开发环境 在开始之前我们需要…

企业架构LNMP学习笔记8

1、 运维人员需要考虑安全性、稳定性。 安装&#xff1a; 解压进入到目录&#xff1a; shell > tar zxf php-7.2.12.tar.gz shell > cd php-7.2.12 安装依赖软件&#xff1a; yum -y install libxml2-devel libjpeg-devel libpng-devel freetype-devel curl-devel op…

【100天精通Python】Day52:Python 数据分析_Numpy入门基础与数组操作

目录 1 NumPy 基础概述 1.1 NumPy的主要特点和功能 1.2 NumPy 安装和导入 2 Numpy 数组 2.1 创建NumPy数组 2.2 数组的形状和维度 2.3 数组的数据类型 2.4 访问和修改数组元素 3 数组操作 3.1 数组运算 3.2 数学函数 3.3 统计函数 4 数组形状操作 4.1 重塑数组形…

使用Windbg动态调试排查软件启动不了的问题

目录 1、问题说明 2、初步分析 3、使用Windbg启动程序进行动态调试 4、进一步分析 5、何时使用Windbg静态分析&#xff1f;何时使用Windbg进行动态调试&#xff1f; 6、最后 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&…

Go在安装Gin时出现Failed to connect 报错问题的解决方案(已解决)

在命令行中输入&#xff1a;go get -u github.com/gin-gonic/gin指令安装Gin第三方包时出现连接错误与连接超时的情况如下&#xff1a; 在较新版本的Go中引入了全新的包管理机制&#xff0c;出现上述错误可能是包管理机制设置不恰当的问题&#xff0c;尝试在终端窗口输入如下…

解决Apache Tomcat “Request header is too large“ 异常 ‍

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…