在过去几年,Java模块化一直是一个活跃的话题。从JSR 277(现已废止)到JSR 291,模块化看起来是Java进化过程当中的必经一环。即使是基于JVM的将来语言,好比Scala,也考虑了模块化的问题。本文是关于模块化Java系列文章中的第一篇,讨论模块化的含义,以及为何要关注它。php
模块化是个通常概念,这一律念也适用于软件开发,可让软件按模块单独开发,各模块一般都用一个标准化的接口来进行通讯。实际上,除了规模大小有区别外,面向对象语言中对象之间的关注点分离与模块化的概念基本一致。一般,把系统划分外多个模块有助于将耦合减至最低,让代码维护更加简单。html
Java语言并非按照模块化思想设计的(除了package,按照Java语言规范introduction一 节的介绍,package相似于Modula-3模块),可是在Java社区依然有不少实际存在的模块。任何一个Java类库实际上都是一个模块,不管其 是Log4J、Hibernate仍是Tomcat。一般,开源和非开源的应用都会依赖于一个或多个外部类库,而这种依赖关系又有可能传递到其余类库上。java
类库毫无疑问也是模块。对于类库来说,可能没有一个单一接口与之通讯,但每每却有‘public’ API(可能被用到)和‘private’ package(文档中说明了其用途)。此外,它们也有本身依赖的类库(好比JMX或JMS)。这将引发自动依赖管理器引入许多并不是必须的类库:以Log4J-1.2.15为例,引入了超过10个依赖类库(包括javax.mail
apache
和javax.jms
),尽管这些类库中有很多对于使用Log4J的程序来讲根本不须要。api
某些状况下,一个模块的依赖能够是可选的;换句话说,该模块可能有一个功能子集缺乏依赖。在上面的例子中,若是JMS没有出如今运行时 classpath中,那么经过JMS记录日志的功能将不可用,可是其余功能仍是可使用的。(Java经过使用延迟连接——deferred linking来达到这一目的:直到要访问一个类时才须要其出现,缺乏的依赖能够经过ClassNotFoundException
来处理。其余一些平台的弱连接——weak linking概念也是作相似的运行时检查。)浏览器
一般,模块都附带一个版本号。许多开源项目生成的发行版都是以相似log4j-1.2.15.jar的方式命名的。这样开发者就能够在运行时经过手动方式来检测特定开源类库的版本。但是,程序编译的时候可能使用了另外一个不一样版本的类库:假定编译时用log4j-1.2.3.jar
而运行时用log4j-1.2.15.jar
,程序在行为上依然可以保持兼容。即便升级到下一个小版本,仍然是兼容的(这就是为何log4j 1.3 的问题会致使一个新分支2.0产生,以表示兼容性被打破)。全部这些都是基于惯例而非运行时已知约束。app
做为通常概念,模块化有助于将应用分解为不一样的部件,各个部件能够单独测试(和开发)。正如上面所提到的,大多数类库都是模块。那么,对于那些生产类库提 供给别人使用的人来讲,模块化是一个很是重要的概念。一般,依赖信息是在构建工具(maven pom 或 ivy-module)里进行编码并被明确记录在类库使用文档中的。另外,高层类库开发过程当中须要修改较低层级类库bug,以提供更好支持的状况并很多 见,即使低层类库的最新版本已经对bug进行了修正。(但是有时候这种状况可能会致使出现一些微妙的问题。)less
若是一个类库是提供给他人使用的,那么它就已是一个模块了。可是世上鲜有“Hello World”这样的类库,也鲜有“Hello World”这样的模块。只有当应用足够大时(或者是用一个模块化构建系统进行构建时),把应用划分为不一样部件的概念就派上用场了。eclipse
模块化的好处之一是便于测试。一个小模块(具备定义良好的API)一般比应用总体更好测试。在GUI应用中尤为如此,GUI自身可能很差测试,可是其调用的代码倒是可测试的。maven
模块化的另外一个好处是便于进化。尽管系统总体有一个版本号,但实际上,其下有多个模块及相应版本(不论开源与否,总有一些类库——甚至是Java版本—— 是系统所依赖的)。这样,每一个模块均可以本身的方式自由地进化。某些模块进化得快些,另外一些则会长期保持稳定(例如,Eclipse 3.5 的org.eclipse.core.boot
从2008年2月以来一直没有改变过)。
模块化也可给项目管理带来方便。若是一个模块公布的API可供其余模块预先使用,那么各个模块就能够由不一样的团队分别开发。这在大型项目中一定会发生,各个项目子团队能够负责不一样模块的交付。
最后,将一个应用程序模块化,能够帮助识别正在使用依赖类库的哪一个版本,以便协调大型项目中的类库依赖。
不管在编译时仍是运行时,Java的classpath都是扁平的。换句话说,应用程序能够看到classpath上的全部类,而无论其顺序如何(若是没 有重复,是这样;不然,老是找最前面的)。这就使Java动态连接成为可能:一个处于classpath前面的已装载类,不须要解析其所引用的可能处于 classpath后面的那些类,直到确实须要他们为止。
若是所使用的接口实现到运行时才能清楚,一般使用这种方法。例如,一个SQL工具能够依赖普通JDBC包来编译,而运行时(能够有附加配置信息)能够实例化适当的JDBC驱动。这一般是在运行时将类名(实现了预约义的工厂接口或抽象类)提供给Class.forName
查找来实现。若是指定的类不存在(或者因为其余缘由不能加载),则会产生一个错误。
所以,模块的编译时classpath可能会与运行时classpath有些微妙的差异。此外,每一个模块一般都是独立编译的(模块A多是用模块C 1.1 来编译的,而模块B则多是用模块C 1.2 来编译的),而另外一方面,在运行时则是使用单一的路径(在本例中,便可能是模块C的1.1版本,也多是1.2版本)。这就会致使依赖地狱(Dependency Hell),特别当它是这些依赖传递的末尾时更是这样。不过,像Maven和Ivy这样的构建系统可让模块化特性对开发者是可见的,甚至对最终用户也是可见的。
Java有一个很是好的底层特性,叫作ClassLoader, 它可让运行时路径分得更开。一般状况下,全部类都是由系统ClassLoader装载的;但是有些系统使用不一样的ClassLoader将其运行时空间 进行了划分。Tomacat(或者其余Servlet引擎)就是一个很好的例子,每一个Web应用都有一个ClassLoader。这样Web应用就没必要去 管(不管有意与否)在同一JVM中其余Web应用所定义的类。
这种方式下,每一个Web应用都用本身的ClassLoader装载类,这样一个(本地)Web应用实现装载的类不会与其余Web应用实现相冲突。但这就要 求对任何ClassLoader链,类空间都是一致的;这意味着在同一时刻,你的VM能够同时从两个不一样的Classloader中各自装载一个Util.class
, 只要这两个ClassLoader互相不可见。(这也是为何Servlet引擎具备无需重启便可从新部署的能力;扔掉了一个ClassLoader,你 也就扔掉了其引用类,让老版本符合垃圾回收的条件——而后让Servlet引擎建立一个新的ClassLoader并在运行时中从新部署应用类的新版本。)
构建一个模块化系统其实是把系统划分红(有可能)可重用模块的过程,并使模块间耦合最小化。同时,其也是一个减小模块需求耦合的过程:例如,Eclipse IDE许多plugin对GUI和非GUI组件(如jdt.ui
和jdt.core
)的依赖是分开的,这样就能够在IDE环境以外使用这些非GUI模块(headless builds、分析及错误检查等等)。
除了做为总体的rt.jar
以外,任何其余系统均可以被分解为不一样的模块。问题是这么作是否值得?毕竟,从头构建一个模块化系统比起把一个单模块系统分割成多个模块要容易得多。
之因此这样,缘由之一是跨越模块边界的类泄漏。例如,java.beans
包逻辑上不该该依赖于任何GUI代码;但是Beans.instantiate()
所使用的java.beans.AppletInitializer
引用了Applet
,这必然致使对整个AWT的依赖。所以从技术上讲java.beans
有依赖于AWT的选项,尽管常识告诉咱们不该该有。若是核心java类库从一开始就采用了模块化方法来构建,那么这种错误早在API公布以前就发现了。
有些状况下,一个模块看上去不能再被划分红子模块了。但是,有时候相关功能保持在同一个模块中是为了便于组织,当须要的时候还能够再进一步分解。例如,对重构的支持起初是Eclipse JDT的一部分,如今被抽出为一个模块,以便其余语言(如CDT)利用其重构能力。
许多系统都是经过plugin概念进行扩展的。在这种状况下,宿主系统定义了一套plugin必须遵循的API及plugin注入方式。许多应用(如Web浏览器、IDE及构建工具)一般都是经过提供带有适当API的插件来对应用进行定制的。
有时候这些plugin受到限制或只有一些普通操做(音频或视频解码),可是组织起来效果也很是不错(例如,IDE的众多plugin)。有时候,这些 plugin能够提供本身的plugin,以便进一步定制行为,使得系统具备更高可定制性。(但是,增长这些中间层级会使系统难以理解。)
这种plugin API成为各个plugin必须遵照的契约的一部分。这些plugin本身也是模块,也面临依赖链和版本问题。因为(特定)plugin API演化的复杂性,所以plugin本身也面临这一问题(必须维持向后兼容性)。
Netscape plugin API成功的缘由之一是其简单性:只需实现少许的函数。只要宿主浏览器用适当的MIME类型将输入重定向,plugin就能够处理其余事情。但是,更复杂的应用(如IDE)一般须要更紧密集成各个模块,所以须要一个更复杂的API来推进。
目前,Java领域存在许多模块化系统和plugin体系。IDE是名气最大的,IntelliJ、NetBeans和Eclipse都提供了其本身的 plugin系统做为其定制途径。并且,构建系统(Ant、Maven)甚至终端用户应用(Lotus Notes、Mac AppleScript应用)都有可以扩展应用或系统核心功能的概念。
OSGi是Java领域里无可辩驳的最成熟的模块系统,它与Java几乎是如影相随,最先出现于JSR 8,可是最新规范是JSR 291。 OSGi在JAR的MANIFEST.MF文件中定义了额外的元数据,用来指明每一个包所要求的依赖。这就让模块可以(在运行时)检查其依赖是否知足要求, 另外,可让每一个模块有本身的私有 classpath(由于每一个模块都有一个ClassLoader)。这可让dependency hell尽早被发现,可是不能彻底避免。和JDBC同样,OSGi也是规范(目前是4.2版),有多个开源(及商业)实现。由于模块不须要依赖任何OSGi的特定代码,许多开源类库如今都将其元信息嵌入到manifest中,以便OSGi运行时使用。有些程序包没有这么作,也能够用bnd这样的工具,它能够处理一个已有的JAR文件并为其产生合适的默认元信息。自2004年Eclipse 3.0 从专有plugin系统切换到OSGi以后,许多其余专有内核系统(JBoss、WebSphere、Weblogic)也都随之将其运行时转向基于OSGi内核。
最近建立的Jigsaw项目是为了模块化JDK自身。尽管其是JDK内部的一部分,而且极可能在其余SE 7 实现中不被支持,可是在该JDK以外使用Jigsaw并没有限制。尽管仍在开发当中,Jigsaw还极可能成为前面提到的JSR 294的参考实现。最低要求SE 7(加上目前还没有Java 7的事实)说明了Jigsaw仍在开发中,并且运行在Java 6或更低版本上的系统基本上是用不上了。
为了鼓励采用标准模块化格式,JSR 294专家组目前正在讨论简单模块系统提议:在这一提议中,Java类库(来自Maven库及Apache.org)的开发者可以提供让Jigsaw和OSGi系统都能使用的元信息。结合对Java语言的微小变更(最值得关注的是增长的module
关键字),这一信息能够在编译时由高级编译器产生。运行时系统(如Jigsaw或OSGi)可使用这些信息来校验所安装的模块及其依赖。
本文讨论了模块化的通常概念,以及在Java系统中是如何实现的。因为编译时和运行时路径可能不一样,有可能会产生不一致的类库需求,从而致使依赖地狱。然 而,plugin API容许装载多种代码,但其必须遵循宿主的依赖处理规则,这又增长了发生不一致的可能性。为了防止这种状况出现,像OSGi这样的运行时模块化系统能够 在决定应用是否能被正确启动以前就校验各项要求,而不是在运行时不知不觉发生错误。
最后,有人在正在进行中的JSR 294的邮件列表中提出,要为Java语言建立一个模块系统,其能够彻底在Java语言规范中被定义,以便Java开发者能够产生带有编码依赖信息的标定过版本的模块,该模块之后能够用于任何模块系统。
查看英文原文:Modular Java: What Is It?。
转自:http://www.infoq.com/cn/articles/modular-java-what-is-it/