高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

其他替代方式优于Java本身序列化

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

序列化

本章涉及对象序列化(object serialization),它是Java的框架,用于将对象编码为字节流(序列化)并从其编码中重构对象(反序列化)。 一旦对象被序列化,其编码可以从一个虚拟机发送到另一个虚拟机或存储在磁盘上以便以后反序列化。 本章重点介绍序列化的风险以及如何将序列化的风险最小化。

85. 其他替代方式优于Java本身序列化

当序列化在1997年添加到Java中时,它被认为有一定的风险。这种方法曾在研究语言(模块3)中尝试过,但从未在生产语言中使用过。虽然程序员不费什么力气就能实现分布式对象的承诺很吸引人,但代价是不可见的构造方法和API与实现之间模糊的界线,可能会出现正确性、性能、安全性和维护方面的问题。支持者认为收益大于风险,但历史证明并非如此。

本书前几版中描述的安全问题与一些人担心的一样严重。 2000年之前中讨论的漏洞在未来十年被转化为严重漏洞,其中最著名的包括2016年11月对旧金山大都会运输署(San Francisco Metropolitan Transit Agency)市政铁路(SFMTA Muni)的勒索软件攻击,导致整个收费系统关闭了两天[Gallagher16]。

序列化的一个基本问题是它的攻击面太大而无法保护,而且还在不断增长:通过调用ObjectInputStream类上的readObject方法反序列化对象图。这个方法本质上是一个神奇的构造方法,可以用来实例化类路径上几乎任何类型的对象,只要该类型实现Serializable接口。在反序列化字节流的过程中,此方法可以执行来自任何这些类型的代码,因此所有这些类型的代码都是攻击面的一部分。

攻击面包括Java平台类库中的类,第二方类库(如Apache Commons Collections)和应用程序本身。 即使你遵守所有相关的最佳实践并成功编写无法攻击的可序列化类,你的应用程序仍可能容易受到攻击。 引用CERT协调中心技术经理Robert Seacord的话:

Java反序列化是一个明显且存在的危险,因为它直接被应用程序广泛使用,并间接地由Java子系统(如RMI(远程方法调用),JMX(Java管理扩展)和JMS(Java消息系统))广泛使用。 不受信任的流的反序列化可能导致远程代码执行(RCE),拒绝服务(DoS)以及一系列其他漏洞利用。 应用程序即使没有做错也容易受到这些攻击。[Seacord17]

攻击者和安全研究人员研究Java类库和常用的第三方类库中的可序列化类型,寻找在反序列化过程中调用的执行潜在危险活动的方法。这种方法称为gadget。多个gadget可以同时使用,形成一个gadget链(chain)。偶尔会发现gadget链,它的功能足够强大,允许攻击者在底层硬件上执行任意的本机代码,只要有机会提交精心设计的字节流进行反序列化。这正是SFMTA Muni袭击中发生的事情。这次袭击并不是孤立事件。已经发生过,而且还会有更多。

不使用任何gadget,就可以通过导致需要很长时间反序列化的短字节流,进行反序列化操作,轻松地发起拒绝服务攻击。这种流被称为反序列化炸弹(deserialization bombs)[Svoboda16]。下面是Wouter Coekaerts的一个例子,它只使用HashSet和字符串[Coekaerts15]:

// Deserialization bomb - deserializing this stream takes forever
static byte[] bomb() {
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    Set<Object> s2 = new HashSet<>();
    for (int i = 0; i < 100; i++) {
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        t1.add("foo"); // Make t1 unequal to t2
        s1.add(t1);  s1.add(t2);
        s2.add(t1);  s2.add(t2);
        s1 = t1;
        s2 = t2;
    }
    return serialize(root); // Method omitted for brevity
}

对象图由201个HashSet实例组成,每个实例包含3个或更少的对象引用。整个流的长度为5744字节,但是在完成反序列化之前,太阳都已经耗尽了。问题是反序列化HashSet实例需要计算其元素的哈希码。root实例的2个元素本身就是包含2个HashSet元素的HashSet,每个HashSet元素包含2个HashSet元素,以此类推,深度为100。因此,反序列化set会导致hashCode方法被调用超过2100次。除了反序列化会持续很长时间之外,反序列化器没有任何错误的迹象。生成的对象很少,并且堆栈深度是有界的。

