什么是序列化和反序列化?
如果我们需要持久化Java对象比如将Java对象保存在文件中,或者在网络传输Javad对象,这些场景都需要用到序列化。
简单来说:
序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是JSON,XML等文本格式。
反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程。
对于Java这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而class对应的是对象类型。
下面1是序列化和反序列化常见应用场景:
对象在进行网络传输(比如远程方法调用RPC的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
将对象存储到数据库(如Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
将对象存储到内存之前需要序列化,从内存中读取出来之后需要进行反序列化
维基百科是如是介绍序列化的:
序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可以取用格式(例如存成文件,存于缓存,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。
依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。
对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。
面向对象中的对象序列化,并不概括之前原始对象所关系的函数。
这种过程也称为对象编组(marshalling).
从一系列字节提前数据结构的反向操作,是反序列化(也称为解编组,deserialization,unmarshalling)
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统,数据库,内存中。
序列化协议对应于TCP/IP4层模型的那一层?
我们知道网络通信的双方必须要采用和遵守相同的协议。
TCP/IP四层模型是下面这样的,序列化协议属于哪一层呢?
1.应用层
2.传输层
3.网络层
4.网络接口层
TCP/IP四层模型
如上图所示,OSI七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。
反过来的话,就是将二进制流转换成应用层的用户数据。
这不就对应的是序列化和反序列化么?
因为,OSI七层协议模型中的应用层,表示层会化层对应的多少TCP/IP四层模型中的应用层,所以序列化协议属于TCP/IP协议应用层的一部分、
常见序列化协议有那些?
JDK自带的序列化方式一般不会用,因为序列化效率低并且存在安全问题。
比较常用的序列化协议有Hessian,Kryo,Protobuf,ProtoStuff,这些都是基于二进制的序列化协议。
像JSON和XML这种属于文本类序列化方式。
虽然可读性比较好,但是性能较差,一般不会选择。
JDK自带的序列化方式
JDK自带的序列化,只需实现java.io.Serializable接口即可。
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
private String requestId;
private String integfaceName;
private String methodName;
private Object[] parameters;
private Class<?>[] paramTypes;
private RpcMessageTypeEnum rpcMessageTypeEnum;
}
serialVersionUID有什么作用?
序列化后serialVersionUID属于版本控制的作用。
反序列化时,会检查serialVersionUID是否和当前类的serialVersionUID一致。
如果serialVerisionUID不一致则会抛出InvalidClassException异常。
强烈推荐每个序列化类都手动指定其serialVersionUID,如果不手动指定,那么编译器会动态生成默认的serialVersionUID.
serialVersionUID不是被static变量修饰了吗?
为什么还会被“序列化”?
static修饰的变量是静态变量,属于类而非类的实例,本身是不会被序列化的。
然而,serialVersionUID是一个特例,serialVersionUID的序列化做了特殊处理。
当一个对象被序列化时,serialVersionUID会被写入到序列化的二进制流中;在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。
如果两者不匹配,反序列化过程将抛出InvalidClassException,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。
官方说明如下:
A serializable class can declare its own serialVersionUID explicitly by declaring a field named "serialVersionUID" that must be static,final,and of type long;
如果想显示指定serialVersionUID,则需要在类中使用static和final关键字来修饰一个long类型的变量,变量名必须为“serialVersionUID”
也就是说,serialVersionUID只是用来被JVM识别,实际并没有被序列化。
如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,可以使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的序列化;
当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。
关于transient还有几点注意:
transient只能修饰变量,不能修饰类和方法。
transient修饰的变量,在反序列化后变量值将被置成类型的默认值。
例如,如果是修饰int类型,那么反序列后结果就是0.
static变量因为不属于任何对象(Object),所以无论有没有transient关键字修饰,均不会被序列化。
为什么不推荐使用JDK自带的序列化?
我们很少或者几乎不会直接使用JDK自带的序列化方式,主要原因有下面这些原因:
不支持跨语言调用:如果调用的是其他语言开发的服务的时候就不支持了。
性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积过大,导致传输成本加大。
存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
相关阅读:应用安全:JAVA反序列化漏洞之殇-Cryin
Java反序列化安全漏洞怎么回事?-Monica
Kryo
Kryo是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用字节码生成机制,拥有较高的运行速度和较小的字节码体积。
另外,Kryo已经是一直非常成熟的序列化实现了,已经在Twitter,Groupon,Yahoo以及多个著名开源项目(如Hive,Storm)中广泛的使用。
guide-rpc-framework就是使用kryo进行序列化,反序列化相关的代码如下:
/**
* Kryo serialization class,Kryo serialization efficiency is very high,but only compatible * with Java language
*
* @author shuang.kou
* @createTime 2020年05月13日 19:29:00
*/
@Slf4j
public class KryoSerializer implements Serializer {
/**
* Because Kryo is not thread safe.So,use ThreadLocal to store Kryo object
*/
private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kry();
kryo.register(RpcResponse.class);
kryo.register(RpcRequest.class);
return kryo;
});
@Override
public byte[] serialize(Object obj) {
try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Outpur(byteArrayOutputStream)){
Kryo kryo = kryoThreadLocal.get();
// Object->byte:将对象序列化为byte数组
kryo.writeObject(output,obj);
kryoThreadLocal.remove();
return output.toBytes();
} catch(Exception e){
throw new SerializeException("Serialization failed");
}
}
@Override
public <T> T deserialize(byte[] bytes,Class<T> clazz) {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream)) {
Kryo kryo = kryoThreadLocal.get();
// byte —>Object:从byte数组中反序列化出对象
Object o = kryo.readObject(input,clazz);
kryoThreadLocal.remove();
return clazz.cast(o)
} catch (Exception e) {
throw new SerializeException("Deserialization failed");
}
}
}
GitHub地址:https://github.com/EsotericSoftware/kryo
Protobuf
Protobuf出自于Google,性能还比较优秀,也支持多做语言,同时还是跨平台的。
就是在使用中过于繁琐,因为你需要自己定义IDL文件和生成对应的序列化代码。
这样虽然不灵活,但是,另一方面导致protobuf没有序列化漏洞的风险。
Protobuf包含序列化格式的定义,各种语言的库以及一个IDL编译器。
正常情况下你需要定义proto文件,然后使用IDL编译器编译成你需要的语言。
一个简单的proto文件如下:
// protobuf的版本
syntax = "proto3";
// SearchRequest会被编译成不同的编程语言的相应对象,比如Java中的class,Go中的struct
message Person {
//string类型字段
string name = 1;
// int 类型字段
int32 age = 2;
}
Github地址:https://github.com/protocolbuffers/protobuf.
Protostuff
由于Protobuf的易用性较差,它的哥哥Protostuff诞生了。
protostuff基于Google protobuf,但是提供了更多的功能和更简易的用法。
虽然更加易用,但是不代表ProtoStuff性能更差。
Github地址:https://github.com/protostuff/protostuff
Hessian
Hessian是一个轻量级的,自定义描述的二进制RPC协议。
Hessian是一个比较老的序列化实现,并且同样也是跨语言的。
Dubbo2.x默认启用的序列化方式是Hessian2,但是Dubbo对Hessian2进行了修改,不过大体结构还是差不多。
总结
Kryo是专门针对Java语言序列化方式并且性能非常好,如果你的应用是专门针对Java语言的话可以考虑使用,并且Dubbo官方的一篇文章中提到说推荐使用Kryo作为生产环境的序列化方式。
(文章地址:https://cn.dubbo.apache.org/zh-cn/docsv2.7/user/serialization/)
像Protobuf,ProtoStuff,hessian这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。
除了我上面介绍到的序列化方式,还有像Thrift,Avro这些。