Effective-java-读书笔记之序列化

2022-10-25 11:33:34 浏览数 (1)

第85条 优先考虑非Java序列化的其他选择

Java的序列化容易被黑客利用, 引发安全问题.

deserialization bombs: 反序列化它将花费很长时间, 或永远无法完成.

避免序列化漏洞被利用的最佳方法是永远不要反序列化任何东西。

有很多机制可以替代序列化, 并且提供更多好处, 比如跨平台, 高性能, 社区支持等.

本书把这些替代方式称作cross-platform structured-data representations.

比较流行的有:

  • JSON
  • Protocol Buffers (protobuf).

如果你不能完全地避免Java序列化, 你可以:

  • 永远不要反序列化不受信任的数据.
  • 如果对数据安全性不能完全确定, 使用Java 9的java.io.ObjectInputFilter在反序列化前过滤数据(优先使用白名单策略).

第86条 谨慎地实现Serializable接口

实现Serializable不是一个轻率的决定.

实现Serializable接口而付出的最大代价是, 一旦一个类被发布, 就大大降低了"改变这个类的实现"的灵活性.

如果你接受了默认的序列化形式, 这个类中私有的和包级私有的实例域将都变成导出的API的一部分, 这不符合"最低限度地访问域"的实践原则, 从而它就失去了作为信息隐藏工具的有效性.

序列化会使类的演受到限制, 这种限制的一个例子与流的唯一标识符有关, 通常它也被称为序列版本UID(serial version UID). 如果没有显式声明, 系统会自动生成.

实现Serializable的第二个代价是, 它增加了出现Bug和安全漏洞的可能性. -> 反序列化是一个隐藏的构造器.

实现Serializable的第三个代价是, 随着类发行新的版本, 相关的测试负担也增加了.

为了继承而设计的类应该尽可能少地去实现Serializable接口, 用户的接口也应该尽可能少地继承Serializable接口.

内部类不应该实现Serializable接口, 除非是静态内部类.

第87条 考虑使用自定义的序列化形式

如果没有先认真考虑默认的序列化形式是否合适, 则不要贸然接受.

如果一个对象的物理表示法等同于它的逻辑内容, 可能就适合于使用默认的序列化形式.

即使你确定了默认的序列化形式是合适的, 通常还必须提供一个readObject方法以保证约束关系和安全性.

当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时, 使用默认序列化形式会有以下4个缺点:

  • 它使这个类的导出API永远地束缚在该类的内部表示法上.
  • 消耗过多的空间.
  • 消耗过多的时间.
  • 会引起栈溢出.

transient修饰符: 从序列化形式中省略掉实例域. 反序列化时这些域将被初始化为默认值.

当你决定把一个字段标记为非transient之前, 首先需要说服自己, 这个字段是这个对象逻辑状态的一部分.

无论你是否使用默认的序列化形式, 如果在读取整个对象状态的任何其他方法上强制任何同步, 则也必须在对象序列化上强制这种同步.

不论你选择了哪种序列化形式, 都要为自己编写的每个可序列化的类声明一个显式的序列版本UID(serial version UID).

除非你要破坏和所有已经存在的实例的兼容性, 否则就不要改序列版本UID.

第88条 保护性地编写readObject方法

readObject方法实际上相当于一个公有的构造器, 如同其他的构造器一样, 它也要求注意同样的所有注意事项.

构造器必须检查其参数的有效性, 并且在必要的时候对参数进行保护性拷贝.

编写更加健壮的readObject()方法的指导方针:

  • 对于对象引用域必须保持为私有的类, 要保护性地拷贝这些域中的每个对象. 不可变类的可变组件就属于这一类别.
  • 对于任何约束条件, 如果检查失败, 则抛出一个InvalidObjectException异常. 这些检查动作应该跟在所有的保护性拷贝之后.
  • 如果整个对象图在被反序列化之后必须进行验证, 就应该使用ObjectInputValidation接口.
  • 无论是直接还是间接方式, 都不要调用类中任何可被覆盖的方法.

第89条 对于实例控制, 枚举类型优先于readResolve

如果单例模式的类加上了implements Serializable, 就多了一种创建实例的途径.

readResolve特性允许你用readObject创建的实例代替另一个实例.

对于一个正在被反序列化的对象, 如果它的类定义了一个readResolve方法, 并且具备正确的声明, 那么在反序列化之后, 新建对象上的readResolve方法就会被调用.

然后该方法返回的对象引用将被返回, 取代新建的对象. 在这个特性的绝大多数用法中, 指向新建对象的引用不需要再被保留, 因此立即成为垃圾回收的对象.

可以利用readResolve方法保证单例模式. -> 方法忽略被反序列化的对象, 只返回该类初始化时创建好的那个实例.

如果依赖readResolve进行实例控制, 带有对象引用类型的所有实例域都必须声明为transient的.

从历史上来看, readResolve方法被用于所有可序列化的实例受控(instance-controlled)的类. 自从Java1.5以来, 它就不再是在可序列化的类中维持实例控制的最佳方法了.

应该尽可能地使用枚举类型来实施实例控制的约束条件.

但是如果这不可能做到, 或者你需要一个实现了序列化的实例受控的类, 那么你就必须提供一个readResolve方法, 然后确保所有的字段都是primitive或transient的.

第90条 考虑用序列化代理代替序列化实例

序列化代理模式(serialization proxy pattern):

  • 为可序列化的类设计一个私有的静态嵌套类(序列化代理), 它应该有一个单独的构造器, 其参数类型就是那个外围类.
  • 在外围类中添加writeReplace方法. -> 产生代理类实例.
  • 外围类中添加readObject方法. -> 防止伪造.
  • 代理类中提供readResolve方法, 返回一个逻辑上相当的外围类的实例. -> 序列化代理转变回外围类的实例.

序列化代理模式的局限性:

  • 不能与可以被客户端扩展的类兼容.
  • 不能与对象图中包含循环的某些类兼容.
  • 序列化代理模式的功能和安全性有性能开销的代价.

总而言之, 每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候, 就应该考虑使用序列化代理模式.

0 人点赞