GMTC北京站本周日开幕,58个议题全部上线,点击查看 了解详情
写点什么

关于 Java 7 模块系统

2009 年 5 月 11 日

最近,新的 Java 模块系统已经受到了大量的关注。在观看过 Devoxx 关于 Jigsaw 的一段演示后,我很兴奋,觉得它应该会是针对复杂类路径版 本问题和 JAR 陷阱等问题的解决方案。开发者最终能够使用他们所期望的任何 Xalan 版本,而无需被迫使用授权机制。不幸的是,通往更加有效的模块系统的 征途并不是很清晰。

在研究确实问题之前,我们先来看一些基本概念:

模块化

模块化是解决复杂性问题很重要的工具。把应用分成不同的部分(模块、库、包、子项目和组件),再分别进行计算,是行之有效的方式。模块化的最终目的是能定义出一套 API 用于模块间的沟通。

如果模块间所有的通讯都只通过这种 API 来实现,那么模块是松耦合的,于是:

  • 改变某个模块的实现会很容易
  • 开发和测试各个模块能很容易独立开来

面向对象模式也是类似的道理。在 OOP 中,理想的状况是拥有大量小的、可重用的、简单并分离良好的对象。在模块系统中,就可以完美地实现小的、可重用的、简单并分离良好的模块。它们的想法和最初的动机是完全一样的,只是规模有所不同。

逻辑分离

传统上,Java 中有两种办法来实现模块化。逻辑分离是最自然的方式。它包括将应用程序分割成逻辑模块(子项目),最后再部署成一个完整的应用。通过定义正确的包来实现逻辑分离也是可能的,但更通用的办法是把应用分割成一些存档文件(也就是 JAR 包)。逻辑分离里能促进模块的重用,有助实现模块间的松耦合。你甚至有可能定义一个 API,然后宣布所有模块间的通讯都要通过这个给定的 API 来实现。这样的想法有个大问题,那就是你很难强破大家都采用这种限制性用法,而且没有任何一种机制能够确保这个 API 的用法。你也没法把那些应用通过给定模块来使用的类和作为公共 API 一部分的类区分开来。如果一个类是“ 公共的”,那它就可以被任何其他类使用,无论调用它的那个类属于哪个模块。另一方面,受保护的或者包级可见性的类在其模块内部的调用也有限制。通常来说,涵盖了一些包以及包中类的模块需要能够互相调用。因此即使某个应用是由一些逻辑模块组成,但如果这些模块是耦合的,那么分离也根本没有用处。

物理分离

另外一个传统的办法就是物理上的分离。你可以通过将应用分割成不同的组件,然后把每个组件部署到不同的 JVM 上而实现分离。这些组件间通过远程访问机制进行通讯,比如 RMI、CORBA 或者 WebServices。物理分离实现了分离,也实现了松耦合,但负面影响是开支很大。为实现分离而专门采用远程访问机制,有点杀鸡用牛刀的味道。这会增加开发和部署不必要的复杂性,性能上所受到的影响也不能忽视的。

模块系统

模块系统的作用位于逻辑分离和物理分离之间。它强调模块分离,但各个模块仍然部署到同一个 JVM 中,而且模块间的通讯由简单传统的方法调用组成,因此不会有运行时的开支负担。在 Java 生态系统中最流行的模块框架是 OSGi 。它是一个成熟的规范,具有几个不同的实现。在 OSGi 中,模块被称作 bundle,每个 bundle 等同于一个 JAR。每个 bundle 也包含一个 META-INF/MANIFEST.MF 文件,这个文件会宣布导出哪些包(package)以及导入哪些包。只有那些导出包中的类才能被其他 bundle 所使用,而其他包都只面向包的内部成员,包里的类也只能在自身 bundle 中使用。

比如下面这个声明:

复制代码
Manifest-Version: 1.0
Import-Package: net.krecan.spring.osgi.common
Export-Package: net.krecan.spring.osgi.dao
Bundle-Version: 1.0.0
Bundle-Name: demo-spring-osgi-dao
Bundle-SymbolicName: net.krecan.spring-osgi.demo-spring-osgi-dao

这个规范指定了名叫 demo-spring-osgi-dao 的 bundle,它要导入包名为 net.krecan.spring.osgi.common 中的类,并导出包名为 net.krecan.spring.osgi.dao 中的类。换句话说,这段声明表明其他模块只能使用 net.krecan.spring.osgi.dao 包。相反地,这个模块要使用的则只是 net.krecan.spring.osgi.common 包,而且也可能会由 OSGi 来提供专门的模块负责导出这个包。当然你完全可以在导入和导出声明中定义多个包名。

需要特别注意的是,OSGi 的模块化是构建在 Java 之上的。它不是语言的一部分!这里的模块分离虽然可以由 GUI 来执行,但不可以由编译器来执行。运行基于 OSGi 的应用时,你会需要一个 OSGi 的容器。这个容器可能是运行时环境的一部分,比如在 Spring DM 服务器中,也可能是嵌入在应用程序中的。这个容器不仅负责提供分离,也提供了其他诸如安全、模块管理和生命周期管理之类的服务。OSGi 还提供大量其他有趣的特性,但这些并不是本文所要关注的。