那么你能做些什么来抵御这些问题呢? 每当反序列化你不信任的字节流时,就会打开攻击。 避免序列化漏洞利用的最佳方法是永远不要反序列化任何东西。用1983年电影《战争游戏》(WarGames)中名为约书亚(Joshua)的电脑的话来说,“唯一的制胜的招式就是不玩”。没有理由在你编写的任何新系统中使用Java序列化。 还有其他在对象和字节序列之间进行转换的机制,可以避免Java序列化的许多危险,同时提供许多优势,例如跨平台支持,高性能,大型工具生态系统以及广泛的专业知识社区。 在本书中,我们将这些机制称为跨平台结构化数据表示( cross-platform structured-data representations)。 虽然其他人有时将它们称为序列化系统,但本书避免了这种用法,以防止与Java序列化混淆。

这些表示的共同点是它们比Java序列化简单得多。它们不支持任意对象图的自动序列化和反序列化。相反,它们支持由一组属性值对(attribute-value pairs)组成的简单结构化数据对象。只支持少数基本数据类型和数组数据类型。事实证明,这个简单的抽象足以构建功能极其强大的分布式系统,而且足够简单,可以避免Java序列化从一开始就存在的严重问题。

领先的跨平台结构化数据表示是JSON [JSON]和Protocol Buffers,也称为protobuf [Protobuf]。 JSON由Douglas Crockford设计用于浏览器——服务器通信,并且Protocol Buffers由Google设计用于在其服务器之间存储和交换结构化数据。 即使这些表示有时被称为中立语言(language-neutral),JSON最初是为JavaScript开发的,而protobuf是为C++开发的; 这两种表述都保留了其起源的痕迹。

JSON和protobuf之间最显着的区别是JSON是基于文本的,人类可读的,而protobuf是二进制的,而且效率更高; JSON是一种专门的数据表示,而protobuf提供模式(类型)来文档记录和执行适当的用法。 尽管protobuf比JSON更有效,但JSON对于基于文本的表示非常有效。 虽然protobuf是二进制表示,但它确实提供了一种替代文本表示,用于需要人们可读性的地方(pbtxt)。

如果不能完全避免Java序列化,可能需要在它的遗留系统环境里中工作,那么下一个最佳选择就是永远不要反序列化不受信任的数据。特别是,不应该接受来自不可信源的RMI流量。Java的官方安全编码指南说“反序列化不受信任的数据本质上是危险的,应该避免。这句话是用大号、粗体、斜体和红色字体设置的,它是整个文档中唯一应用这种处理([Java-secure)的文本。

如果无法避免序列化,并且不能绝对确定反序列化数据的安全性,那么可以使用Java 9中添加的对象反序列化过滤器,并将其移植到早期版本(Java .io. objectinputfilter)。该工具允许指定一个过滤器,该过滤器在反序列化之前应用于数据流。它在类的粒度上运行,允许接受或拒绝某些类。默认接受类并拒绝潜在危险类的列表称为黑名单;在默认情况下拒绝类并接受假定安全的类的列表称为白名单。比起黑名单,更喜欢白名单,因为黑名单只保护你免受已知的威胁。一个名为Serial Whitelist Application Trainer (SWAT)的工具可用于应用程序自动准备一个白名单[Schneider16]。过滤工具还将保护免受过度使用内存和过深的对象图的影响,但它不能保护免受如上面所示的序列化炸弹的影响。

不幸的是,序列化在Java生态系统中仍然普遍存在。 如果要维护基于Java序列化的系统,请认真考虑迁移到跨平台的结构化数据表示,即使这可能是一项耗时的工作。 实际上,可能仍然发现自己必须编写或维护可序列化的类。 编写一个正确,安全,高效的可序列化类需要非常小心。 本章的其余部分提供了有关何时以及如何执行此操作的建议。

总之,序列化是危险的,应该避免。如果从头开始设计一个系统,可以使用跨平台的结构化数据表示,如JSON或protobuf。不要反序列化不受信任的数据。如果必须这样做,请使用对象反序列化过滤器,但要注意,它不能保证阻止所有攻击。避免编写可序列化的类。如果你必须这样做,一定要非常小心。