序列化:将一个对象编码成一个字节流;
反序列化:从字节流编码中重新构建对象;
序列化技术为远程通信提供了标准的线路级对象表示法,也为JavaBeans组件结构提供了标准的持久化数据格式;
谨慎的实现Serializable接口
虽然直接开销很低,但长期开销却很大;
最大代价:
一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性;
(一旦这个类被广泛使用,往往必须永远支持这种序列化的形式)
(以后又要改变这个类的内部表示法,就可能导致序列化格式的不兼容)
如果接受了默认的序列化形式,这个类中私有的和包级私有的实例域都将变成导出的API的一部分, 这不符合“最低限度的访问域”的实践准则,从而它就失去了作为信息隐藏工具的有效性;
序列化会使类的演变收到限制,其中一个原因是与序列版本UID有关;
(自动生成的序列版本UID其值受类名,实现的接口,所有公有的和受保护的成员名称影响)
第二个代价:
增加了出现bug和安全漏洞的可能性;
序列化机制是一种语言之外的对象创建机制;
反序列化是一个隐藏的构造器,具备与其他构造器相同的特点;
所有经常会忘记要确保:反序列化过程必须也要保证所有“真正的构造器建立起来的约束关系”, 并且不允许攻击者访问正在构造过程中的对象的内部信息;
依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏以及非法访问;
第三个代价:
随着类发行新的版本,相关的测试负担也增加了;
要考虑新旧版本序列化的兼容性问题;
除了二进制兼容性(能序列化反序列化成功),还要测试语义兼容性(产生的对象真正是原始对象的复制品);
为了继承而设计的类,应该尽可能少的去实现Serializable接口,用户的接口应该尽可能少的继承Serializable接口;
如果类有一些约束条件,当类的实例域被初始化成他们的默认值时,就会违背这些约束条件, 这时候就必须给这个类添加readObjectNoData方法;
如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类,特别是, 如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化;
因此对于为继承而设计的不可序列化的类,应该考虑提供一个无参构造器;
最好在所有约束关系都已经建立的情况下再创建对象,如果为了建立这些约束关系而要求客户端提供一些数据, 这实际上就排除了使用无参构造器的可能性;
(不可盲目的为类增加无参构造器和单独的初始化方法)
在允许子类实现Serializable接口和禁止子类实现Serializable接口两者间的折中方案是提供一个可访问的无参构造器:
内部类不应该实现Serializable,它们使用编译器产生的合成域来保存指向外围实例的引用, 以及保存来着外围作用域的局部变量的值;
(内部类的默认序列化形式是定义不清楚的)
静态成员类却是可以实现Serializable接口;
总结:实现Serializable是个很严肃的承诺,必须认真对待;
考虑使用自定义的序列化形式
如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接收;
(灵活性,性能,正确性等多方面考察)
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式;
即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性;
当物理表示法和它的逻辑数据内容有实质性区别时,使用默认序列化形式有以下缺点:
1. 使这个类的到处API永远的束缚在该类的内部表示法上;
(如上面私有的StringList.Entry变成公有API的一部分,如果将来版本中,内部实现发生变化, 这个类也摆脱不了维护链表项所需要的所有代码,即使它不再使用链表作为内部数据结构了)
2. 消耗过多的空间;(如上链表项只是实现细节,不值得记录在序列化形式中,会消耗过多空间)
3. 消耗过多时间;(序列化逻辑并不了解对象图的拓扑关系,所有它必须经过一个昂贵的图遍历,但上面例子中沿着next引用遍历是非常简单的)
4. 引起栈溢出;(默认的序列化过程要对对象图进行一次递归遍历)
上例中合理的序列化如下:
transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉;
如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject 也是允许的,但是不推荐这样做;
冗余域:值可以根据其他基本数据域计算而得到的域;
在决定将一个域做成非transient之前,请一点要确信它的值将是改对象逻辑状态的一部分;
如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有实例域则都应该标记为transient,就像上例中一样;
默认序列化形式,被标记为transient的实例域反序列化时将被初始化成默认值;
无论是否使用默认序列化形式, 如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化方法上强制这种同步 如:
无论选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显示的序列版本UID, 这样可以避免序列版本UID称为潜在的不兼容根源,也可提高性能;
(不显示提供,就需要在运行时通过一个高开销的计算过程产生)
可以如下声明:
当要升级新版本,且不兼容现有版本时,只需修改serialVersionUID即可
保护性的编写readObject方法
不严格的说,readObject是一个用字节流作为唯一参数的构造器;
通过伪造字节流,创建实例对象,可以违反它所属类的约束条件;
当一个对象被反序列化时,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用, 就必须要做保护性拷贝,这是非常重要的;
不要使用ObjectOutputStream的writeUnshared和readUnshared方法,他们通常比保护性拷贝更快,但是不提供必要的安全性保护;
如何更加健壮的编写readObject:
1. 必须执行构造器锁要求的所有有效性检查和保护性拷贝;
(另一种方法是使用序列化代理模式)
2. 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象,不可变类的可变组件就属于这一类别;
3. 对应任何约束条件如果检查失败,则抛出一个InvalidObjectException异常,这些检查动作应该跟在所有的保护性拷贝之后;
4. 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口;
5. 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法;
对于实例控制,枚举类型优先于readResolve
不考虑反射这种极端情况,还有一种情况会破坏单例模式,就是反序列化;
jdk其实预料到这种情况,解决方法就是加入readResolve();
单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法;
如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的(或者基本类型);
(否则攻击者可能在readResolve运行前,保护指向反序列化对象的引用)
但是这种方法很脆弱,容易被攻破,建议用枚举代替;
考虑用序列化代理代替序列化实例
实现Serializable会增加出错和安全问题的可能性,因为它导致实例要利用语言外的机制来创建,而不是普通的构造器, 序列化代理模式可以极大地减少这些风险;
实现:为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类的实例的逻辑状态, 这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类;
序列化代理方法可以阻止伪字节流攻击以及内部域的盗用攻击;
序列化代理的局限性:
不能与可以被客户端扩展的类兼容;
不能与对象图中包含循环的某些类兼容;序列化代理也付出了开销增加的代价;
总结:当必须在一个不能被客户端扩展的类上编写readObject和writeObject方法时,就应该考虑使用序列化代理模式;
我是今阳,如果想要进阶和了解更多的干货,欢迎关注公众号”今阳说“接收我的最新文章
网友评论