马克·雷因霍尔德(Mark Reinhold)最近提议延迟Java 9,以花更多的时间来完成Jigsaw项目,这是即将发布的版本的主要功能。 虽然这个决定肯定会使Java的厄运论者重回舞台,但我个人感到很放心,并认为这是一个很好且必要的决定。 Java 9功能完成的里程碑当前设置为12月10日 ,禁止在该日期之后引入新功能 。 但是, 从 Jigsaw项目的早期访问版本来看,Java的模块系统似乎尚未为该开发阶段做好准备。
在最新的Java发布周期中,项目Jigsaw的延迟已成为一种习惯 。 一定不能将其误解为无能,而只能作为一种指示,说明向Java引入模块有多困难,而Java目前对于真正的模块化来说是陌生的。 最初,Java的模块系统是在2008年提出的,以包含在Java 7中。但是直到今天,拼图的实现始终比预期的要困难得多。 在几次停职甚至是暂时的放弃之后,Java的管理员肯定会承受最终成功的压力。 很高兴看到这种压力并没有促使Java团队急于发布。
在本文中,我尝试总结一下Jigsaw项目的状态,并在Jigsaw邮件列表中进行了公开讨论 。 我写这篇文章是对当前讨论的贡献,希望有更多的人参与正在进行的开发过程。 我无意淡化Oracle的辛勤工作。 我在明确声明这一点是为了避免在隐藏sun.misc.Unsafe之后对拼图进行了相当激动的讨论之后进行的误解。
模块化反射
究竟是什么使Jigsaw项目如此困难? 如今,可见性修饰符是封装类范围的最接近的近似值。 Package-privacy可以用作其包装类型的不完美保留。 但是对于跨越多个程序包的内部API的更复杂的应用程序,可见性修饰符不足,因此需要真正的模块。 使用项目Jigsaw,可以真正封装类,这使它们对于某些代码不可用,即使这些类被声明为公共类也是如此。 但是,基于所有类在运行时始终可用的假设下构建的Java程序可能需要进行根本性的更改。
对于最终用户应用程序的开发人员而言,此更改的根本性可能不如Java库和框架的维护人员。 库通常在编译过程中不知道其用户代码。 为了克服此限制,库可以回退到使用反射。 这样,用于依赖项注入的容器(例如Spring)可以实例化应用程序的bean实例,而框架在编译时并不知道bean类型。 为了实例化此类对象,容器只需将其工作延迟到运行时,直到它扫描应用程序的类路径并发现现在可见的bean类型。 对于这些类型中的任何一种,框架都将定位一个构造函数,该构造函数在解析所有注入的依赖项后会进行反射性调用。
一长串Java框架使用与反射配对的运行时发现。 但是在模块化环境中,如果不解决模块边界问题,就无法再运行以前的运行时分辨率。 使用项目Jigsaw,Java运行时断言每个模块仅访问在访问模块的描述符中声明为依赖项的模块。 此外,导入的模块必须将相关的类导出到其访问器。 依赖项注入容器的模块化版本无法将任何用户模块声明为依赖项,因此禁止进行反射访问。 实例化未导入的类时,这将导致运行时错误。
为了克服此限制,Jigsaw项目添加了一个新的API,该API允许在运行时包含其他模块依赖性。 使用此API并添加所有用户模块后,模块化的依赖项注入容器现在可以继续实例化在编译时不知道的bean类型。
但是这个新的API真的可以解决问题吗? 从纯功能的角度来看,此附加API允许迁移库以保留其功能,即使将其重新包装为模块也是如此。 但是不幸的是,模块边界的运行时强制要求在使用大多数反射代码之前先进行仪式舞蹈。 在调用方法之前,调用者需要始终确保相应的模块已经是调用者的依赖项。 如果框架忘记添加此检查,则会在编译期间抛出运行时错误而没有发现任何机会。
由于许多库和框架都过度使用了反射,因此可访问性的这种变化不太可能改善运行时封装。 即使安全管理器会限制框架添加运行时模块依赖项,强制执行此类边界也可能会破坏大多数现有应用程序。 更现实的是,大多数违反模块边界的行为并不表示真正的错误,而是由代码迁移不当引起的。 同时,如果大多数框架抢占了对大多数用户模块的访问权限,则运行时限制不可能改善封装。
当模块在其自身类型上使用反射时,此要求当然不适用,但是在实践中这种反射的使用非常少见,可以通过使用多态来代替。 在我眼中,在使用反射时强制执行模块边界与它的主要用例相矛盾,并使本来就不容易的反射API更加难以使用。
模块化资源
除了这个限制,目前还不清楚依赖注入容器将如何发现它应该实例化的类。 在非模块化应用程序中,框架可以例如期望给定名称的文件存在于类路径上。 然后,此文件用作描述如何发现用户代码的入口点。 通常通过从类加载器请求命名资源来获取此文件。 对于项目Jigsaw,当所需的资源也封装在模块的边界内时,这将不再可能。 据我所知,资源封装的最终状态尚未完全确定。 但是,当尝试当前的早期访问版本时,将无法访问外部模块的资源。
当然,拼图项目的当前草案中也解决了这个问题。 为了克服模块边界,已授予Java 预先存在的ServiceLoader类超能力。 为了使特定的类可用于其他模块,模块描述符提供了一种特殊的语法,该语法允许通过模块边界泄漏某些类。 应用此语法,框架模块声明它提供了某种服务。 然后,用户库声明框架可以访问该服务的实现。 在运行时,框架模块使用服务加载器API查找其服务的任何实现。 这可以用作在运行时发现其他模块的方式,并且可以替代资源发现。
乍看之下,这种解决方案似乎很优雅,但我仍然对此提议表示怀疑。 服务加载器API的使用非常简单,但同时,其功能也非常有限。 此外,很少有人对其自己的代码进行修改,这可以视为其范围有限的指标。 不幸的是,只有时间才能证明此API是否能够充分容纳所有用例。 同时,可以将单个Java类与Java运行时紧密地联系在一起,从而几乎不可能弃用和替换服务加载程序API。 在Java的历史背景下,已经讲述了许多看起来不错但又变味的想法,我发现创建这样一个神奇的中心很容易成为现实,它很容易成为实现瓶颈。
最后,还不清楚如何在模块化应用程序中公开资源。 尽管Jigsaw不会破坏任何二进制兼容性,但从以前一直返回值的ClassLoader::getResource调用返回null可能只是将应用程序埋在成堆的null指针异常下。 例如,代码操纵工具需要一种方法来定位类文件,这些类文件现在已经封装起来,这至少会阻碍它们的采用过程。
可选依赖项
服务加载器API无法容纳的另一个用例是可选依赖项的声明。 在许多情况下,可选的依赖项不被认为是一种好习惯,但实际上,如果可以将依赖项组合成大量排列,它们提供了一种便捷的方法。
例如,如果特定的依赖项可用,则库可能能够提供更好的性能。 否则,它将退回到另一个不太理想的选择。 为了使用可选的依赖关系,需要库根据其特定的API进行编译。 但是,如果此API在运行时不可用,则库需要确保永不执行可选代码,并退回到可用的默认值。 这样的可选依赖关系无法在模块化环境中表达,在模块化环境中,即使从未使用过依赖关系,在应用程序启动时都会验证任何声明的模块依赖关系。
可选依赖项的一个特殊用例是可选注释包。 今天,Java运行时将注释视为可选的元数据。 这意味着,如果类加载器无法定位注释的类型,则Java运行时将仅忽略所关注的注释,而不是抛出NoClassDefFoundError 。 例如, FindBugs应用程序提供了一个注释包,用于在用户发现相关代码为假阳性后抑制潜在的错误。 在应用程序的常规运行时期间,不需要特定于FindBugs的注释,因此它们不包含在应用程序包中。 但是,在运行FindBugs时,该实用程序会显式添加注释包,以使注释变为可见。 在拼图项目中,这不再可能。 仅当模块声明对注释包的依赖性时,注释类型才可用。 如果以后在运行时缺少此依赖项,则尽管注释不相关,也会引发错误。
非模块化
未命名模块的一部分
尽管这种选择退出可能是沉重反射框架的最佳解决方案,但是缓慢采用项目Jigsaw的确也违反了模块系统的目的。 由于时间的缺乏是大多数开源项目的主要限制,因此很可能会出现这种结果。 此外,许多开源开发人员都必须将其库编译为旧版Java。 由于模块化和非模块化代码的运行时行为不同,因此框架需要维护两个分支,以便能够使用Java 9 API遍历模块化包中的模块边界。 许多开源开发人员不太可能花时间来使用这种混合解决方案。
代码检测
在Java中,反射方法访问不是库与未知用户代码进行交互的唯一方法。 使用工具API ,可以重新定义类以包含其他方法调用。 例如,这通常用于实现方法级别的安全性或收集代码指标。
在检测代码时,通常会在类加载器加载Java类的类文件之前立即对其进行更改。 由于通常在紧接类加载之前应用类转换,因此当前无法预先更改模块图,因为未加载的类的模块是未知的。 如果检测代码在首次使用之前无法访问已加载的类,则可能会导致无法解决的冲突无法解决。
摘要
软件估算很困难,我们所有人都倾向于低估应用程序的复杂性。 Jigsaw项目对Java应用程序的运行时行为进行了根本性的更改,将发布推迟到彻底评估所有可能性是非常合理的。 当前,有太多未解决的问题,这是延迟发布日期的不错选择。
我希望模块边界根本不由运行时强制执行,而是保留为编译器构造。 尽管存在一些缺陷,Java平台已经实现了泛型类型的编译时擦除,并且该解决方案运行良好。 如果没有运行时强制实施,则在JVM上采用动态语言的模块也将是可选的,因为与Java中相同的模块化形式可能没有意义。 最后,我觉得当前严格的运行时封装形式试图解决一个不存在的问题。 在使用Java多年之后,我很少遇到无意间使用内部API引起严重问题的情况。 相比之下,我记得在很多情况下滥用本应为私有的API可以解决我无法解决的问题。 同时,Jigsaw仍无法解决Java中缺少模块的其他症状(通常称为jar hell) ,Jigsaw不能区分模块的不同版本。
最后,我认为向后兼容性的适用范围超出了二进制级别。 实际上,二进制不兼容通常比行为更改更容易处理。 在这种情况下,Java多年来成就斐然。 因此,方法合同应与二进制兼容性一样受尊重。 尽管项目Jigsaw从技术上讲不会通过提供未命名的模块来破坏方法契约,但是模块化基于其绑定对代码行为进行了微妙的更改。 我认为,这将使经验丰富的Java开发人员和新手都感到困惑,并导致重新出现运行时错误。
这就是为什么我发现强制运行时模块边界的价格与其提供的好处相比过高的原因。 OSGi是一个具有版本控制功能的运行时模块系统,适用于确实需要模块化的用户。 作为一个很大的好处,OSGi在虚拟机之上实现,因此不会影响VM行为。 另外,我认为Jigsaw可以为库提供一种规范的方式,使其在有道理的情况下选择退出运行时约束,例如对于大量反射的库。
翻译自: https://www.javacodegeeks.com/2015/12/project-jigsaw-incomplete-puzzle.html