美文网首页
EffectiveJava-10-序列化

EffectiveJava-10-序列化

作者: 今阳说 | 来源:发表于2021-01-21 09:05 被阅读0次

    序列化:将一个对象编码成一个字节流;

    反序列化:从字节流编码中重新构建对象;

    序列化技术为远程通信提供了标准的线路级对象表示法,也为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方法时,就应该考虑使用序列化代理模式;

    我是今阳,如果想要进阶和了解更多的干货,欢迎关注公众号”今阳说“接收我的最新文章

    相关文章

      网友评论

          本文标题:EffectiveJava-10-序列化

          本文链接:https://www.haomeiwen.com/subject/akrrzktx.html