【Java 拾遗】不可不知的 Java 序列化
前言
在程序运行的生命周期中,序列化与反序列化的操作,简直无时无刻不在产生着。对于任何一门语言来说,不论它是编译型还是解释型,只有它须要通信或者长久化时,就必然波及到序列化与反序列化操作。然而,又正因为序列化与反序列化太过重要,太过广泛,大部分编程语言和框架都对其进行了很好的封装,又因为他的润物细无声,使得咱们很多时候基本没有意识到,代码上面其实进行了许许多多序列化相干的操作。明天咱们就一起去探寻这位最相熟的陌生人。
序列化是什么
百度百科中给序列化的定义是『序列化 (Serialization)是将对象的状态信息转换为能够存储或传输的模式的过程。』。仿佛有点形象,上面用一个例子简略类比一下。
日常生活中,总少不了人跟人之间的交换与沟通。而沟通的前提是先要把咱们大脑中想的内容,通过某种模式表达出来。而后他人再通过咱们表白出的内容去了解。
而表白的形式多种多样,最常见的就是谈话,咱们通过说一些话,把咱们脑海里想的内容表达出来,对方听了这些话立即明确了咱们的想法。当然表白也能够是文字,比方你正在看的本文,不也是在与你交换吗?导演通过电影去表白本人对于世界的了解,画家通过画作述说的对美的渴望,音乐家通过乐符形容着对自在的向往。凡此种种,举不胜举。
所以,这些又跟咱们的主题 序列化 有什么关系呢?
其实人与人之间少不了沟通交流,程序与程序之间,机器与机器之间也少不了沟通交流。只不过通常不会说是沟通,咱们会说申请、响应、传输、通信…… 同样的内容只是换了一种说法。
上文中提到,人与人之间的沟通须要一种表达方式。通过这种表达方式把咱们大脑中所想的内容,转化成别人能够了解的内容。而机器与机器之间的通信也须要这样一种表达方式,通过这种表达方式把内存中的内容,转化成其它机器能够读取的内容。
所以序列化能够简略的了解成是 机器内存中信息的表达方式。
为什么须要序列化
通常状况下,咱们的语言一方面用于交换,比方聊天,把我脑海中的思维,通过语言表达进去,对方听到咱们的话语,会心咱们的想法。
另一方面,咱们的语言除了用于沟通交流,还能够用于记录。有一句话叫做『好忘性不如烂笔头』。说的就是记录的重要性,因为话在咱们的脑子里,很容易就忘了,通过记录下来能够保留更久。
而序列化性能又正好对应这两点,一个是用来传输信息,另一个是用来长久化。序列化用来传输的作用,前文曾经说过了,对于长久化的作用,也很好了解。首先明确一个问题,序列化的是什么内容?通常是内存中的内容。而内存有一个特点咱们都晓得,那就是一重启就没了。对于局部内容,咱们想在重启后还存在(比如说 tomcat 中 session 外面的对象),要怎么办呢?答案就是把内存中的对象保留到磁盘上,这样就不怕重启了,而长久化就须要用到序列化技术。
如何实现序列化
人与人之间有许许多多的表达方式,而且机器与机器之间也同样,序列化的形式多种多样。
Java 原生模式
对于如此广泛的序列化需要,Java 其实早在 JDK 1.1 开始就在语言层面进行了反对。而且应用起来十分不便,上面咱们就一起看看具体代码。
- 首先咱们要把想序列化的类实现 Java 自带的
java.io.Serializable
接口
/*
*
* * *
* * * blog.coder4j.cn
* * * Copyright (C) 2016-2020 All Rights Reserved.
* *
*
*/
package cn.coder4j.study.example.serialization;
import java.io.Serializable;
import java.util.StringJoiner;
/**
* @author buhao
* @version HaveSerialization.java, v 0.1 2020-09-17 16:58 buhao
*/
public class HaveSerialization implements Serializable {
private static final long serialVersionUID = -4504407589319471384L;
private String name;
private Integer age;
/**
* Getter method for property <tt>name</tt>.
*
* @return property value of name
*/
public String getName() {return name;}
/**
* Setter method for property <tt>name</tt>.
*
* @param name value to be assigned to property name
*/
public void setName(String name) {this.name = name;}
/**
* Getter method for property <tt>age</tt>.
*
* @return property value of age
*/
public Integer getAge() {return age;}
/**
* Setter method for property <tt>age</tt>.
*
* @param age value to be assigned to property age
*/
public void setAge(Integer age) {this.age = age;}
@Override
public String toString() {return new StringJoiner(",", HaveSerialization.class.getSimpleName() + "[", "]")
.add("name='" + name + "'")
.add("age=" + age)
.toString();}
}
须要留神的是,虽说是实现了 java.io.Serializable
接口,然而咱们其实没有笼罩任何办法。这是为什么呢?咱们一起看一下 java.io.Serializable
的源码。
public interface Serializable {}
没错,是个空接口,除了接口定义局部,啥也没有。通常遇到这种状况,咱们称之为标记接口,次要为了标记某些类,标记的起因是,把它与其它类区别进去,不便咱们前面专门解决。而 Serializable 这个标记接口,就是为了让咱们晓得这个类是要进行序列化操作的类,仅此而已。
另外,尽管咱们只实现一个空接口,然而仔细的你,必定发现了咱们的类中多了一个 serialVersionUID 属性。那么这个属性的作用是什么呢?
它次要目标就是为了验证序列化与反序列化的类是否统一。比方下面 HaveSerialization
这个类当初有业务属性 name
与 age
,当初因为业务须要,咱们要增加一个 address
的属性。序列化操作是没有问题的,然而把序列化信息传输给其它机器,其它机器在反序列化的时候,就呈现了问题。因为其它机器的 HaveSerialization
没有 address
这个属性。
为了解决这个问题,JDK 通过应用 serialVersionUID 在作为该类的版本号,在反序列化时比拟传输的类的值与要反序列化类的值是否统一,不统一就会报 InvalidCastException。
当然,出发点是好的,然而间接抛异样会导致业务无奈进行上来,通常 serialVersionUID 生成好后,咱们不会再更新,序列化如果没有更新,对应变更的属性会为空,咱们只有在业务里做好兼容就好了。
- 序列化对象
好了,咱们曾经实现了第一步,定义了一个序列化类,上面咱们就把他给序列化掉。
/**
* 序列化对象(保留序列化文件)* @throws IOException
*/
@Test
public void testSaveSerializationObject() throws IOException {
// 创建对象
final HaveSerialization haveSerialization = new HaveSerialization();
haveSerialization.setName("kiwi");
haveSerialization.setAge(18);
// 创立序列化对象保留的文件
final File file = new File("haveSerialization.ser");
// 创建对象输入流
try (final ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file))) {
// 将对象输入到序列化文件
objectOutputStream.writeObject(haveSerialization);
}
}
能够看到代码非常简略,大体分成如下 4 步:
- 创立要序列化的对象
其实就是你下面的实现java.io.Serializable
的类,如果没有实现,在这里会报NotSerializableException
异样 - 创立一个
File
对象,用来保留序列化后的二进制数据。
留神这里文件名我用的是*.ser
,这个 ser 后缀并没有强制要求,只是不便了解,你可能写成其它后缀 - 创建对象输入流
创立一个ObjectOutputStream
对象输入流的对象,并把下面定义的序列化文件对象通过构造函数传给它。 - 通过输入流把对象写到序列化文件里
留神这里我用的 JDK 8 的try with resource
语法,所以不必手动close
好了,到这里咱们序列化也实现了。
- 反序列化对象
既然有序列化,那必定也有反序列化。反序列化能够了解成是序列化的逆向操作,既然序列化把内存中的对象转成一个能够长久化的文件,那么反序列化要做的就是把这个文件再加载到内存中的对象。话不多说,间接看代码。
/**
* 反序列化对象(从序列化文件中读取对象)* @throws IOException
* @throws ClassNotFoundException
*/
@Test
public void testLoadSerializationObject() throws IOException, ClassNotFoundException {
// 创建对象输入流
try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("haveSerialization.ser")))) {
// 从输入流中创建对象
final Object obj = objectInputStream.readObject();
System.out.println(obj);
}
}
是的,反序列化代码比序列化代码还少,次要分成如下 2 步:
- 创建对象输出流
创立一个ObjectInputStream
对象,并把序列化文件通过构造函数传给它 - 从对象输出流中读取对象
间接通过readObject
办法即可,留神读取后是Object
类型,后续应用需手动强转一次
到这里,咱们便通过 JDK 原生的办法实现了序列化与反序列化操作,是不是还很简略。然而日常工作不太举荐间接应用原生的形式实现序列化,一方面它生成的序列化文件较大,一方面也比一些第三方框架生成的慢,然而序列化原理大抵相似。上面咱们简略看一下其它形式如何序列化。
通用对象序列化
通常序列化是与语言绑定的,比如说通过下面 JDK 序列化的文件,不可能拿给 PHP 利用反序列化成 PHP 的对象。不过能够通过某些非凡的通用对象构造序列化来实现跨语言应用,比拟常见的是 JSON、XML。上面咱们以 JSON 为例看一下
/**
* 测试序列化通过 json
*/
@Test
public void testSerializationByJSON(){
//------------- 序列化操作 ---------------
// 创建对象
final HaveSerialization haveSerialization = new HaveSerialization();
haveSerialization.setName("kiwi");
haveSerialization.setAge(18);
// 序列化成 JSON 字符串
final String jsonString = JSON.toJSONString(haveSerialization);
System.out.println("JSON:" + jsonString);
//------------- 反序列化操作 ---------------
final HaveSerialization haveSerializationByJSON = JSON.parseObject(jsonString, HaveSerialization.class);
System.out.println(haveSerializationByJSON);
}
运行后果:
JSON:{"age":18,"name":"kiwi"}
HaveSerialization[name='kiwi', age=18]
上述代码应用的 JSON 框架是 alibaba/fastjson。然而大部分 JSON 框架应用起来都大同小异。能够按集体爱好去替换。
序列化框架
序列化框架其实有很多,比方 kryo
、hessian
、protostuff
。它们各有优缺点,具体的比拟能够看这篇文章 序列化框架 kryo VS hessian VS Protostuff VS java。大家能够按各自的应用场景抉择应用,下文以 kryo
为例演示。
- 依赖
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.0.0-RC9</version>
</dependency>
- 具体代码
/**
* 测试序列化通过 kryo
*/
@Test
public void testSerializationByKryo() throws FileNotFoundException {
//------------- 序列化操作 ---------------
// 创建对象
final HaveSerialization haveSerialization = new HaveSerialization();
haveSerialization.setName("kiwi");
haveSerialization.setAge(18);
final Kryo kryo = new Kryo();
// 注册序列化类
kryo.register(HaveSerialization.class);
// 序列化操作
try (final Output output = new Output(new FileOutputStream("haveSerialization.kryo"))) {kryo.writeObject(output, haveSerialization);
}
// 反序列化
try (final Input input = new Input(new FileInputStream("haveSerialization.kryo"))) {final HaveSerialization haveSerializationByKryo = kryo.readObject(input, HaveSerialization.class);
System.out.println(haveSerializationByKryo);
}
}
其实看代码能够发现跟 JDK 的流程简直一样,其中有几点须要留神的,kryo
在序列化前,要手动通过 register
注册序列化的类,有点相似 JDK 实现 java.io.Serializable
接口。而后 Input
、Output
对象不是 JDK 的。是 kryo
提供的。另外 Kryo
有不少须要留神的中央,能够查看参考链接局部的内容学习。
源码地址
因文章篇幅无限,无奈展现所有代码,曾经另外把残缺代码上传到 github,具体链接如下:
https://github.com/kiwiflydream/study-example/tree/master/study-serialization-example
参考链接
- 序列化框架 kryo VS hessian VS Protostuff VS java – 知其然,知其所以然 – ITeye 博客
- Kryo 使用指南 – hntyzgn – 博客园
- 深刻了解 RPC 之序列化篇 –Kryo | 徐靖峰 | 集体博客
- EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic
总结
本文次要介绍了 Java 序列化的相干内容,次要介绍序列化是什么?与人与人之间沟通的表达方式做类比,失去是 机器内存中信息的表达方式 。而为什么须要序列化,咱们通过举例说明了序列化 信息传输与长久化 的性能。最初咱们一起从 JDK 原生的实现 java.io.Serializable 的形式,再到通用对象序列化的 JSON、XML 形式,最终到第三方框架 kryo 的模式理解如何去实现序列化。