Effective Java读书笔记-17

其他方法优先于Java序列化

序列化的根本问题在于,其攻击面(attack surface)过于庞大,无法进行防护,并且它还在不断地扩大:对象图是通过在ObjectInputStream上调用readObject方法进行反序列化的。

避免序列化攻击的最佳方式是永远不要反序列化任何东西。在新编写的任何新系统中都没有理由再使用Java序列化。

最前沿的跨平台结构化数据表示法是JSON和Protocol Buffers,也称作 protobuf 。JSON是Douglas Crockford为浏览器-服务器之间的通信设计的,Protocol Buffers 是Google为了在服务器之间保存和交换结构化数据设计的。

JSON和 protobuf之间最明显的区别在于,JSON是基于文本的,人类可以阅读,而protobuf是二进制的,从根本上来说更有效;JSON纯粹就是一个数据表示法,而 protobuf 则提供模式(类型),建立文档,强制正确的用法。虽然 protobuf 比 JSON 更加有效,但JSON对于基于文本的表示法却非常高效。protobuf虽然是一个二进制表示法,但它提供了可以替代的另一种文本表示法(pbtxt),当人类需要读懂它的时候可以使用。

如果无法完全避免Java序列化,或许是因为需要在Java序列化的遗留系统环境中工作,下一步最好永远不要反序列化不被信任的数据。

序列化是很危险的,应该予以避免。如果是重新设计一个系统,一定要用跨平台的结构化数据表示法代替,如JSON或者 protobuf。不要反序列化不被信任的数据。如果必须这么做,就要使用对象的反序列化过滤,但要注意的是,它并不能确保阻止所有的攻击。不要编写可序列化的类。

谨慎地实现Serializable接口

要想使一个类的实例可被序列化,非常简单,只要在它的声明中加入implements Serializable字样即可。虽然使一个类可被序列化的直接开销非常低,但是为了序列化而付出的长期开销往往是相当高的。

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

实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。

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

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

内部类不应该实现Serializable接口。它们使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用,以及保存来自外围作用域的局部变量的值。

千万不要认为实现Serializable接口会很容易。 除非一个类只在受保护的环境下使用,在这里版本之间永远不会交互,服务器永远不会暴露给不可信任的数据,否则,实现Serializable接口就是个很严肃的承诺,必须认真对待。如果一个类允许继承,则更要加倍小心。

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

  • 如果事先没有认真考虑默认的序列化形式是否合适,则不要贸然接受。
  • 如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。
  • 即使你确定了默认的序列化形式是合适的,通常还必须提供一个readobject方法以保证约束关系和安全性。

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

  • 它使这个类的导出APl永远地束缚在该类的内部表示法上。
  • 它会消耗过多的空间。
  • 它会消耗过多的时间。
  • 它可能导致堆栈溢出。

在决定将一个域做成非瞬时的之前,请一定要确信它的值将是该对象逻辑状态的一部分。

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

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

不要修改序列版本UID,否则将会破坏类现有的已被序列化实例的兼容性。

保护性地编写readObject方法

当—个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的。

在编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例。下面以摘要的形式给出一些指导方针,有助于编写出更加健壮的readObject方法:

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

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

如果依赖 readResolve 进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient。
readResolve 的可访问性(accessibility)很重要。 如果把readResolve方法放在一个final类上,它就应该是私有的。如果把readResolver方法放在一个非final类上,就必须认真考虑它的可访问性。如果它是私有的,就不适用于任何子类。如果它是包级私有的,就只适用于同一个包中的子类。如果它是受保护的或者公有的,就适用于所有没有覆盖它的子类。如果readResolve方法是受保护的或者是公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样有可能导致ClassCastException异常。

总而言之,应该尽可能地使用枚举类型来实施实例控制的约束条件。 如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolver方法,并确保该类的所有实例域都为基本类型,或者是瞬时的。

考虑用序列化代理代替序列化实例

决定实现Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是用普通的构造器。然而,使用序列化代理模式(serialization proxy pattern),可以极大地减少这些风险。

序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理(serialization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。从设计的角度来看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口。

序列化代理模式有两个局限性。它不能与可以被客户端扩展的类相兼容。它也不能与对象图中包含循环的某些类相兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCast-Exception异常,因为你还没有这个对象,只有它的序列化代理。

最后一点,序列化代理模式所增强的功能和安全性并不是没有代价的。

总而言之,当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法时,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值