摘要:
编译环境对于今日的Java企业级应用程序来说,越来越难于管理了。堆积如山的代码,配置文件,以及对第三方的依赖(third-party dependencies)都使得管理编译环境变得困难。本文将展示一个Ant编译环境的例子,它来自我对多年来的多个项目的经验的修改。此时此地,它或许不是最好的方案,但是它的确经历了时间的考验,也一定会帮助你建立并运行在大多数项目上,不管是大是小。版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
英文原文地址:
http://www.onjava.com/pub/a/onjava/2005/06/22/modularant.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43716_Ant.html
关键词: Ant Compile
编译环境对于今日的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. -->
<echo>Executing "${target}" \
target for the core module...</echo>
<ant target="${target}" dir="core"/>
<echo>Executing "${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">
<echo>Cleaning all builds"</echo>
<antcall target="template">
<param name="target" value="clean"/>
</antcall>
</target>
根build.xml 文件通过直接调用template 目标来间接地实现调用clean 目标,从而保证了所有模块都被清理。
上面的模块组织和相关的编译目标真地使管理源码和编译变得更容易了。这种结构有助于你更快,更容易地找到你想要的源码。而template 目标负责管理任务是如何执行的。
但模块结构最精彩的部分是这里:
在完成了整个工程的完整编译后,可以对任何模块进行独立的编译。只要在命令行中切换到该模块目录下,并执行:
> ant target
然后那个模块的build.xml 文件就会被执行。你可以在编译的任何级别下运行任何目标,而且只对该级别进行编译。
为什么这点很重要?因为它容许你独立地工作在你的模块中,并且只对该模块进行编译。这样,每次你对该模块源码的修改都不需要你重新编译整个工程,对于一个巨大的工程而言,这将节省很多时间。
现在,让我们来看看一个独立的模块的内部是如何构造的。
模块的内容
我们按照一般的Java业界习惯来组织模块的目录结构,以便管理源码。尽管有很多不同的习惯,我们的编译环境中使用一下的目录结构:
modulename
|-- build/
|-- etc/
|-- src/
|-- test/
|-- build.xml
下面是每个节点的含义:
· build: 这个目录比较特殊,它是由模块的编译而产生的。除了它以外,上面所列出的其他文件和目录都会被加入修订控制系统(RCS:Revision Control System)。build目录中包含编译过程中所产生的所有文件,从自动生成的XML文件到编译完成的Java .class文件,以及最终的任何发布文件(distribution artifacts)(如.war, .jar, .ear,等等)。这使得清理编译变得很容易,只要删除这个目录就好了。
· etc: 这个目录中存放编译或运行时(run time)模块所需的配置文件。大多数时候,你会在这里找到属性文件和XML配置文件,例如log4j.properties 或 struts-config.xml。假设有很多文件,他们通常被存放在他们相关组件的子目录中。例如:etc/spring/, etc/struts/, etc/ejb/, 等等。
· src: 这是你源文件目录树的根目录。这里除了与包和(或者)路径相对应的目录之外,就别无他物了。因此,在这里你经常会看到com/ 或 net/ 或 org/ 目录,作为com.whatever 或 net.something 或 org.mydomain 包的开始。要注意,只有和classpath有一一对应关系的东西才能放在这个目录中。例如:包目录,或 .java源文件。
· test: 这个目录用来存放你的测试类文件。(例如,JUnit测试单元)。从目录组织的角度出发,这里最重要的是,这里的包结构是src 目录的严格镜像。这很便于管理测试程序,因为你立即就会知道:moduleroot/test/com/domain/pkg/BusinessObjectTest
是对moduleroot/src/com/domain/pkg/BusinessObject的进行的测试。这个简单的镜像技术对于管理大量的code是非常有用的。它使你很容易的找到你的测试程序。
· build.xml: 这个Ant文件知道如何去做每件这个模块需要做的事情,以完成编译和分配它所负责的物件。如果当前模块含有子模块,那么它也知道如何去编译以及按何种顺序去编译那些子模块。子模块和编译顺序都是我们很快要解释的,非常重要的概念。
子模块
子模块就是另外一个模块(父模块)的子集。你或许看过其他的模式――基于Ant的扁平层级:例如,只有一层深度的结构。而我们的编译结构要走的更远一些:我们有2层。
继续我们的编译和子模块的概念,你将会看到如下的一个编译层次,模块和子模块目录展开如下:
module1/
submodule1.1/
|-- etc/
|-- src/
...
|-- build.xml
submodule1.2/
|-- etc/
|-- src/
...
|-- build.xml
build.xml
module2/
...
OK, 这个看起来有点复杂。我们为什么需要这个?
让我们补充点企业级应用和物件驱动的背景知识,再来回答这个问题。
企业级应用程序大部分情况下都是基于客户/服务器模式的。即便你只是开发一个网页应用程序,它通常构成一个客户/服务器MVC应用。这样,网页本身就是一个客户视角,而“服务器”端组件通常是商业POJO,它代替发布网页组件执行商业逻辑。尽管它们在一个 .war文件中,但是在主要用于绘制视图(rendering a view)(客户端代码)的代码和用于处理商业请求(服务器端代码)的代码之间有明确的架构分离。至少,在这里是这样的!
在传统的客户/服务器应用程序中,客户和服务器的概念更加明显。那里有独立的客户GUI通过socket和服务器端的商业对象进行通信。
如果对于客户应用程序我们只需要改动客户端代码,对于服务器端程序只改动服务器端源码,那将是简洁而优雅的。这两层也共享一些通用代码,因此向客户端和服务器端发送共用的.jar文件也是好主意。我们的编译环境有能力实现我们的想法。
接下来我们会看到子模块是如何帮我我们实现物件驱动编译的。
分级结构和编译物件
部署场景(deployment scenario)只描述了物件驱动编译的表层需求:编译环境中的每个模块或子模块复杂创建一个会被部署到客户或服务器端的物件。在我们的编译环境中这个很容易实现,只要把已有的模块进一步分解为common, client, 和 server 子模块就好了。父子关系以及编译责任委托也促成这种编译层级结构。
以我们示例程序中的admin 模块为例,让我们看看在展开的目录树中这种层级结构是怎样的:
appname/
|-- admin/
|-- common/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- client/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- server/
|-- etc/
|-- src/
|-- test/
|-- build.xml
|-- build.xml
...
每个子模块的内容都按照先前定义的构建,但是有些值得注意的不同:
admin 模块没有通常模块的那些内容。它只含有子模块和一个build.xml文件,而且它自己也不会产生任何编译产物。而是通过先前描述的模板(template)技术来调用common/build.xml, server/build.xml, 和 client/build.xml 文件中的编译目标来完成编译工作的。
因此,如果你想编译admin 模块,你只需要切换到admin目录下,然后运行Ant:
> cd admin/
> ant
这个命令会调用admin的build.xml 文件,其结果是编译common, server, 和 client 子模块。在每个子模块被编译后,将会产生三个物件:
appname-admin-common.jar
appname-admin-server.jar
appname-admin-client.jar
common 和 server .jars 文件可以被部署到服务器端(例如,在一个 .ear文件中),而common and client .jars 文件可以被部署到客户端(如,在 .war文件的WEB-INF/lib 目录中)。
每个子模块的目的是什么呢?它们帮助以简洁的功能性子集来组织代码,这些子集会被部署到应用程序的不同层面上。一下是上述三个子模块通常所含有的内容:
· common: 模块中,所有在客户和服务器层都会用到的代码。这通常意味这POJO接口,单元类,等等。
· server:服务器层所需类的实现。它们通常是商业POJO接口的实现,DAO为EIS连接的实现,等等。
· client: 客户层所需类的实现,例如Swing GUI对象,EJO远程接口,等等。
这种子模块的独立性(granularity)及其相应的部署物件从4个方面对你产生实质性帮助:
1. 下载时间:你可以确保独立的客户端应用程序,例如applet或Java Web Start 程序,只需下载程序运行所需的.jar文件的最小子集。这样可以确保第一次运行的applet或应用程序能尽快的被下载。
2. 依赖性管理: 通过子模块的build.xml 文件中Ant的<path> 标识,你可以添加当前子模块所依赖的其他模块或子模块。这样避免了任何懒惰或意外的使用了开发者不支持的或在运行时不支持的API
。
3. 依赖性顺序: 因为父模块决定了子模块的编译顺序,因此你可以重新设定以确保你写的客户端代码依赖于common 代码,而不依赖于服务器端代码。同样,common 代码不能依赖于服务器或客户端代码。如果你没有这么做,编译就会中止,你就会立即的得到警告,你用了你不该使用的类。这听起来好像是很琐碎的问题,但是在复杂的工程或那些程序员有着不同的经验的情况下,这个问题会迅速浮出水面,而且不会在依赖性管理中被注意到。
4. 正如同处理模块一样,你也可以通过切换到子模块的目录里并运行以下命令来对一个子模块进行单独编译:
> ant
而Ant会只编译该模块,节省了时间。
结论
模块和子模块看起来有点复杂。在这点上它们在你看来好象没此必要。但请相信我的经验,它们极大的简化你如何管理源码和关联文件,以及Ant是如何编译你的产品的。在这里定义的结构真真正正地使得产品特性和源码管理在团队环境下变得更容易了。它剔除了为了实现全部组织工作所臆想的许多任务,而且一旦建立起来,它是相当透明的。如果你正在开始一项新的客户/服务器模式的项目,那么就试着应用它。你会有更多的时间投入到你的程序中,而不用在为配置管理而但心。
特别感谢Transdyn delivers 的Jeremy Haile,他提供了有意义的信息和审阅了本文。