目录
2.1.1 Externalizable与Serializable的异同
1、序列化与反序列化的作用
序列化:将堆中对象转化成可以存储或网络传输的二进制字节序列的过程。
反序列化:将来自于网络传输或者存储的字节序列恢复成Java对象的过程。
当Java对象需要进行本地文件存储时,需要将Java对象进行序列化;
当Java对象需要进行网络传输,如 rpc调用、数据库持久化,需要将Java对象进行序列化;
参考文档:JAVA基础4---序列化和反序列化深入整理(JDK序列化)
2、序列化协议
2.1 JDK序列化协议
下面是JDK序列化的一个示例:
package com.autocoding.serializable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class JdkSerializer {
public static void serialize(Student student) throws FileNotFoundException, IOException {
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("D:\\student.txt")));
oo.writeObject(student);
oo.close();
}
public static Student deserialize() throws IOException, Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\student.txt")));
Student student = (Student) ois.readObject();
return student;
}
public static void main(String[] args) {
Student student = new Student();
student.setId("1000");
student.setName("李桥");
try {
// 序列化
JdkSerializer.serialize(student);
// 反序列化
Student serializeStudent = JdkSerializer.deserialize();
System.out.println(serializeStudent.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
private static class Student implements Serializable {
private static final long serialVersionUID = 8888L;
private String id;
private String name;
//构造器访问控制符为私有的,埋下一个伏笔
privateStudent() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
下面是序列化到文件 D:\\student.txt 的内容:
比较粗略的可以看到序列化之后,存储了对象的类型信息,字段的类型信息。
2.1.1 Externalizable与Serializable的异同
Externalizable接口扩展自java.io.Serializable接口。实现java.io.Serializable即可获得对类的对象的序列化功能。而Externalizable可以通过writeExternal()和readExternal()方法可以指定序列化哪些属性。
- 序列化内容
Externalizable自定义序列化可以控制序列化的过程和决定哪些属性不被序列化。 - Serializable反序列化时不会调用默认的构造器,而Externalizable序列化时会调用默认Public无参构造器的
- 使用Externalizable时,必须按照写入时的确切顺序读取所有字段状态。否则会抛出java.io.EOFException。而Serializable接口没有这个要求。
参考文档:Externalizable的用法及与Serializable的异同
2.2 Google ProtocolBuf 协议
ProtocolBuff是谷歌推出的一种序列化协议,是一个非常高效的序列化协议。相对于Java这种数据类型固定长度的序列化(int 4字节, long 8字节), PB提供了可伸缩性的数据类型(int 1-5字节)。
当数据比较小的时候int只占用1个字节, 而大部分场景下, 数据都是很小的, 不然jdk本身也不会缓存 -127~128了。
参考文档:Protocol Buffer序列化对比Java序列化.
性能最好的序列化反序列化,Protobuf的用法
优点:1、序列化后码流小,性能高
2、通过标识字段的顺序,可以实现协议的前向兼容
3、结构化数据存储格式(XML JSON等),结构化的文档更容易管理和维护
缺点:1、需要依赖于工具生成代码
2、支持的语言相对较少,官方只支持Java 、C++ 、Python
3、序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息
使用场景: 对性能要求高的RPC调用。
Google 的ProtocolBuff不好用,需要自己编码.proto文件,现在推荐比较好用的类库Baidu的JProtoBuf。
jprotobuf是针对Java程序开发一套简易类库,目的是简化java语言对protobuf类库的使用使用jprotobuf可以无需再去了解proto文件操作与语法,直接使用java注解定义字段类型即可。
Github: https://github.com/jhunters/jprotobuf
2.3 ProtoStuff 协议
protostuff是一个基于protobuf实现的序列化协议,它较于protobuf最明显的好处是,在几乎不损耗性能的情况下做到了不用我们写.proto文件来实现序列化。
代码示例:
package com.autocoding.serializable;
import java.util.Arrays;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtobufIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
public class ProtoStuffSerializer {
public static <T> byte[] serialize(T t, Class<T> clazz) {
return ProtobufIOUtil.toByteArray(t, RuntimeSchema.createFrom(clazz),
LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
}
public static <T> T deserialize(byte[] data, Class<T> clazz) {
RuntimeSchema<T> runtimeSchema = RuntimeSchema.createFrom(clazz);
T t = runtimeSchema.newMessage();
ProtobufIOUtil.mergeFrom(data, t, runtimeSchema);
return t;
}
public static void main(String[] args) {
Student student = new Student();
student.setId("1000");
student.setName("李桥");
try {
// 序列化
byte[] bytes = ProtoStuffSerializer.serialize(student, Student.class);
System.out.println("序列化之后:" + Arrays.toString(bytes));
// 反序列化
Student serializeStudent = ProtoStuffSerializer.deserialize(bytes, Student.class);
System.out.println(serializeStudent.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
private static class Student {
private String id;
private String name;
private Student() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
程序运行结果:
序列化之后:[10, 4, 49, 48, 48, 48, 18, 6, -26, -99, -114, -26, -95, -91]
李桥
参考文档:初探Protostuff的使用
github: https://github.com/protostuff/protostuff
2.4 Kryo 协议
Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、 Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用。而FST是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例,但它还是非 常有前途的,
下面我们比较下,java原生序列化Kryo序列化性能比较。Kryo的性能是JDK序列化协议性能的10倍-100倍之间。
2.5 XML协议
优点:1、人机可读性好,可指定元素或特性的名称
2、数据可以有层次结构
缺点:1、只能序列化属性和字段、不能序列化方法
2、文件庞大,文件格式复杂,传输占带宽
3、序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息
使用场景: 配置文件存储数据
xml类库主要有:dom4j、Jdom、xstrem 参考文档: Java 主流开源类库解析 XML
2.6 JSON协议
优点:1、数据格式比较简单,易于读写
2、序列化后数据较小,可扩展性好,兼容性好
3、与XML相比,其协议比较简单,解析速度比较快
缺点:1、数据的描述性比XML差
2、不适合性能要求为ms级别的情况
3、额外空间开销比较大
使用场景: 传输数据量相对小,实时性要求相对低(例如秒级别)的服务
2.6.1 开源的Jackson
相比json-lib框架,Jackson所依赖的jar包较少,简单易用并且性能也要相对高些。而且Jackson社区相对比较活跃,更新速度也比较快。
Jackson对于复杂类型的json转换bean会出现问题,一些集合Map,List的转换出现问题。Jackson对于复杂类型的bean转换Json,转换的json格式不是标准的Json格式
2.6.2 Google的Gson
Gson是目前功能最全的Json解析神器,Gson当初是为因应Google公司内部需求而由Google自行研发而来,
但自从在2008年五月公开发布第一版后已被许多公司或用户应用。
Gson的应用主要为toJson与fromJson两个转换函数,无依赖,不需要例外额外的jar,能够直接跑在JDK上。
而在使用这种对象转换之前需先创建好对象的类型以及其成员才能成功的将JSON字符串成功转换成相对应的对象。
类里面只要有get和set方法,Gson完全可以将复杂类型的json到bean或bean到json的转换,是JSON解析的神器。
Gson在功能上面无可挑剔,但是性能上面比FastJson有所差距。
2.6.3 阿里巴巴的FastJson
Fastjson是一个Java语言编写的高性能的JSON处理器,由阿里巴巴公司开发。无依赖,不需要例外额外的jar,能够直接跑在JDK上。FastJson在复杂类型的Bean转换Json上会出现一些问题,
可能会出现引用的类型,导致Json转换出错,需要制定引用。FastJson采用独创的算法,将parse的速度提升到极致,超过所有json库。
2.6.4 总结
综上对各种Json技术的比较,在项目选型的时候可以使用Google的Gson和阿里巴巴的FastJson两种并行使用,如果只是功能要求,没有性能要求,可以使用google的 Gson ,
如果有性能上面的要求可以使用Gson将bean转换json确保数据的正确,使用 FastJson 将Json转换Bean。
2.7 FST
高性能序列化框架FST:http://liuyieyer.iteye.com/blog/2136240
FST 是完全兼容JDK序列化协议的系列化框架,序列化速度大概是JDK的4-10倍,序列化之后码流大小是JDK大小的1/3左右。
2.8 hessian
Hessian类似Web Service,是一种高效简洁的远程调用框架。Hessian的主页:http://hessian.caucho.com/
相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议(Binary),因为采用的是二进制协议,所以它很适合于发送二进制数据。Hessian通常通过Web应用来提供服务,因此非常类似于WebService。
只是它不使用SOAP协议。 Hessian通过Servlet提供远程服务。需要将匹配某个模式的请求映射到Hessian服务。Spring的DispatcherServlet可以完成该功能,DispatcherServlet可将匹配模式的请求转发到Hessian服务。
Hessian的server端提供一个servlet基类, 用来处理发送的请求,而Hessian的这个远程过程调用,完全使用动态代理来实现的,,推荐采用面向接口编程,因此,Hessian服务建议通过接口暴露。
Hessian处理过程示意图:
客户端——>序列化写到输出流——>远程方法(服务器端)——>序列化写到输出流 ——>客户端读取输入流——>输出结果
Hessian的有着不同语言的版本:Java、Flash、Python、C++、.NET C#、D、Erlang、PHP、Ruby,Delphi等等,以后也许会更多更多。Java版的Hessian性能稍逊于RMI。Hessian最大的优势就是简单而且强大!
2.9 Facebook的Thrift
Thrift源于Facebook,2007年Facebook将Thrift做为一个开源项目交给了apache基金会。
特点:
- 1、 Thrift支持多种语言(C++,C#,Cocoa,Erlag,Haskell,java,Ocami,Perl,PHP,Python,Ruby,和SmallTalk)
- 2、Thrift适用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于Json和xml在性能上和传输大小上都有明显的优势。
- 3、Thrift支持三种比较典型的编码方式。(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)
3、序列化与单例模式
在将二进制流反序列化时,通过二进制中的类型信息,进行反射,获取Class对象,然后调用 newInstance()方法来创建对象,请注意newInstance()方法会调用该Class对象的无参构造器(private、public都可以),这样就创建了一个
对象实例,这样很可能会破坏单例模式中的该类只能保留一个实例的约束,为了保证单例模式真正的安全,我们一般使用枚举类型来实现真正的单例模式。
在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
使用枚举来实现单例模式可以很好的解决这种因为反射而带来的多例风险,所以个人推荐使用枚举来实现单例模式。
4、序列化在Java框架中的应用
4.1 Dubbo 序列化
参考文档:Dubbo序列化
在dubbo RPC中,同时支持多种序列化方式,例如:
- dubbo序列化:阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它
- hessian2序列化:hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的hessian lite,它是dubbo RPC默认启用的序列化方式
- json序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。
- java序列化:主要是采用JDK自带的Java序列化实现,性能很不理想。
在通常情况下,这四种主要序列化方式的性能从上到下依次递减。对于dubbo RPC这种追求高性能的远程调用方式来说,实际上只有1、2两种高效序列化方式比较般配,而第1个dubbo序列化由于还不成熟,所以实际只剩下2可用,所以dubbo RPC默认采用hessian2序列化。
但hessian是一个比较老的序列化实现了,而且它是跨语言的,所以不是单独针对java进行优化的。而dubbo RPC实际上完全是一种Java to Java的远程调用,其实没有必要采用跨语言的序列化方式(当然肯定也不排斥跨语言的序列化)。
最近几年,各种新的高效序列化方式层出不穷,不断刷新序列化性能的上限,最典型的包括:
- 专门针对Java语言的:Kryo,FST等等
- 跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack等等
这些序列化方式的性能多数都显著优于hessian2(甚至包括尚未成熟的dubbo序列化)。
有鉴于此,我们为dubbo引入Kryo和FST这两种高效Java序列化实现,来逐步取代hessian2。
其中,Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用。而FST是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例,但我认为它还是非常有前途的。
在面向生产环境的应用中,我建议目前更优先选择Kryo。
4.2 Netty 编解码
参考一篇优秀的文章:Netty之编解码技术(四)
Netty自带的编解码器
Netty提供了编解码器框架,使得编写自定义的编解码器很容易,并且也很容易重用和封装。Netty提供了 io.netty.handler.codec.MessageToByteEncoder和io.netty.handler.codec.ByteToMessageDecoder接口,方便我们扩展编解码。
-
Codec,编解码器
-
Decoder,解码器
-
Encoder,编码器
编解码器Codec
因为编解码器由两部分组成:Decoder(解码器),Encoder(编码器)编码器。负责将消息从字节或其他序列形式转成指定的消息对象,编码器则相反;解码器负责处理“入站”数据,编码器负责处理“出站”数据。编码器和解码器的结构很简单,消息被编码后解码后会自动通过ReferenceCountUtil.release(message)释放,如果不想释放消息可以使用ReferenceCountUtil.retain(message),这将会使引用数量增加而没有消息发布,大多数时候不需要这么做。