Java 序列化详解
什么是序列化和反序列化
如果需要持久化 Java 对象比如将 Java 对象保存在文件、或者保存在NoSQL数据中、或者在网络上传输 Java 对象。则这些场景都需要用到序列化。
- 序列化: 将数据结构或对象转换成二进制字节流的过程。
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class)。
对于 C++ 这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
常见序列化和反序列化定义场景
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;存入之前通常反序列化为 JSON,取出之前将 JSON 反序列化 为 Class 对象
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
序列化在网络模型中的位置
OSI网络分层作用序列化相关TCP/IP网络分层应用层为计算机用户提供服务应用层(在TCP/IP)中,序列化属于应用层的一部分表示层数据处理(编解码,加密解密,压缩等)序列化相关层会话层管理(建立,维护)应用程序之间的会话传输层为两台主机进程之间的通信提供通用的数据传输服务传输层网络层路由和寻址(决定数据在网络中的传输路径)网络层(网际层)数据链路层帧编码和误差纠正控制网络接口层物理层透明地址传送,比特流传输
常见的序列化协议
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有
Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
JDK序列化
JDK自带的序列化方式,只需要实现 java.io.Serializable 接口即可。
@AllArgsConstructor@NoArgsConstructor@Getter@Builder@ToStringpublicclassRpcRequestimplementsSerializable{privatestaticfinallong serialVersionUID =1905122041950251207L;privateString requestId;privateString interfaceName;privateString methodName;privateObject[] parameters;privateClass<?>[] paramTypes;privateRpcMessageTypeEnum rpcMessageTypeEnum;}
serialVersionUID作用
序列化号
serialVersionUID
起版本控制作用。反序列化时,会检查
serialVersionUID
是否和当前类的
serialVersionUID
一致。如果
serialVersionUID
不一致则会抛出
InvalidClassException
异常。强烈推荐每个序列化类都手动指定其
serialVersionUID
,如果不手动指定,那么编译器会动态生成默认的
serialVersionUID
。
某些字段不参与序列化
对于不想序列化的字段变量,可以使用
transient
关键字修饰。
transient
关键字的作用是:
阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被
transient
修饰的变量值不会被持久化和恢复。
关于
transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
serialVersionUID 不是被 static 变量修饰了吗?为什么还会被“序列化”???
static
修饰符修饰的是静态变量,位于方法区中,本身是不会被序列化的。
static
变量是属于类的而不是对象。反序列之后,
static
变量的值就像是默认赋予给了对象一样,看着就像是
static
变量被序列化,实际只是假象罢了。
不推荐使用JDK序列化的原因
在业内很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
- 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
- 相关阅读:应用安全:JAVA反序列化漏洞之殇 - Cryinopen in new window 、Java反序列化安全漏洞怎么回事? - Monicaopen in new window。
其他序列化方式
Protobuf(参考Seata提供的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/protobufopen in new window。
Kryo
Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。
另外,Kryo 已经是一种非常成熟的序列化实现了,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。
guide-rpc-frameworkopen in new window 就是使用的 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
*/@Slf4jpublicclassKryoSerializerimplementsSerializer{/**
* Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects
*/privatefinalThreadLocal<Kryo> kryoThreadLocal =ThreadLocal.withInitial(()->{Kryo kryo =newKryo();
kryo.register(RpcResponse.class);
kryo.register(RpcRequest.class);return kryo;});@Overridepublicbyte[]serialize(Object obj){try(ByteArrayOutputStream byteArrayOutputStream =newByteArrayOutputStream();Output output =newOutput(byteArrayOutputStream)){Kryo kryo = kryoThreadLocal.get();// Object->byte:将对象序列化为byte数组
kryo.writeObject(output, obj);
kryoThreadLocal.remove();return output.toBytes();}catch(Exception e){thrownewSerializeException("Serialization failed");}}@Overridepublic<T>Tdeserialize(byte[] bytes,Class<T> clazz){try(ByteArrayInputStream byteArrayInputStream =newByteArrayInputStream(bytes);Input input =newInput(byteArrayInputStream)){Kryo kryo = kryoThreadLocal.get();// byte->Object:从byte数组中反序列化出对象Object o = kryo.readObject(input, clazz);
kryoThreadLocal.remove();return clazz.cast(o);}catch(Exception e){thrownewSerializeException("Deserialization failed");}}}
Github 地址:https://github.com/EsotericSoftware/kryoopen in new window 。
总结
在序列化中,Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。像 Protobuf、 ProtoStuff、hessian 这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。
版权归原作者 yuluo_YX 所有, 如有侵权,请联系我们删除。