Welcome 微信登录

首页 / 软件开发 / JAVA / 模块化Java简介

模块化Java简介2011-03-25 infoq Alex Blewitt 译:宋玮在过去几年,Java模块化一直是一个活跃的话题。从JSR 277(现已废止)到JSR 291 ,模块化看起来是Java进化过程中的必经一环。即便是基于JVM的未来语言,比如Scala, 也考虑了模块化的问题。本文是关于模块化Java系列文章中的第一篇,讨论模块化的含义 ,以及为什么要关注它。

什么是模块化?

模块化是个一般概念,这一概念也适用于软件开发,可以让软件按模块单独开发,各 模块通常都用一个标准化的接口来进行通信。实际上,除了规模大小有区别外,面向对象 语言中对象之间的关注点分离与模块化的概念基本一致。通常,把系统划分外多个模块有 助于将耦合减至最低,让代码维护更加简单。

Java语言并不是按照模块化思想设计的(除了package,按照Java语言规范 introduction一节的介绍,package类似于Modula-3模块),但是在Java社区依然有很多 实际存在的模块。任何一个Java类库实际上都是一个模块,无论其是Log4J、Hibernate还 是Tomcat。通常,开源和非开源的应用都会依赖于一个或多个外部类库,而这种依赖关系 又有可能传递到其他类库上。

类库也是模块

类库毫无疑问也是模块。对于类库来讲,可能没有一个单一接口与之通信,但往往却 有‘public’ API(可能被用到)和‘private’ package(文档中说明了其用途)。此 外,它们也有自己依赖的类库(比如JMX或JMS)。这将引起自动依赖管理器引入许多并非 必须的类库:以Log4J-1.2.15为例,引入了超过10个依赖类库(包括javax.mail和 javax.jms),尽管这些类库中有不少对于使用Log4J的程序来说根本不需要。

某些情况下,一个模块的依赖可以是可选的;换句话说,该模块可能有一个功能子集 缺少依赖。在上面的例子中,如果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产 生,以表示兼容性被打破)。所有这些都是基于惯例而非运行时已知约束。

模块化何时能派上用场?

作为一般概念,模块化有助于将应用分解为不同的部件,各个部件可以单独测试(和 开发)。正如上面所提到的,大多数类库都是模块。那么,对于那些生产类库提 供给别 人使用的人来说,模块化是一个非常重要的概念。通常,依赖信息是在构建工具(maven pom 或 ivy-module)里进行编码并被明确记录在类库使用文档中的。另外,高层类库开 发过程中需要修改较低层级类库bug,以提供更好支持的情况并不少见,即便低层类库的 最新版本已经对bug进行了修正。(可是有时候这种情况可能会导致出现一些微妙的问题 。)

如果一个类库是提供给他人使用的,那么它就已经是一个模块了。但是世上鲜有 “Hello World”这样的类库,也鲜有“Hello World”这样的模块。只有当应用足够大时 (或者是用一个模块化构建系统进行构建时),把应用划分为不同部件的概念就派上用场 了。

模块化的好处之一是便于测试。一个小模块(具有定义良好的API)通常比应用整体更 好测试。在GUI应用中尤其如此,GUI自身可能不好测试,但是其调用的代码却是可测试的 。

模块化的另一个好处是便于进化。尽管系统整体有一个版本号,但实际上,其下有多 个模块及相应版本(不论开源与否,总有一些类库——甚至是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并在运行 时中重新部署应用类的新版本。)