关于JSR-277 曾经有很多争议,这个JSR 一度跟OSGi 有部分重复。连续好几个月,双方的专家都极力辩论谁更优秀,直到JSR-277 被宣布放弃,而新的模块系统将会是Java 7 的一部分。

JSR-294

这个新的模块系统的第一部分就是 JSR-294 ,即所谓的超级包。也正是这个规范阐释了 Java 语言的模块部分的概念。

JSR-294 引入了新的可见性关键字“module”。如果一个成员拥有这样的可见性,那就意味着它只对同一模块中的成员可见。它会创建一个内部的 API,只有模块本身能调用。就此看来,“public”关键字应当只在声明一个公共的 API 时才用。而在其他情况下,应当使用“module”或者有更多限制的可见性关键字。当然,一旦语言中有了“module”关键字,那么模块之间的可见性限制将会由编译器来负责检查。

JSR-294 也允许定义依赖性。你可以在某个给定版本中,定义某个模块依赖于另一模块。比如:

复制代码
//org/netbeans/core/module-info.java
@Version("7.0")
@ImportModule(name="java.se.core", version="1.7+")
module org.netbeans.core;

最后一句表明“org.netbeans.core”模块依赖“java.se.core”的 1.7 版本或者更高。这类似于 Maven 的依赖性或者 OSGi 的导入。你也可以暂时不要管这些语法,因为将来语法可能会另有变化。重要的是,这儿的依赖是在 module-info.java 中定义的,会被编译成 class 文件。而 OSGi 中,依赖则是在普通的文本文件中定义的。

Jigsaw 项目

Jigsaw 项目是这个新模块系统的第二部分。我预计它会是 JSR-294 特定于 Sun 的实现,也会是 Sun JDK 的模块化实现。既然创建完整的JDK 模块化是有必要的,Sun 就希望把标准库分装成模块。这直接简化了JRE 中的内容整合。整个JRE 除了Swing 之外的所有内容因此都能够在移动设备上运行。它还有可能为语言引入新的标准API,而无需再等待整个平台的新版本发布。目前看起来,这个项目绝对有希望实现。

但我对此还有个担忧,那就是,a href=“ http://blogs.sun.com/mr/entry/jigsaw ”> 专有的 Jigsaw 和 JSR 标准之间的关系并不清晰,正如 Mark Reinhold 所说的:

对 Jigsaw 的投入无疑会创建出一个简单的、低层次的模块系统,它的设计会严格地朝着 JDK 模块化的目标而发展。开发人员可以把这个模块系统运用到他们的代码中去,Sun 对这个模块系统也会是绝对的支持,但它不会是 Java SE 7 平台规范的官方部分,也可能不会被其他 SE 7 实现所支持。

这段话说的不是很清楚,当中有很多疑问。他的意思是说创建的模块只能在 Sun JRE 中运行吗?还是想说,如果开发者写了“@ImportModule(name="java.se.core", version="1.7+")”,那么这个模块只能在 Sun JRE 中运行,而不能在 IBM JRE 环境中运行吗?或者他的意思是不是说 Sun 会以某种方式把它的 JRE 分割成许多模块,而 Oracle 会选择另外的方式去分割吗?(译者注:至少现在看来,不太会有这样的可能了,因为 Oracle 刚刚收购了 Sun)。我们希望都不是,因为还有“编写一次,到处运行”的原则。

细究起来问题更多。我们并不清楚 Jigsaw 项目的主要目标是什么。据项目本身所宣布的主要目标来看,它要实现的是 Sun JRE 的模块化,但如果纯粹是要实现模块化的话,就不需要对语言做任何改变。Sun 可以对 JRE 进行模块化,而不修改 Java 语言本身。

这些语言上的变化会不会成为 Sun JRE 模块化带来的副产品?如果是,那就彻底错了!语言变化必须是一等公民,而不是专属的副产品。

依赖

我的另外一个担心在于依赖性。如果依赖性由模块系统来管理,那就不再需要 classpath 了。一方面这很不错,因为 classpath 经常会导致所谓的 JAR 地狱问题。但另一方面,classpath 也是极度灵活的,我恐怕这种灵活性是不可能由一个静态的模块依赖能够拥有的。让我们来看看为什么:

部署时依赖

Java 中有两种类路径(classpath)。一个是构建路径(buildpath),它用在构建时。另外一个是类路径,用在运行时。两者几乎相同,但又不完全是。经典的例子就是 JDBC 驱动。在构建时,你不需要指定 JDBC 驱动,JDBC 接口是 Java 核心库的一部分。但在运行时,你就有必要确保类路径中有 JDBC 的驱动。如果某个编程人员需要修改数据库连接,他只需要在配置文件中修改驱动类的名称,并把驱动 jar 文件添加到类路径就可以了。如果所有的依赖需要在编译时指定,开发者很明显无法做到这点。当然,在 Java EE 中,他可以使用 JNDI 数据源,但在 Java SE 中没有类似的东西,一旦修改 JDBC 驱动,就不得不重新编译整个应用,这明显很不靠谱。

