编译环境对于今日的Java企业级应用程序来说,越来越难于管理了。堆积如山的代码,配置文件,以及对第三方的依赖(third-party dependencies)都使得管理编译环境变得困难。
简而言之,我们勉强接受那种把所有的源代码放在一个根目录下,所有的配置文件放在另一个根目录下,而第三方类库也这样处理的做法。但是企业级编译环境很少这么做。今日的企业级Java项目,在结构,功能,以及组织上都很复杂。它们通常都有大量的源代码和支持资源(属性文件,图片,等等。编者注:原文为supporting artifacts,直译为支持物件,但这里根据上下文意译为支持资源较妥)要去管理。有这么多的东西去管理,当一个开发团队试图去建立一个优化的编译方案时,他们常常感到困惑和挫败。
如果,不管这个项目有多大,我们的编译环境都能够在统一的构架中简洁地处理我们所有的源代码,事情是不是会变得好一些呢?本文将展示一个Ant编译环境的例子,它来自我对多年来的多个项目的经验的修改。此时此地,它或许不是最好的方案,但是它的确经历了时间的考验,也一定会帮助你建立并运行在大多数项目上,不管是大是小。
警告
先就一些问题说明一下,这样你就不会读完了这篇文章才发现它对你没有任何价值:
? 本文基于对Ant的了解。它是针对那些会用并喜欢Ant的读者的。
? 这里所说的编译环境是指模组(modular)和模块(module),而模块又是由目录和子目录来定义的。(译者注:模组modular是模块module的集合。它由多个独立的模块构成。)这意味着文件和源代码被存放在许多不同的目录中。因此,如果你使用类似Eclipse或IntelliJ Idea这种可以帮你管理类和文件的位置的IDE工具的话,本文对你会更加有益。当然,你也可以使用文本编译器,但是恐怕你会发现你频频地在多棵“目录树”上爬上爬下。
概念
首先,让我们来谈及掉隐藏在编译环境之后的几个核心概念。它们是模组,层级结构(hierarchical),和资源驱动(artifact-driven)。它们确切的含义又是什么呢?
模组
模组编译是指围绕软件模块来进行组织的一种编译方式。一个模块是一个逻辑的,集合的,功能性单元,对应于系统中的一个特性。对于编译环境而言,一个模块表现为源代码和配置文件的一个自我包含集合(self-contained collection),这些源代码和配置文件用来构建表现了模块所对应的那个命名特性的软件。它几乎和你修订控制系统(RCS:Revision Control System)(例如CVS或者Subversion)中的目录树是一一对应的。举几个例子:security, administration, wiki, email都可以是一个模块。
层级结构
层级结构编译是指含有分层模块的编译方式。也就是,对于一个模块,它可能是由更小的,更特定的子模块(submodule)来构成的。
如果一个模块含有子模块,那么它有责任保证那些子模块以合适的方式被编译。
随后,我们会讨论例子是如何应用层级结构的概念来建立编译环境的。
物件驱动
物件驱动编译是指每个存在的模块(module)或子模块(submodule),都是为了产生一个单独的,可部署的物件。在Java项目中,这些物件主要是.jar,.war,或.ear文件。在其他类型的编译中,它们通常是二进制可执行文件或动态连接库(.dll或.so)。
编译环境的例子也是物件驱动的,我们将会讨论它是如何创建可部署的物件的。
尽管这三个概念都很容易理解,但结合起来用在编译环境中的话,它们会变得非常强大。
现在让我们来看看编译环境是如何组织的。
模组结构
当有很多要去实现的时候,把问题分解为若干个小的部分是个很有效的方法。我们需要一个好的分而治之(divide-and-conquer)的技术来帮助我们来管理大量的源码。在编译环境中创建编译模块是个好方法。
我们通过在应用程序的根目录下创建一个目录来创建一个模块。这个新的目录成为这个模块的基础。在每个模块目录下,我们存放与其相关的文件和源码。
这是一个示例程序的编译环境,按照模块来组织:
appname/
|-- admin/
|-- core/
|-- db/
|-- lib/
|-- ordermgt/
|-- reports/
|-- web/
|-- build.xml
下面是每个节点的含义:
? 除了lib/ 以外的每个目录都是一个模块。在这个例子中,admin模块提供了POJO的实现,它容许某人来管理应用(例如,创建用户,授权等等)。同样的,reports模块中,有能够产生报告的组件的实现。而core 模块中是那些在很多或全部模块中都用到的组件,它们不是真正地和系统的某个功能相联系。(例如,StringUtil 类)通常,其他地所有模块都会依赖核心(core)模块。
其他模块与admin, reports, 及core模块一样:他们有着各自的自包含的系统功能,并与其他模块区别开来。此外,由于我们的范例应用可以支持基于web的交互,我们还可以有一个web模块,包含了用以创建一个.war文件所需要的一切内容。
? lib/ 目录比较特殊。它含有应用程序编译或运行所需地所有第三方.jars文件。我们把其他模块所需的所有第三方.jars文件放在这个目录中,而不是它们自己的模块中。原因如下:
1. 在一个地方更便于管理对第三方的依赖(third-party dependencies)。可以在一个模块的build.xml 文件中,利用Ant的<path 语句来定义改模块是否使用这些库文件。
2. 通过排除重复.jars文件的可能性,从而避免了装载类或API的版本冲突。如果有不止一个模块使用了一个负责存储commons-logging.jar文件的Jakarta Commons Logging模块,会发生什么情况?假设每个模块都持有Jakarta Commons Logging模块的备份,这样就会有一个潜在的问题??一个模块所持有的备份和另外一个模块所持有的版本不同。当应用程序开始运行,只有第一个在classpath上找到的.jar文件被载入以满足所需,这就潜在地引起了与其他模块的冲突。我们通过在根目录下只持有一个.jar文件来避免这种冲突。
3. 对第三方的依赖随你的源码改变版本。浏览很多项目,会发现,这是你想把你所依赖的库文件放在CVS上的最重要原因。通过这样做,你能确保,无论你从CVS上导出的是那个版本或那个分支的软件,你都能找到第三方类库的合适版本来支持你的软件的特定版本。
? 根build.xml 文件是主要的管理文件。它知道为了编译每个模块,什么文件和目标(target:译者注,应该是<target,是Ant中的一个语句)是必须的。然后,由模块来保证这些物件(artifact)被正确的编译。
例如,假设一个项目正在编译,现在是编译ordermgt 模块的时候了,根编译文件(root build file)应该知道,去调用ordermgt/build.xml 文件中一个Ant任务来完成这编译。而ordermgt/build.xml 文件应该确切的知道要编译生成ordermgt .jar 文件需要些什么。而且,如果整个项目被编译并合并入一个.ear文件,这个根build.xml 文件应该知道如何去构建。
根build.xml 文件是怎么知道要去编译一个模块并且以何种顺序来编译的呢?下面是一个Ant XML文件的一部分,它显示了build.xml文件是如何完成设定的目标的:
<!-- =========================================
Template target.
Never called explicitly,
only used to pass calls to underlying
children modules.
========================================= -- <target name="template" depends="init"
<-- Define the modules and the order in which
they are executed for any given target.
This means _order matters_.
Any
dependencies that are to be satisfied by
one module for another must be declared
in the order the dependencies occur. --
<echoExecuting "${target}" target for the core module...</echo
<ant target="${target}" dir="core"/
<echoExecuting "${target}" target for the admin module...</echo
<ant target="${target}" dir="admin"/
...</target
无论根build.xml 文件调用了哪个编译目标,都由这个template 目标负责以一定的顺序传递给相应的子模块。例如,如果我们想要清理整个工程,我们应该只需在工程的根部调用clean 目标即可,然后下面的任务将被执行:
<!-- =========================================
Clean all modules.
========================================= --<target name="clean" depends="init"
<echoCleaning all builds"</echo
<antcall target="template"
<param name="target" value="clean"/
</antcall</target
根build.xml 文件通过直接调用template 目标来间接地实现调用clean 目标,从而保证了所有模块都被清理。
上面的模块组织和相关的编译目标真地使管理源码和编译变得更容易了。这种结构有助于你更快,更容易地找到你想要的源码。而template 目标负责管理任务是如何执行的。
但模块结构最精彩的部分是这里:
在完成了整个工程的完整编译后,可以对任何模块进行独立的编译。只要在命令行中切换到该模块目录下,并执行:
ant target