googleprotobuf实体类和java对象互转_⼀⽂讲透Java序列化前⾔
Oracle 公司计划废除 Java 中的古董:序列化技术,因为它带来了许多严重的安全问题(如序列化存储安全、反序列化安全、传输安全等),据统计,⾄少有3分之1的漏洞是序列化带来的,这也是 1997 年诞⽣序列化技术的⼀个巨⼤错误。但是,序列化技术现在在 Java 应⽤中
⽆处不在,特别是现在的持久化框架和分布式技术中,都需要利⽤序列化来传输对象,如:Hibernate、Mybatis、Java RMI、Dubbo
等,即对象要存储或者传输都不可避免要⽤到序列化技术,所以删除序列化技术将是⼀个长期的计划。
你在实际⼯作中可能会很难有机会真正⽤到Java⾃带的序列化技术了,⼯业界⼀般也会选择⼀些更安全的对象编解码⽅案例如Google的Protobuf等。所以,对于Java序列化,我们不必再投⼊过多的精⼒学习,你花20分钟读完本⽂所掌握的知识,对于应付⽇常源码阅读中遇到的遗留的Java序列化技术应该是⾜够了。
⼀、序列化是什么
序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过⽹络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运⾏⽽独⽴存在。
序列化:将⼀个Java对象写⼊IO流中
反序列化:从IO流中恢复该Java对象
本⽂中⽤序列化来简称整个序列化和反序列化机制。
⼆、为什么需要序列化
所有可能在⽹络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,⽐如RMI(Remote Method Invoke,即远程⽅法调⽤,
是JavaEE的基础)过程中的参数和返回值;所有需要保存到磁盘⾥的对象的类都必须可序列化,⽐如Web应⽤中需要保存到HttpSession或ServletContext属性的Java对象。
因为序列化是RMI过程的参数和返回值都必须实现的机制,⽽RMI⼜是Java EE技术的基础——所有的分布式应⽤常常需要跨平台、跨⽹
络,所以要求所有传递的参数、返回值必须实现序列化。因此序列化机制是Java EE平台的基础。通常建议:程序创建的每个JavaBean类
都实现Serializable。
三、序列化怎么⽤
如果⼀个类的对象需要序列化,那么在Java语法层⾯,这个类需要:
实现Serializable接⼝
使⽤ObjectOutputStream将对象输出到流,实现对象的序列化;使⽤ObjectInputStream从流中读取对象,实现对象的反序列化
下⾯我们通过代码⽰例来看看序列化最基本的⽤法。我们创建了Person类,其拥有两个基本类型的属性,并实现了Serializable接⼝。testSerialize⽅法⽤来测试序列化,testDeserialize⽅法⽤来测试反序列化。
1 import org.junit.Test;
2
3 import java.io.*;
4
5 public class SerializableTest {
6
7    @Test
8    public void testSerialize() {
9        Person one = new Per
四、序列化深度探秘
4.1 为什么必须实现Serializable接⼝
如果某个类需要⽀持序列化功能,那么它必须实现Serializable接⼝,否则会报 java.io.NotSerializableException。Serializable接⼝是⼀
个标志性接⼝(Marker Interface),也就是说,该接⼝并不包含任何具体的⽅法,是⼀个空接⼝,仅仅⽤来判断该类是否能够序列化。
JDK8中Serializable接⼝的源码如下:
1 package java.io;
2
3 public interface Serializable {
4 }
在 ObjectOutputStream.java 的 writeObject0 ⽅法中,我们确实可以看到对对象是否实现了 Serializable接⼝进⾏了验证(第15⾏),否则会抛出 NotSerializableException 异常(第22⾏)。
1    private void writeObject0(Object obj, boolean unshared)
2        throws IOException
3    {
4        boolean oldMode = bout.setBlockDataMode(false); 5
java源代码加密4.2 被序列化对象的字段是引⽤时该怎么办
在第三部分“序列化怎么⽤”部分的⽰例中,Person类的字段全都是基本类型,我们知道基本类型其地址中直接存放的就是它的值,那如
果是引⽤类型呢?引⽤类型其地址中存放的是指向堆内存中的⼀个地址,难道序列化时就是将这个地址进⾏了保存吗?显然,这是说不通的,因为对象的内存地址是可变的,在同⼀系统的不同运⾏时刻或者是不同系统中,对象的地址肯定是不同的,因此,序列化内存地址没有意义。
如果被序列化对象的字段是引⽤,那么要求该引⽤的类型也是可序列化实现了Serializable接⼝的,否则⽆法序列化。当对某个对象进⾏序
列化时,系统会⾃动把该对象的所有Field依次进⾏序列化,如果某个Field引⽤到另⼀个对象,则被引⽤的对象也会被序列化;如果被引⽤
的对象的Field也引⽤了其他对象,则被引⽤的对象也会被序列化,这种情况被称为递归序列化。
4.3 同⼀个对象会被序列化多次吗
如果对象A和对象B同时引⽤了对象C,那么,当序列化对象A和对象B时,对象C会被序列化两次吗?答案显然是不会。
要解释这个问题,就不得不说⼀下Java序列化的基本算法了:
所有序列化到⼆进制流的对象都有⼀个序列化编号
当程序试图序列化⼀个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并赋予⼀个唯⼀的编号
如果某个对象已经序列化过,程序将只是直接输出其序列化编号,⽽不是再次重新序列化该对象
4.4 只想序列化对象的部分字段该怎么办
在⼀些特殊的场景下,如果⼀个类⾥包含的某些Field值是敏感信息,例如银⾏账户信息等,这时不希望系统将该Field值进⾏序列化;或者
某个Field的类型是不可序列化的,因此不希望对该Field进⾏递归序列化,以避免引发java.io.NotSerializableException异常。
此时,我们就需要⾃定义序列化了。⾃定义序列化的常⽤⽅式有两种:
使⽤transient关键字
重写writeObject与readObject⽅法
我们先看第⼀种⽅式,使⽤transient关键字。transient关键字只能⽤于修饰Field,不可修饰Java程序中的其他成分。使⽤transient修饰
的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引⽤类型,值是null;基本类型,值是0;boolean类型,值是false。
下列代码中,我们把People的height字段设置为transient,在反序列化时,可观察到输出为默认值0.0。
1 import org.junit.Test;
2
3 import java.io.*;
4
5 public class SerializableTest {
6
7    @Test
8    public void testSerialize() {
9        Person one = new Per 程序输出:
Person{age=12, height=0.0}Person{age=16, height=0.0}Process finished with exit code 0
使⽤transient关键字修饰Field虽然简单、⽅便,但被transient修饰的Field将被完全隔离在序列化机制之外,这样导致在反序列化恢复
Java对象时⽆法取得该Field值。Java还提供了⼀种⾃定义序列化机制,通过这种⾃定义序列化机制可以让程序控制如何序列化各Field,甚⾄完全不序列化某些Field(与使⽤transient关键字的效果相同)。在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的⽅法,这些特殊的⽅法⽤以实现⾃定义序列化。
private void writeObject(java.io.ObjectOutputStream out)    throws IOException private void readObject(java.io.ObjectInputStream in)    throws IOExcepti
writeObject()⽅法负责写⼊特定类的实例状态,以便相应的readObject()⽅法可以恢复它。通过重写该
⽅法,程序员可以完全获得对序列化机制的控制,可以⾃主决定哪些Field需要序列化,需要怎样序列化。在默认情况下,该⽅法会调⽤out.defaultWriteObject来保存Java对象的各Field,从⽽可以实现序列化Java对象状态的⽬的。
readObject()⽅法负责从流中读取并恢复对象Field,通过重写该⽅法,程序员可以完全获得对反序列化机制的控制,可以⾃主决定需要反序列化哪些Field,以及如何进⾏反序列化。在默认情况下,该⽅法会调⽤in.defaultReadObject来恢复Java对象的⾮静态和⾮瞬态Field。在通常情况下,readObject()⽅法与writeObject()⽅法对应,如果writeObject()⽅法中对Java对象的Field进⾏了⼀些处理,则应该在readObject()⽅法中对其Field进⾏相应的反处理,以便正确恢复该对象。
当序列化流不完整时,readObjectNoData()⽅法可以⽤来正确地初始化反序列化的对象。例如,接收⽅使⽤的反序列化类的版本不同于发送⽅,或者接收⽅版本扩展的类不是发送⽅版本扩展的类,或者序列化流被篡改时,系统都会调⽤readObjectNoData()⽅法来初始化反序列化的对象。
下⾯的⽰例代码中,我们在writeObject⽅法中对Person的字段进⾏了简单的加密处理,在readObject⽅法中对其进⾏了相应的解密。
1 import org.junit.Test;
2
3 import java.io.*;
4
5 public class SerializableTest {
6
7    @Test
8    public void testSerialize() {
9        Person one = new Per
4.5 被序列化对象具有继承关系该怎么办
被序列化对象具有继承关系时⽆⾮就两种情况,第⼀,该类具有⼦类,第⼆,该类具有⽗类。
当该类实现了Serializable接⼝且具有⼦类时,根据官⽅⽂档中的说明,其⼦类天然具有可被序列化的属性,不需要显式实现Serializable接⼝;。
All subtypes of a serializable class are themselves serializable.
当该类实现了Serializable接⼝且具有⽗类时,,该类的⽗类需要实现Serializable接⼝吗?在JDK8中Serializable接⼝的官⽅⽂档中有这样⼀段话:
1 /**
2  * ......
3  *
4  * To allow subtypes of non-serializable classes to be serialized, the
5  * subtype may assume responsibility for saving and restoring the
阅读⽂档我们得知,为了使得不可序列化类的⼦类能够序列化,其⼦类必须担负起保存和恢复其超类的public、protected 和 package(if accessible)实例域的责任,且要求其⽗类必须有⼀个可访问的⽆参构造函数以使得在反序列化时能够初始化实例域。
我们写代码验证⼀下,如果⽗类中没有可访问的⽆参构造函数会发⽣什么,注意Person类中没有⽆参构造函数。
1 import org.junit.Test;
2
3 import java.io.*;
4
5 public class SerializableTest {
6
7    @Test
8    public void testSerialize() {
9        Student one = new Stu 程序输出产⽣异常:
java.io.InvalidClassException: Student; no valid constructor    at java.io.wInvalidClassException(ObjectStreamClass.ja 当我们为Person类添加默认构造函数时:
1 class Person{
2    protected int age;
3    protected double height;
4
5    public Person() {
6    }
7
8    public Person(int age, double height) {
9        th
程序输出如下,我们可观察到,⽗类中的字段都是默认值,只有⼦类中的字段得到了正确的序列化。出现这种情况的原因是⼦类并没有担负
起序列化⽗类中字段的责任。
Student{age=0, height=0.0, id='1234'}Student{age=0, height=0.0, id='5678'}Process finished with exit code 0
为了解决上述问题,我们需要借助上⼀节中学到的知识,使⽤⾃定义的序列化⽅法writeObject和readObject来主动将⽗类中的字段进⾏序
列化。
1 import org.junit.Test;
2
3 import java.io.*;
4
5 public class SerializableTest {
6
7    @Test
8    public void testSerialize() {
9        Student one = new Student(1
程序输出如下,可以看到完全正确。
Student{age=12, height=156.6, id='1234'}Student{age=16, height=177.7, id='5678'}Process finished with exit code 0
五、serialVersionUID的作⽤及⾃动⽣成
我们知道,反序列化必须拥有class⽂件,但随着项⽬的升级,class⽂件也会升级,序列化怎么保证升级前后的兼容性呢?
java序列化提供了⼀个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可
以正确被反序列化回来。如果反序列化使⽤的class的版本号与序列化时使⽤的不⼀致,反序列化会报InvalidClassException异常。下⾯是
JDK 8中ArrayList的源码中的serialVersionUID。
1 public class ArrayList extends AbstractList
2        implements List, RandomAccess, Cloneable, java.io.Serializable
3 {
4    private static final long serialVersionU
序列化版本号可⾃由指定,如果不指定,JVM会根据类信息⾃⼰计算⼀个版本号,这样随着class的升级,就⽆法正确反序列化;不指定版
本号另⼀个明显隐患是,不利于jvm间的移植,可能class⽂件没有更改,但不同jvm可能计算的规则不⼀样,这样也会导致⽆法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况。
如果只是修改了⽅法,反序列化不容影响,则⽆需修改版本号
如果只是修改了静态Field或瞬态Field,则反序列化不受任何影响
如果修改类时修改了⾮静态Field、⾮瞬态Field,则可能导致序列化版本不兼容。如果对象流中的对象
和新类中包含同名的Field,⽽
Field类型不同,则反序列化失败,类定义应该更新serialVersionUID Field值。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
我们在⽇常编程实践中,⼀般会选择使⽤IDE来⾃动⽣成serialVersionUID,这样可以最⼤化地减少重复的可能性。对于IntelliJ IDEA,⾃
动⽣成serialVersionUID有三步:
修改IDEA配置:File->Setting->Editor->Inspections->Serialization issues->Serializable class without ’serialVersionUID’
类实现Serializable接⼝
在类名上执⾏Alt+Enter,然后选择⽣成serialVersionUID即可
六、序列化的缺点
Java序列化存在四个致命缺点,导致其不适⽤于⽹络传输:
⽆法跨语⾔:在⽹络传输中,经常会有异构语⾔的进程的交互,但Java序列化技术是Java语⾔内部的私有协议,其他语⾔⽆法进⾏反序列化。⽬前所有流⾏的RPC框架都没有使⽤Java序列化作为编解码框架。
潜在风险⾼:不可信流的反序列化可能导致远程代码执⾏(RCE)、拒绝服务(DoS)和⼀系列其他攻击。
序列化后的码流太⼤
序列化的性能较低
在真正的⽣产环境中,⼀般会选择其它编解码框架,领先的跨平台结构化数据表⽰是 JSON 和 Protocol Buffers,也称为 protobuf。JSON 由 Douglas Crockford 设计⽤于浏览器与服务器通信,Protocol Buffers 由⾕歌设计⽤于在其服务器之间存储和交换结构化数
据。JSON 和 protobuf 之间最显著的区别是 JSON 是基于⽂本的,并且是⼈类可读的,⽽ protobuf 是⼆进制的,但效率更⾼。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。