# 序列化

在前面我们已经讲了，Java分布式通讯基于Socket，使用Socket通讯必须指定以下关键点：

* IP
* Port
* 通讯协议

其中很多通讯协议通常与RPC框架有关联，但是比较通用的，比如HTTP协议，这里就不再过多解释。关于通讯协议的部分，将会结合着RPC框架来进行讲解。本篇文章将会简单描述几种常用的序列化的使用方式，着重比较他们之间的序列化与反序列化效率。

通讯协议中不仅包含协议的内容，更是包含了我们通讯的数据。我们的数据在传输之前，位于Java代码中都是以JavaObject的形式表示的，因此这些数据在网络传输时，不可避免序列化、反序列化的过程。恰当的序列化方式不仅可以提高系统的性能，更是可以提高系统的安全性、通用性、强壮性，让系统易于调试和拓展。

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-6000db09e49a2477b5346a8a23f687956c2fc52b%2F20190426171040775_8597.png?alt=media)

## 常见的序列化机制

| 序列化方式，时间排序      | 优点                                                        | 缺点                       | 主要应用             |
| --------------- | --------------------------------------------------------- | ------------------------ | ---------------- |
| Serializable    |                                                           | 序列化结果大、传输效率低、不能跨语言对接     | RMI              |
| XML             | 通俗易懂、跨语言                                                  | 序列化结果大、冗余标签多、传输效率低       | WebService(SOAP) |
| JSON            | 通俗易懂、比起xml更精简，与js兼容好                                      | 序列化结果仍然很大、性能较低           | HTTP Rest        |
| Hessian         | 跨语言、紧凑的二进制协议、序列化速度快                                       | 序列化后的结果很大，大于Java序列化、可读性差 |                  |
| MessagePack     | 兼容 json 的数据格式、但是比JSON跟节省空间、支持多语言                          | 序列化时间长大约是JSON的两倍         |                  |
| Protocol Buffer | 吊打上面几个                                                    | 需要预编译、安装 比较麻烦            |                  |
| Apache Thrift   | 与Protocol Buffers差不多、序列化时间要比Protocol Buffers短、反序列化时间要比PB长 |                          |                  |
| Apache Avro     |                                                           | 配置复杂                     |                  |
| Kryo            | 针对Java的序列化系统                                              | 不跨语言                     |                  |
| FST             | 针对Java的序列化系统                                              | 不跨语言                     |                  |

## Serializable

Serializable 是Java自带的序列化方式，速度慢，不跨语言，唯一的优点可能就是Java自带，不需要引入任何第三方扩展。`Serializable`的序列化与发序列化需要借助`ObjectInputStream`和`ObjectOutputStream`，下面是代码演示：

```java
@Test
public void testSerialize() throws Exception {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));
    oos.writeObject(Person.getInstance());
}

@Test
public void testDeSerialize() throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));
    Person p = (Person) ois.readObject();
    System.out.println(p.getName());
}
```

### serialVersionUID

凡是实现`Serializable`接口的类，都需要声明 `serialVersionUID` 来标识序列化版本号。当使用版本号1进行序列化时，只能通过相同的版本号的类进行反序列化，否则就会抛出`java.io.InvalidClassException`。以此保证序列化类型与反序列化类型是同一类型。

如果未指定`serialiVersionUID`，Java 编译器会自动对类进行摘要算法生成一个`serialVersionUID`，所以只要目标Java类有任何变动，得到的结果就会截然不同。

### 静态变量序列化

**序列化并不保存静态变量的状态**，所以序列化时静态变量 `a` 的值为2，那么反序列化时a的值不见的就是2，他的值仍然是 `类.a` 的值

### transient 关键字

如果想要某个属性不参与序列化，可以给这个属性加上`transient`关键字。

### 父子类序列化问题

* 如果父类没有实现序列化接口、子类实现了序列化接口，那么父类中的属性无法参与序列化
* 如果父类实现了序列化接口，子类没有实现，子类继承父类，所以父子类属性都会参与序列化

### 序列化的存储规则

