「一探究竟」迷之序列化,Java 性能优化最佳实践
服务端不可随意在任意位置增加字段,因为客户端不升级的话会导致反序列化失败
不能使用第三方包提供的集合类工具包作为返回值
使用方式如下:
// 其中 StuDemo 类需要增加 @Message 注解标识需要被 MessagePack 序列化
// MessagePack 序列化方式不需要依赖 Serializable
public static void main(String[] args) throws IOException {
StuDemo demo = new StuDemo("Kerwin");
MessagePack pack = new MessagePack();
// 序列化
byte[] bytes = pack.write(demo);
// 反序列化
StuDemo res = pack.read(bytes, StuDemo.class);
System.out.println(res.getName());
}
复制代码
PS:我司的 RPC 框架目前就使用的 MessagePack 序列化方式,也是因为此,所以上述调整 serialVersionUID 时没有发生任何问题 同理,受制于底层序列化的限制,我们的新人文档中也明确提到了上述的限制,比如必须在最末尾增加字段等等。
Hessian2 序列化
Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架,在 Hessian 的基础之上,Hessian2 的性能和压缩率大大提升。
Hessian 会把复杂的对象所有属性存储在一个类似 Map 的结构中进行序列化,所以在父类、子类中存在同名成员变量的情况下,它先序列化子类,然后序列化父类,因此会导致子类同名成员变量的值被父类覆盖等情况。
它有八大核心设计目标,官网:
必须自我描述序列化类型,即不需要外部模式或接口定义
必须与语言无关,包括支持脚本语言
必须在一次传递中可读或可写
必须尽可能紧凑(压缩)
必须简单
必须尽可能快
必须支持 Unicode 字符串
必须支持 8 位二进制数据
必须支持加密
使用方式如下:
public class StuHessianDemo implements Serializable {
private static final long serialVersionUID = -640696903073930546L;
private String name;
public StuHessianDemo(String name) {
this.name = name;
}
public String getName()
{
return name;
}
public void setName(String name) {
this.name = name;
}
}
复制代码
public static void main(String[] args) throws IOException {
StuHessianDemo hessianDemo = new StuHessianDemo("Kerwin");
ByteArrayOutputStream stream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(stream);
hessianOutput.writeObject(hessianDemo);
ByteArrayInputStream inputStream = new ByteArrayInputStream(stream.toByteArray());
// Hessian 的反序列化读取对象
HessianInput hessianInput = new HessianInput(inputStream);
System.out.println(((StuHessianDemo) hessianInput.readObject()).getName());
}
// 结果:Kerwin
复制代码
选择的依据
由上文我们得知了几种常用的序列化方式,及其优劣,比如 MessagePack 就是极致的压缩和快,Hessian2 则依赖 Serializable 接口,在保证安全性、自身描述性的基础上,尽可能的追求空间利用率,效率等,而 Java 序列化方式则一直被诟病,难等大雅之堂,因此在 RPC 框架选择底层序列化方式时,需要根据自身所需,有所侧重的选择某一项序列化方式。
选择的依据如下,优先级从高到低:
一点思考
JSON 序列化的地位
其实 JSON 序列化才是我们最熟知的序列化方式,它本身也不需要实现 Serializable 接口,为什么大多数 RPC 框架没有选择用它作为默认的序列化方式呢?
在了解完上文的内容后,我们知道关键还是在性能,效率、空间开销上,因为 JSON 是一种文本类型序列化框架,采用 KEY-VALUE 的方式存储数据,它在进行序列化的额外空间开销相对就更大,在反序列化时更不必说,需要依赖反射,因此性能进一步缩水。
然而 JSON 本身又具备极强的可读性、因此被作为 Web 中 HTTP 协议的事实标准。
为什么还要自定义 serialVersionUID
在《Effect Java》中有一句提到:
不管你选择了哪种序列化方式,都要为自己编写的每个可序列化的类声明一个显式的序列版本 UID。
为什么架构师会提醒我实现它?为什么书中也会这么说?
serialVersionUID 分解下来全称为:serial Version UID,序列版本 UID,每一个可序列化的类都有一个 long 域中显式地指定该编号,如果编码者未定义的话,系统就会对这个类的结构运用一个加密的散列函数(SHA-1),从而在运行时自动产生该标识号,该编号会受类名称、接口名称、公有及受保护的成员变量所影响,一旦有相关改动例如增加一个不重要的公有方法即会影响 UID,导致异常发生。
因此这是一个习惯问题,也是为了避免潜在风险。
总结
--
截止到这里,我们了解了原来之前学习到的 Java 序列化是那么的不实用(甚至到了被吐槽的地步),也知晓了一些框架使用注意事项底层的秘密(比如 MsgPack 增加字段),下面是关于序列化的一些小建议:
无论是否依赖
Serializable
,接口出参都建议实现序列化接口。如果实现了序列化接口,务必自行实现 serialVersionUID。
接口出参对象不宜使用特殊的数据类型(如 MsgPack 第三方集合等)、过于复杂的结构(继承等),不然会导致很多莫名其妙的问题发生。
当发生服务端/客户端数据不一致时,第一时间想到是序列化问题,并针对当前序列化方式的特点,仔细排查。
如果觉得这篇内容对你有帮助的话:
当然要点赞支持一下啦~
参考资料
评论