通常来说,重新编译不太可能。在一些组织中,最终的应用是由所谓的应用装配器的模块组装而成的。开发者没有源代码,他只是把一些 JAR 放在一起,修改一下配置文件,然后创建最终的包。应用装配器的角色甚至在 Java EE 的规范中都有提到。

可选依赖

类似的问题就是可选依赖。我们假设我们要做一个像 log4j 这样的日志框架。这个库可以对 JMS 记录日志,因此 JMS 包必须涵盖在构建路径中。但 99% 的用户并不使用 JMS 日志,因此他们不需要把依赖放在类路径中。对于这样的问题,必须要有某种机制来解决。我们需要一个库来构建这个模块,这种依赖对最终用户来说则是可选的。当然,完美的情况是,JMS 功能会是个独立模块,可我们并不是生活在一个完美的世界,而且某些时候用这种方式来分割项目也不太现实。

依赖冲突

另外一个大问题就是依赖冲突。如果你用过 Maven,就不难理解我在说什么了。大多数企业应用都会用到大约十几个第三方库,它们之间的互相依赖有时就会发生冲突。比如,一个开发者想要使用 Hibernate,它依赖 commons-collections 2.1.1,他还想用 commons-dbcp,却需要依赖 commons-collections 2.1。开发者自己或者应用装配器需要决定怎样解决此类问题。他要么决定在应用中只用某个特定版本的库,要么决定在应用的不同部分采用不同版本的类库。重要的是,这些问题无法自行解决。它总需要由某个了解各个模块在应用中如何运作的人来作决定,而这个人又要能识别不同版本间可能存在的不兼容性。

关于 Java 依赖性,还有许多东西本文不展开讨论,但需要铭记的一点是,它们不是静态的。一个应用的构件可能采用了某套类库,而它的运行却需要另外一套完全不同的库。所有模块系统必须以某种方式把这些问题解决掉。Maven 具有大量关于如何配置依赖,以及如何处理依赖冲突等等的选项,但它只是个构建系统。最糟糕的情况是需要手动配置类路径。OSGi 则是另外一种情形。它只处理运行时(部署时)依赖,不管构建时。新的 Java 模块系统会同时支持构建时和运行时依赖(我猜测),甚至会把既有的复杂问题变得愈加复杂。

总结

当然,我相信 Sun 的工程师并不想要破坏 Java 本身。我想他们也是为了让 Java 变得更好、更易于使用,但我担心政治和市场因素会远大于技术影响。再次声明,这不会仅仅是个 API 的变化或者是特定于 Sun 的变化。这会是语言级别的变化!一旦语言被改变了,一旦添加了“module”关键字,就不会再有回头路。到那时,Java 中会有个模块系统,无论喜不喜欢,我们都非得要用到这个模块系统。真得很难想象带模块化的 JVM,也很难想像 Java 语言中会有个 “module”关键字,而我们还要在这之上使用 OSGi。

参考

Luka Krecan 是个 Java EE 开发者兼自由作者。他在现实世界中为实体公司工作,但闲时他在理想而完美世界中围绕编程而写作


给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2009 年 5 月 11 日 23:127005
用户头像

发布了 127 篇内容, 共 37.3 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

week10

不在调上

微服务架构的一些思考

Acker飏

架构师训练营 W10 学习心得

telliex

架构师训练营 第十周 作业

CR

DDD

GalaxyCreater

架构

第十周作业

刘卓

[高冷面试]好不容易走到HR,结果被HR盘了,14题带走

我是程序员小贱

centos中Anaconda的安装以及keras安装

我是程序员小贱

week10作业1

LeetCode题解:88. 合并两个有序数组,双指针+从前往后+使用新数组Copy,JavaScript,详细注释

Lee Chen

前端进阶训练营

一网打尽 Java 并发模型

cxuan

Java 后端 并发

第十周学习总结

潜默闻雨

架构师训练营 W10 作业

telliex

FastDFS不同步怎么破

心平气和

Binlog 同步 fastdfs

linux终端的快捷命令汇总

良知犹存

Linux

悄咪咪提高团队幸福感 & Surprise!

Kerwin

Java 开源项目

架构师训练营 week10 - 学习总结

devfan

SpringBoot 实战:一招实现结果的优雅响应

看山

springboot 实战

计算机网络基础(十八)---传输层-TCP的流量控制

书旅

TCP 计算机网络 协议栈 网络层 流量控制

第十周作业

Geek_a327d3

week10 小结

Geek_196d0f

第十周学习总结

刘卓

第十周命题作业

菲尼克斯

计算机网络怎么学?学会这几个工具有助你理解网络协议!

我是程序员小贱

翻译: Effective Go (4)

申屠鹏会

golang 翻译

第10周作业

小胖子

架构师训练营 week10

devfan

微服务架构

阿飞

微服务架构

架构师训练营 第十周 总结

CR

mini-vue之proxy代理

晓枫

vue.js

B 站收藏 10W+,GitHub 标星 6K+,肝了这门计算机速成课!

JackTian

GitHub 编程 程序员 B站 计算机基础

关于Java 7模块系统-InfoQ