假设将`Person p` 对象序列化写入到文件`p.bak`中，第一次假设耗费15ms的时间写入到了磁盘中，那么第二次写入同样的p对象，此时Java发现磁盘中已经存在此序列化文件，那么Java不会覆盖文件，而是在文件尾部添加了5个字节的引用关系，从而提高了重复对象的写入效率。下面是代码示例：

```java
@Test
public void testTwiceSerialize() throws Exception {
    Person person = Person.getInstance();

    File personFile = new File("person");

    ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream(personFile));
    oos1.writeObject(person);
    oos1.flush();
    System.out.println("第一次写入对象的");

    oos1.writeObject(person);
    oos1.flush();

    ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("person2"));
    oos2.writeObject(person);

    // 进行字节流比对
    File person1File = new File("person1");
    File person2File = new File("person2");
    // 两次同时写入，不会覆盖，也不会完全追加，而是会在原先的基础上增加一个引用地址；原因：增加效率

    System.out.println("person1: " + person1File.length());
    System.out.println("person2: " + person2File.length());
}
```

### 深度克隆

使用Java Serializable 还可以实现深克隆，只需要保证要克隆的对象以及属性都实现了Serializable接口即可，复制出的对象与原有对象是两个对象。代码这里不再演示。

## JSON

JSON序列化方式是Java应用程序中最为常用的序列化方式，与Javascript有较好的兼容性，最典型的就是用在Rest架构中。JSON序列化方式可以使用的第三方jar有很多，比如最常用的，也是SpringBoot中自带的JSON序列化方式：Jackson，以及Alibaba的用起来很方便的Fastjson，以及Android中常用的Gson。下面是三种工具的使用方式。

### Jackson

感觉Jackson用起来不是很方便，使用Jackson需要创建一些前置对象，下面是他最简单的序列化反序列化方式：

```java
// 序列化
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsBytes(data);

// 反序列化
Person p = mapper.readValue(writeBytes, Person.class);
```

### Fastjson

Fastjson是开发中公司用的较多的JSON框架，用起来比较方便，而且速度也很快，下面是他的示例代码：

```java
// 序列化
JSON.toJSONBytes(data);

// 反序列化
Person p = JSON.parseObject(bytes, Person.class);
```

### Gson

Gson是Google的一款JSON序列化框架，使用起来也是比较方便的，笔者是很久之前还在学习Android的时候接触的，算是接触的最早的JSON框架，但是对其使用并不是很了解：

```java
// 序列化
Gson gson = new Gson();
gson.toJson(p);

// 反序列化
Gson gson = new Gson();
Person person = gson.fromJson(json, Person.class);
```

## Protocol Buffer

Protocal Buffer是Google提供的一种数据序列化协议，是一种轻便高效的结构化数据存储格式，适合作为数据存储或者RPC数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

实现一个Protocol Buffer较为复杂，这里使用百度提供的工具包 `jprotobuf`：

```xml
<dependency>
    <groupId>com.baidu</groupId>
    <artifactId>jprotobuf</artifactId>
    <version>2.2.7</version>
</dependency>
```

代码演示：

```java
public class Person {
    @Protobuf(fieldType = FieldType.STRING)
    private String name;
    @Protobuf(fieldType = FieldType.INT32)
    private Integer age;
    // setter / getter
}

// 序列化
Codec<Person> personCodec = ProtobufProxy.create(Person.class, false);
byte[] data = personCodec.encode(p);
// 反序列化
Person decode = personCodec.decode(data);
```

### protobuf性能高有两个主要原因

* 针对字节数据进行了压缩，所以生成的数据较小，所以传输效率较高
* 针对数据进行了缓存

## Hessian

Hessian序列化的方式同样是跨语言的，是一种二进制的序列化方式。值得一提的是，Hessian序列化的速度很快，但是序列化的结果所占的字节数却很大。

```java
// 序列化
ByteArrayOutputStream os = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(os);
ho.writeObject(p);
// 反序列化
HessianInput hi = new HessianInput(new ByteArrayInputStream(os.toByteArray()));
Person o = (Person) hi.readObject();
```
