Nicolai Parlog是一位热情的软件project师。数字版权与开源软件的狂热拥护者;他对AssertJ、ControlsFX、FindBugs及Property Alliance等项目都做出过重要的贡献。
近日,Parlog就Jigsaw项目撰写了一篇文章。谈到了Jigsaw项目的一些不足以及改进之处。
Jigsaw项目有着雄心勃勃的雄伟目标。其目标之中的一个就是彻底摆脱极易出错且问题多多的类路径机制中的JAR地狱问题。只是。尽管该项目的其它目标会在不久的将来得以实现,但解决JAR地狱问题这一目标似乎并非那么easy的。
为了更好地理解我们接下来要讨论的内容,首先来看一下JAR地狱问题。接下来介绍Jigsaw项目将会解决问题的哪些方面,以及为什么说Jigsaw所尝试解决的问题并不会对整个问题域产生本质的影响。
最后,我们来看一下官方对于这个话题的立场,并给出怎样防止出现模块地狱的提案。
JAR地狱问题
JAR地狱存在着例如以下循环问题:
- 表述不清以及传递性依赖
- 遮蔽
- 版本号冲突
- 复杂的类载入
依据构建工具与组件系统(JDK开发人员称之为容器)为我们所带来的诸多功能与特性,我们能够觉得表述不清以及传递性依赖问题已经在非常大程度上得到了解决。遮蔽问题至少得到了缓解,而复杂的类载入也不再是老生常谈的问题了。这样,版本号冲突就成为JAR地狱中最为严重的一个问题了,它影响到了非常多非常多项目每天的更新决策。
Jigsaw将会带来哪些改变?
我之前曾就Jigsaw项目会为Java 9带来哪些新特性专门写过文章进行过介绍。只是这里将从不同的视角进行阐述。
首先,它会受到当前的早期訪问构建版的影响;其次,我们这里仅仅从与JAR/模块地狱相关的角度进行介绍。
Jigsaw为Java带来的核心概念就是模块化。
简而言之。模块就像JAR一样,同一时候带有一些附加信息与特性。这些信息包括了模块的名字以及模块所依赖的其它模块的名字。
依赖
当编译器与JVM在处理模块时,他们会解析这些信息。在编译或启动时。他们会通过模块路径传递性解析全部依赖。整体来说。这相似于类路径扫描。只是如今寻找的是整个模块而非单个类,对于JVM来说。这是在启动期而非执行期进行的。
假设在模块路径上无法找到全部依赖。那么解析模块的传递性依赖就会失败。
这显然能够解决表述不清。以及无休止的传递性依赖的问题。
我觉得这是个非常棒的做法。Java语言如今正式知道关于依赖的信息了。全部工具(编译器与JVM等)都能理解这一点并正常使用!
只是,我觉得这并不会对开发人员每天的工作产生多少积极的影响。由于如今非常多既有的基础设施都已经解决问题了,比方说构建工具等。
遮蔽
Jigsaw消除了遮蔽的问题。模块系统能够确保每一个依赖都会被还有一个模块所实现,每一个模块都会读取至多一个模块,定义了同名包的模块之间并不会相互干扰。
更准确地说,模块系统在遇到模糊不清的情况时就会终止并报错,比方说两个模块将同样的包导出到同样模块中。
版本号冲突
我们觉得第三方库的版本号冲突是JAR地狱最为难以解决的问题。最直接的解决方式就是一个模块系统能够载入同一个模块的不同版本号。这须要确保这些版本号之间不存在互相交互的情况。问题在于:在单个配置中,不是必需支持一个模块的多个版本号。
实际上。当前的构建既不会创建,也无法理解模块版本号信息。曾有人使用了一些变通办法。最丑陋,同一时候也是最可行的办法就是重命名出现冲突的构件。这样他们就不再是同样模块的两个不同版本号了,而是两个全然不同的模块。只是。这样的做法最后证明也是行不通的。
显然,确保“定义了同名包的模块之间不会相互干扰”是在两个模块导出同样包时拒绝不论什么启动配置来实现的。
即便没有模块读取他们亦如此!
复杂的类载入
模块与类载入器之间怎样交互以及怎样改变类载入的复杂性是个非常棘手的问题。实际上,模块系统对模块与类载入器之间的关系并没有做多少限制。类载入器能够从一个模块或是多个模块来载入类型,仅仅要模块之间不存在相互干扰的情况。而且每一个模块中的类型仅仅由一个载入器载入就可以。
因此。类载入器与模块之间是一对多的关系。
模块地狱?
既然依赖与遮蔽问题已经得到了解决,而且类载入问题也得到了改进。那我为何还要讨论模块地狱呢?就是由于版本号冲突么?没错!假设Jigsaw想要解决JAR地狱问题,它就须要特别注意版本号冲突问题。否则,非常多项目并不会出现什么起色。
他们依旧要面对版本号冲突问题,而且会陷入到自己定义类载入器的梦魇中。
提案
我的提案是让开发人员与构建工具能够传递一些额外的信息,这些信息能够解决一些含糊不清的问题。传递这样的信息的两种常见方式是命令行与配置文件。假设使用命令行參数。那么每次启动时都须要输入一次。依据信息的多少以及项目的规模。这样的做法可能会变得非常乏味。
能够通过构建工具来创建配置文件,然后再通过命令行指定配置文件。
这看起来是个不错的解决方式。
眼下,初始模块与全部的传递性依赖都是通过单个配置来解析的。这形成了单独的一个层次。只是,我们能够在执行期将同样模块的多个版本号载入到不同层次中,这正是组件系统要做的事情。总的来说,我的建议就是通过多个层次来显式指定配置。